LOKE Design System
Hooks

useId

Hydration‑safe, deterministic IDs for accessibility and labeling — SSR‑friendly and stable across client/server.

useId

useId returns a unique, deterministic ID string you can use for form controls, ARIA relationships, DOM anchors, etc. It is safe for SSR and hydration: it prefers React’s useId when available (React 18+), and falls back to a counter on the client when needed. You can also pass a fixed deterministicId to force a specific ID.

The returned string is prefixed (e.g., loke-…) for clarity. If you pass a deterministicId, that string is returned unmodified.


API

function useId(deterministicId?: string): string;

Prop

Type

Return value: a string ID you can safely apply to id attributes and ARIA relationships. When no deterministicId is provided, it is guaranteed to be stable between server and client hydration.


Usage

1) Label → input association (accessible forms)

import * as React from 'react';
import { useId } from '@loke/ui/use-id';

export function EmailField() {
  const id = useId();
  return (
    <div className="flex flex-col gap-1.5">
      <label htmlFor={id} className="text-sm">Email</label>
      <input
        id={id}
        type="email"
        className="border rounded px-3 py-2"
        placeholder="you@example.com"
      />
    </div>
  );
}

2) ARIA relationships (description, error text)

import * as React from 'react';
import { useId } from '@loke/ui/use-id';

export function PasswordField({ error }: { error?: string }) {
  const inputId = useId();
  const descId = useId(); // description (helper text)
  const errId = useId();  // error text

  const describedBy = [descId, error ? errId : undefined]
    .filter(Boolean)
    .join(' ') || undefined;

  return (
    <div className="flex flex-col gap-1.5">
      <label htmlFor={inputId} className="text-sm">Password</label>
      <input
        id={inputId}
        type="password"
        aria-describedby={describedBy}
        aria-invalid={!!error}
        className="border rounded px-3 py-2"
      />
      <p id={descId} className="text-xs text-muted-foreground">
        Must be at least 8 characters.
      </p>
      {error && (
        <p id={errId} role="alert" className="text-xs text-destructive">
          {error}
        </p>
      )}
    </div>
  );
}

3) Forcing a deterministic ID

Pass a specific string if you need a fixed ID (e.g., for screenshots, docs, tests):

import { useId } from '@loke/ui/use-id';

export function FixedIdExample() {
  const id = useId('account-email'); // always "account-email"
  return (
    <>
      <label htmlFor={id}>Email</label>
      <input id={id} type="email" />
    </>
  );
}

Behavior & SSR details

  • On React 18+, the hook uses React.useId() when available, returning a hydration‑safe, deterministic ID.
  • On older React or environments where useId is unavailable, the hook falls back to a client‑side counter during layout effect to avoid hydration warnings.
  • If you supply deterministicId, that string is returned as‑is; this can be useful in:
    • server‑only rendering where you must guarantee exact IDs,
    • static docs, or
    • testing snapshots.

The fallback path only runs on the client — on initial server render, the hook returns an empty string then stabilizes on the client. If you want the exact same ID on both sides, provide a deterministicId.


Patterns & tips

  • Generate once per distinct piece of UI you need to reference. For multiple associations (label + description + error), call useId() multiple times to get separate IDs.
  • Prefer aria-describedby (and role="alert" for errors) to communicate additional context to assistive tech.
  • For deeply nested compound components, create IDs in the outermost component and pass them down to subcomponents.

Import

import { useId } from '@loke/ui/use-id';