Labels and descriptions
Labels and descriptions in the Accessibility guide.
A control needs an accessible name. In order of preference: a visible <label>, wrapping it or tied
with for; aria-labelledby pointing at visible text; aria-label when there is no visible text (an
icon button). placeholder is not a label.
On label and output, both for and htmlFor are accepted and both write the for attribute. Use
for, the HTML name, in new code; htmlFor is accepted for migrated React code.
Ids that connect two elements must be unique within the page and must keep connecting the same two
elements after hydration. useId() returns an id derived from the current position in the owner tree and a
per-owner counter. It contains only letters, digits, - and _, so it is safe in id, for,
aria-describedby lists and querySelector("#" + id).
import { useId } from "@shadow-js/core";
export function Field(props: { label: string; hint: string }) {
const id = useId();
const hintId = useId();
return (
<div>
<label for={id}>{props.label}</label>
<input id={id} aria-describedby={hintId} />
<small id={hintId}>{props.hint}</small>
</div>
);
}
export function Form() {
return (
<form>
<Field label="Email" hint="We never share it." />
<Field label="Name" hint="As on your passport." />
</form>
);
}
Ids are derived from the component's position, not from a counter: each component owner carries the
hydration-scope key as an id prefix, so the same useId() call returns the same string in
renderToString, in a client render and while hydrating, even though the browser creates more
effect owners than the server does. Ids contain only letters, digits, - and _, so they are safe in
querySelector("#" + id).
import { renderToString } from "@shadow-js/web/server";
import { Field } from "./src/field";
const html = renderToString(() => <Field label="Email" hint="We never share it." />);
console.log(
`${/<input[^>]*id="([^"]+)"/.exec(html)?.[1]} ${/<small[^>]*id="([^"]+)"/.exec(html)?.[1]}`,
);
s0-0-0 s0-0-1Do not depend on the exact format of an id (in CSS, in a hard-coded aria-controls elsewhere, in a snapshot
of server HTML): it can change between releases. Generate every reference to an id from the same
useId() value, as above.
Do not generate ids with a counter or Math.random() at module scope or during render: they differ
between server and client and between requests. useId throws outside an owner (NO_OWNER_ID),
because no stable id exists there.