useId generates a unique ID that is stable across server and client renders. This is useful for accessibility attributes like aria-describedby that require matching IDs between elements.
Signature
function useId(): string
Parameters
This hook takes no parameters.Returns
Returns a unique string ID that:- Is stable across renders of the same component instance
- Is unique within the application
- Is consistent between server-side and client-side rendering
- Follows the format
P{rootId}-{incrementId}
Basic Usage
import { useId } from 'preact/hooks';
function TextField({ label }) {
const id = useId();
return (
<div>
<label htmlFor={id}>{label}</label>
<input id={id} type="text" />
</div>
);
}
function Form() {
return (
<form>
<TextField label="Name" />
<TextField label="Email" />
</form>
);
}
Accessibility with ARIA
function Tooltip({ text, children }) {
const id = useId();
return (
<>
<span aria-describedby={id}>{children}</span>
<div id={id} role="tooltip" className="tooltip">
{text}
</div>
</>
);
}
function App() {
return (
<div>
<Tooltip text="This is your username">
<input type="text" placeholder="Username" />
</Tooltip>
<Tooltip text="Must be at least 8 characters">
<input type="password" placeholder="Password" />
</Tooltip>
</div>
);
}
Multiple Related IDs
function FormField({ label, hint, error }) {
const id = useId();
const hintId = `${id}-hint`;
const errorId = `${id}-error`;
return (
<div>
<label htmlFor={id}>{label}</label>
<input
id={id}
type="text"
aria-describedby={`${hintId}${error ? ` ${errorId}` : ''}`}
aria-invalid={!!error}
/>
{hint && <small id={hintId}>{hint}</small>}
{error && <span id={errorId} role="alert">{error}</span>}
</div>
);
}
function App() {
return (
<FormField
label="Email"
hint="We'll never share your email"
error="Please enter a valid email"
/>
);
}
Radio Button Group
function RadioGroup({ label, options, name, value, onChange }) {
const groupId = useId();
const labelId = `${groupId}-label`;
return (
<div role="radiogroup" aria-labelledby={labelId}>
<div id={labelId}>{label}</div>
{options.map((option) => {
const optionId = `${groupId}-${option.value}`;
return (
<div key={option.value}>
<input
id={optionId}
type="radio"
name={name}
value={option.value}
checked={value === option.value}
onChange={onChange}
/>
<label htmlFor={optionId}>{option.label}</label>
</div>
);
})}
</div>
);
}
function App() {
const [size, setSize] = useState('medium');
return (
<RadioGroup
label="Select size"
name="size"
value={size}
onChange={(e) => setSize(e.target.value)}
options={[
{ value: 'small', label: 'Small' },
{ value: 'medium', label: 'Medium' },
{ value: 'large', label: 'Large' }
]}
/>
);
}
Combobox with Listbox
function Combobox({ label, options }) {
const [isOpen, setIsOpen] = useState(false);
const [selected, setSelected] = useState(null);
const id = useId();
const listboxId = `${id}-listbox`;
const labelId = `${id}-label`;
return (
<div>
<label id={labelId}>{label}</label>
<input
id={id}
role="combobox"
aria-labelledby={labelId}
aria-expanded={isOpen}
aria-controls={listboxId}
aria-autocomplete="list"
value={selected?.label || ''}
onFocus={() => setIsOpen(true)}
onBlur={() => setIsOpen(false)}
/>
{isOpen && (
<ul id={listboxId} role="listbox" aria-labelledby={labelId}>
{options.map((option) => {
const optionId = `${id}-option-${option.value}`;
return (
<li
key={option.value}
id={optionId}
role="option"
aria-selected={selected?.value === option.value}
onClick={() => setSelected(option)}
>
{option.label}
</li>
);
})}
</ul>
)}
</div>
);
}
Accordion with Multiple Panels
function AccordionPanel({ title, children }) {
const [isOpen, setIsOpen] = useState(false);
const id = useId();
const buttonId = `${id}-button`;
const panelId = `${id}-panel`;
return (
<div>
<h3>
<button
id={buttonId}
aria-expanded={isOpen}
aria-controls={panelId}
onClick={() => setIsOpen(!isOpen)}
>
{title}
</button>
</h3>
<div
id={panelId}
role="region"
aria-labelledby={buttonId}
hidden={!isOpen}
>
{children}
</div>
</div>
);
}
function Accordion() {
return (
<div>
<AccordionPanel title="Panel 1">
<p>Content for panel 1</p>
</AccordionPanel>
<AccordionPanel title="Panel 2">
<p>Content for panel 2</p>
</AccordionPanel>
<AccordionPanel title="Panel 3">
<p>Content for panel 3</p>
</AccordionPanel>
</div>
);
}
Dialog with Description
function Dialog({ title, description, children, isOpen, onClose }) {
const id = useId();
const titleId = `${id}-title`;
const descId = `${id}-description`;
if (!isOpen) return null;
return (
<div
role="dialog"
aria-labelledby={titleId}
aria-describedby={descId}
aria-modal="true"
>
<h2 id={titleId}>{title}</h2>
<p id={descId}>{description}</p>
<div>{children}</div>
<button onClick={onClose}>Close</button>
</div>
);
}
function App() {
const [isOpen, setIsOpen] = useState(false);
return (
<div>
<button onClick={() => setIsOpen(true)}>Open Dialog</button>
<Dialog
title="Confirm Action"
description="Are you sure you want to proceed with this action?"
isOpen={isOpen}
onClose={() => setIsOpen(false)}
>
<button>Confirm</button>
<button onClick={() => setIsOpen(false)}>Cancel</button>
</Dialog>
</div>
);
}
Tab Panel
function TabPanel({ tabs }) {
const [activeTab, setActiveTab] = useState(0);
const baseId = useId();
return (
<div>
<div role="tablist">
{tabs.map((tab, index) => {
const tabId = `${baseId}-tab-${index}`;
const panelId = `${baseId}-panel-${index}`;
return (
<button
key={index}
id={tabId}
role="tab"
aria-selected={activeTab === index}
aria-controls={panelId}
onClick={() => setActiveTab(index)}
>
{tab.label}
</button>
);
})}
</div>
{tabs.map((tab, index) => {
const tabId = `${baseId}-tab-${index}`;
const panelId = `${baseId}-panel-${index}`;
return (
<div
key={index}
id={panelId}
role="tabpanel"
aria-labelledby={tabId}
hidden={activeTab !== index}
>
{tab.content}
</div>
);
})}
</div>
);
}
useId is especially important for server-side rendering (SSR). It ensures that IDs match between the server-rendered HTML and the client-side hydrated version, preventing hydration mismatches.Don’t use
useId to generate keys for list items. Keys should be based on your data, not generated IDs.Each call to
useId generates a new unique ID. If you need multiple related IDs, call useId once and derive the other IDs by appending suffixes like ${id}-label, ${id}-input, etc.