[]previous and next page

Labels and descriptions

Labels and descriptions in the Accessibility guide.

For agentsUpdated

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).

src/field.tsxtsx
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).

field-ssr.tsxtsx
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]}`,
);
Expected output
s0-0-0 s0-0-1

Do 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.