Skip to content

LiveRegion

SM, under 950B gzipped

A declarative live region for components that track their own message as state.

Category
Feedback & status
Budget
sm, single-purpose control
Runtime deps
None

Preview

Preview language

This is the exact fixture the conformance suite renders in CI. Switch the language to see the component mirror and reformat.

Install

npx shadcn@latest add https://kata-ui-rho.vercel.app/r/live-region.json

Copies the source into your project. Pulls in 1 primitive: visually-hidden.

Source

"use client";

import { useEffect, useState } from "react";
import { VisuallyHidden } from "./visually-hidden";

export interface LiveRegionProps {
  message: string;
  politeness?: "polite" | "assertive";
}

/**
 * A declarative live region for a component that already tracks its own
 * message as state, as an alternative to calling the imperative `announce()`
 * function.
 *
 * Renders empty on mount and only starts reflecting `message` one commit
 * later — the same mount-before-fill requirement `announce()` documents:
 * a screen reader must see this node exist before its text first changes, or
 * most implementations never announce it at all.
 */
export function LiveRegion({ message, politeness = "polite" }: LiveRegionProps) {
  const [content, setContent] = useState("");

  useEffect(() => {
    // Deliberately not "just render `message` directly" — the whole point of
    // this component is the one-commit delay between mount and first fill
    // that makes screen readers actually announce the change.
    // eslint-disable-next-line react-hooks/set-state-in-effect
    setContent(message);
  }, [message]);

  return (
    <VisuallyHidden as="div">
      <div role={politeness === "assertive" ? "alert" : "status"} aria-live={politeness} aria-atomic="true">
        {content}
      </div>
    </VisuallyHidden>
  );
}

What CI checks

← All components