npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@quanticjs/react-ui

v9.1.1

Published

UI component library for QuanticJS — Spinner, Toast, Dialog, Skeleton, DataTable, ErrorBoundary, EmptyState

Readme

@quanticjs/react-ui

Cross-cutting UI primitives for QuanticJS apps. Built with Tailwind CSS.

Install

pnpm add @quanticjs/react-ui

Peer dependencies: react, react-dom.

Requires @quanticjs/tailwind-preset >= 8 — components render with the preset's v8 token utilities (shadow-surface|raised|overlay, z-(--z-*), animate-*); on an older preset those compile to nothing. See docs/MIGRATION-8.md.

Components

Button

Canonical button. Defaults to type="button" (pass type="submit" explicitly in forms).

import { Button } from '@quanticjs/react-ui';

<Button onClick={save}>Save</Button>
<Button variant="outline" size="sm">Cancel</Button>
<Button variant="destructive">Delete</Button>
<Button variant="ghost" size="icon" aria-label="Settings"><GearIcon /></Button>
<Button variant="link">Learn more</Button>
  • variant: 'primary' | 'secondary' | 'outline' | 'ghost' | 'destructive' | 'link' (default 'primary')
  • size: 'sm' | 'md' | 'lg' | 'icon' (default 'md')
  • Solid/outline variants carry the surface elevation tier; ghost/link stay flat. Inline <svg> icons auto-size to 1rem.
  • All native <button> props pass through; className merges.

Badge

Generic inline label/count badge — for non-semantic tags ("New", a version, a role name, a count). For status meaning, use StatusBadge instead.

import { Badge } from '@quanticjs/react-ui';

<Badge>New</Badge>
<Badge variant="secondary">v8</Badge>
<Badge variant="outline">Draft</Badge>
<Badge variant="destructive">Overdue</Badge>
  • variant: 'default' | 'secondary' | 'destructive' | 'outline' (default 'default')
  • Renders a <span>; all native span props pass through; className merges.

Card

Surface container family for grouped content.

import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from '@quanticjs/react-ui';

<Card>
  <CardHeader>
    <CardTitle>Profile</CardTitle>
    <CardDescription>Your account details</CardDescription>
  </CardHeader>
  <CardContent>…</CardContent>
  <CardFooter><Button>Save</Button></CardFooter>
</Card>

DescriptionList

Semantic <dl> label/value list for detail and profile views.

import { DescriptionList, DescriptionItem } from '@quanticjs/react-ui';

<DescriptionList columns={2}>
  <DescriptionItem label="Name">Admin User</DescriptionItem>
  <DescriptionItem label="Email">[email protected]</DescriptionItem>
  <DescriptionItem label="Phone" /> {/* renders an em dash */}
</DescriptionList>
  • columns: 1 | 2 | 3 (default 1) — responsive grid from the sm breakpoint.

StatusBadge

Canonical status indicator — use instead of hand-rolled status pills.

import { StatusBadge } from '@quanticjs/react-ui';

<StatusBadge variant="success">Active</StatusBadge>
<StatusBadge variant="destructive" appearance="solid">Disabled</StatusBadge>
  • variant: 'success' | 'warning' | 'destructive' | 'info' | 'neutral'
  • appearance: 'dot' (default — colored dot + foreground text, calm enough to repeat on every table row) or 'solid' (filled pill for a single emphatic status). Both are WCAG AA safe by construction.

Spinner

Accessible loading indicator with three sizes.

import { Spinner } from '@quanticjs/react-ui';

<Spinner />           // default (md)
<Spinner size="sm" />
<Spinner size="lg" />
<Spinner className="text-primary" />

| Prop | Type | Default | |---|---|---| | size | 'sm' \| 'md' \| 'lg' | 'md' | | label | string | 'Loading' | | className | string | — |

Renders a role="status" div with the label as aria-label — pass a translated string for non-English apps.

Skeleton

Placeholder pulse animation for loading states.

import { Skeleton } from '@quanticjs/react-ui';

<Skeleton className="h-4 w-48" />       // text line
<Skeleton className="h-24 w-full" />    // card
<Skeleton className="h-10 w-10 rounded-full" />  // avatar

| Prop | Type | Default | |---|---|---| | className | string | — |

ToastProvider / useToast

Toast notification system with four types and ApiError support.

Setup

Wrap your app (once, typically in the root):

import { ToastProvider } from '@quanticjs/react-ui';

<ToastProvider duration={5000}>
  <App />
</ToastProvider>

| Prop | Type | Default | |---|---|---| | duration | number | 5000 (ms) | | dismissLabel | string | 'Dismiss'aria-label of every toast's dismiss button; override per toast via toast.success('…', { dismissLabel: '…' }) |

Usage

import { useToast } from '@quanticjs/react-ui';

const toast = useToast();

toast.success('Project created');
toast.error('Something went wrong');
toast.warning('Rate limit approaching');
toast.info('New version available');

// With description
toast.success('Saved', { description: 'All changes have been saved' });

// Custom duration
toast.info('Copied', { duration: 2000 });

// Dismiss programmatically
const id = toast.info('Processing...');
toast.dismiss(id);

ApiError support

toast.error() accepts an Error object. If the error has a .detail property (like ApiError), it's shown as the description:

try {
  await client.post('/items', data);
} catch (err) {
  toast.error(err as Error);
  // Title: "Validation failed"  (from error.message)
  // Description: "Name must be unique"  (from error.detail)
}

Dialog

Modal dialog with portal, backdrop, Escape key handling, body scroll lock, focus trapping, and automatic ARIA labelling.

import { Dialog, DialogHeader, DialogBody, DialogFooter } from '@quanticjs/react-ui';

const [open, setOpen] = useState(false);

<Dialog open={open} onClose={() => setOpen(false)}>
  <DialogHeader>Confirm Delete</DialogHeader>
  <DialogBody>
    Are you sure you want to delete this item? This cannot be undone.
  </DialogBody>
  <DialogFooter>
    <button onClick={() => setOpen(false)}>Cancel</button>
    <button onClick={handleDelete}>Delete</button>
  </DialogFooter>
</Dialog>

Dialog Props

| Prop | Type | Description | |---|---|---| | open | boolean | Controls visibility | | onClose | () => void | Called on backdrop click or Escape key | | className | string | Applied to the dialog panel | | aria-label | string | Accessible name when there is no DialogHeader | | aria-describedby | string | Forwarded to the dialog panel |

Sub-components (DialogHeader, DialogBody, DialogFooter) accept children and className.

The panel is automatically labelled by its DialogHeader (aria-labelledby); without one, pass aria-label. To choose which element receives focus on open, mark it with data-autofocus. See Accessibility for the keyboard behavior.

EmptyState

Zero-data placeholder with optional icon and action.

import { EmptyState } from '@quanticjs/react-ui';

<EmptyState
  icon={<FolderIcon className="h-12 w-12" />}
  title="No projects yet"
  description="Create your first project to get started."
  action={<Button onClick={handleCreate}>Create Project</Button>}
/>

| Prop | Type | Description | |---|---|---| | icon | ReactNode | Optional icon above the title | | title | string | Main message | | description | string | Secondary text | | action | ReactNode | CTA button or link | | className | string | — |

DataTable

Typed, dependency-free table for list views: declarative columns, controlled sorting, controlled (server-side) pagination, loading/empty/error states, clickable rows, sticky header.

import { DataTable, type ColumnDef, type SortState } from '@quanticjs/react-ui';

interface User { id: string; name: string; email: string; createdAt: string }

const columns: ColumnDef<User>[] = [
  { key: 'name', header: 'Name', sortable: true },
  { key: 'email', header: 'Email' },
  {
    key: 'createdAt',
    header: 'Joined',
    align: 'end',
    cell: (user) => new Date(user.createdAt).toLocaleDateString(),
  },
];

const [sort, setSort] = useState<SortState | undefined>();

<DataTable<User>
  columns={columns}
  data={users}
  rowKey={(user) => user.id}
  sort={sort}
  onSortChange={(next) => setSort(next ?? undefined)}
  pagination={{ page, pageSize, total }}
  onPageChange={setPage}
  onPageSizeChange={setPageSize}
  onRowClick={(user) => navigate(`/users/${user.id}`)}
  loading={isLoading}
  error={isError ? <QueryErrorPanel error={error} onRetry={refetch} /> : undefined}
/>

| Prop | Type | Description | |---|---|---| | columns | ColumnDef<T>[] | Column definitions (see below) | | data | T[] | Rows for the current page | | rowKey | (row: T) => string | Stable React key per row | | sort | { key, direction } | Controlled sort state | | onSortChange | (sort \| null) => void | Clicking a sortable header cycles asc → desc → null | | pagination | { page, pageSize, total } | Controlled pagination; renders the pagination bar | | onPageChange / onPageSizeChange | (n: number) => void | Pagination callbacks | | pageSizeOptions | number[] | Default [25, 50, 100] | | onRowClick | (row: T) => void | Rows become hoverable, focusable, Enter/Space-activatable | | loading | boolean | Skeleton rows when data is empty; subtle top bar when refetching with data | | error | ReactNode | Rendered in place of the body | | emptyState | ReactNode | Default: <EmptyState title="No data" /> | | stickyHeader | boolean | Pair with containerClassName="max-h-96" (or similar) | | labels | DataTableLabels | i18n overrides for loading/empty/pagination text | | className / containerClassName | string | Outer wrapper / scrollable table container |

ColumnDef<T>: key, header, cell? (defaults to String(row[key]); nullish renders empty), sortable?, align? ('start' \| 'end' \| 'center'; direction-aware, follows dir), width?, headerClassName?, cellClassName?. The values 'left'/'right' are deprecated aliases for 'start'/'end' — still accepted, mapped internally.

Behavior notes:

  • Fully controlled. The component never sorts or slices data — fetch the right page server-side (pairs naturally with usePaginatedQuery from @quanticjs/react-query).
  • Stale page after deletes. If total shrinks below the current page offset, the range display clamps (e.g. 10–10 of 10), but the component will not call onPageChange for you — clamp the page in your data layer.
  • Long content. Default-rendered cells truncate with a title attribute; opt out per column via cellClassName (e.g. whitespace-normal). Custom cell renderers control their own layout.
  • Mobile. The table sits in a horizontal scroll container by default.
  • Accessibility. <th scope="col">, sortable headers are real buttons with aria-sort on the th, clickable rows are keyboard-activatable, pagination controls are labelled (override via labels for i18n). While the empty-table skeleton shows, the table is aria-hidden and a role="status" region announces loading.

Pagination

The pagination bar used by DataTable, exported standalone for non-table lists (card grids, etc.):

import { Pagination } from '@quanticjs/react-ui';

<Pagination
  pagination={{ page, pageSize: 25, total: 312 }}
  onPageChange={setPage}
  onPageSizeChange={setPageSize} // omit to hide the page-size select
  labels={{ previousPage: 'Vorherige Seite', nextPage: 'Nächste Seite' }}
/>

QueryErrorPanel

Categorized error panel for query failures — 403 renders an access message (no retry), 404 a not-found message (no retry), anything else a generic message with a "Try again" button when onRetry is provided.

import { QueryErrorPanel } from '@quanticjs/react-ui';

<QueryErrorPanel
  error={error}
  onRetry={refetch}
  labels={{ retry: 'Erneut versuchen' }} // Partial<QueryErrorPanelLabels>
/>

QueryErrorPanelLabels keys: forbiddenTitle, forbiddenBody, notFoundTitle, notFoundBody, errorTitle, retry — all resolvable from the provider catalog (ui.queryErrorPanel).

ErrorBoundary

React error boundary with configurable fallback.

import { ErrorBoundary } from '@quanticjs/react-ui';

// Default fallback (shows error message + "Try again" button)
<ErrorBoundary>
  <Dashboard />
</ErrorBoundary>

// Static fallback
<ErrorBoundary fallback={<p>Something broke</p>}>
  <Dashboard />
</ErrorBoundary>

// Render function fallback (receives error + reset callback)
<ErrorBoundary
  fallback={(error, reset) => (
    <div>
      <p>Error: {error.message}</p>
      <button onClick={reset}>Retry</button>
    </div>
  )}
  onError={(error, info) => Sentry.captureException(error)}
>
  <Dashboard />
</ErrorBoundary>

| Prop | Type | Description | |---|---|---| | fallback | ReactNode \| (error, reset) => ReactNode | What to render on error | | onError | (error, info) => void | Side effect on catch (e.g., Sentry) | | title | string | Heading of the default fallback (default 'Something went wrong') | | retryLabel | string | Reset button text of the default fallback (default 'Try again') |

useTheme / ThemeToggle

Theme management for the light/dark tokens shipped by @quanticjs/tailwind-preset. Persists an explicit 'light' | 'dark' | 'system' preference to localStorage, resolves 'system' against prefers-color-scheme (live — follows OS changes), and toggles the dark class on document.documentElement. All consumers share one store and stay in sync, including across tabs via the storage event.

import { useTheme, ThemeToggle, type ThemePreference } from '@quanticjs/react-ui';

function ThemeMenu() {
  const { theme, resolvedTheme, setTheme } = useTheme();
  // theme: 'light' | 'dark' | 'system' (the preference)
  // resolvedTheme: 'light' | 'dark' (what is actually applied)
  return (
    <select value={theme} onChange={(e) => setTheme(e.target.value as ThemePreference)}>
      <option value="light">Light</option>
      <option value="dark">Dark</option>
      <option value="system">System</option>
    </select>
  );
}

// Or use the ready-made cycling button:
<ThemeToggle />                              // light → dark → system
<ThemeToggle modes={['light', 'dark']} />    // two-state toggle
<ThemeToggle labels={{ light: 'Hell', dark: 'Dunkel', system: 'Auto', switchTo: (n) => `Wechseln zu ${n}` }} />

useTheme(options?) options

| Option | Type | Default | Description | |---|---|---|---| | storageKey | string | 'quantic.theme' | localStorage key the preference persists under | | darkClass | string | 'dark' | Class toggled on document.documentElement | | defaultTheme | ThemePreference | 'system' | Used when nothing (or an invalid value) is stored |

The store is a singleton per storageKey — pass the same options everywhere (differing options log a dev warning; the first-initialized options win). Distinct storageKeys create independent stores (e.g. embedded micro-frontends).

ThemeToggle props

| Prop | Type | Default | |---|---|---| | modes | ThemePreference[] | ['light', 'dark', 'system'] — cycle order | | labels | ThemeToggleLabels | { light: 'Light', dark: 'Dark', system: 'System' } | | labels.switchTo | (nextLabel: string) => string | `Switch to ${next} theme` — the button's aria-label, always naming the next mode | | className | string | — | | …plus all useTheme options | | |

Preventing the flash of wrong theme (FOUC)

useTheme applies the class after hydration. To set it before first paint, inline this script in your HTML <head> (it reads the same storage key):

<script>
  (function () {
    try {
      var t = localStorage.getItem('quantic.theme');
      var dark = t === 'dark' || ((t === 'system' || !t) &&
        window.matchMedia('(prefers-color-scheme: dark)').matches);
      if (dark) document.documentElement.classList.add('dark');
    } catch (e) {}
  })();
</script>

If you customized storageKey or darkClass, mirror the values here.

Edge-case behavior

  • SSR-safe — render never touches window; the server snapshot returns defaultTheme (resolved 'light' for 'system'), so there are no hydration warnings.
  • localStorage unavailable (privacy mode, quota) — the hook works in-memory; persistence is silently skipped.
  • Invalid stored value — treated as defaultTheme, overwritten on the next setTheme.
  • matchMedia absent'system' resolves to 'light'.

Migrating from a local theme toggler

If your app already manages the dark class itself (its own useTheme hook, an inline toggler, etc.), uninstall it when adopting this hook — two writers of the same class fight each other. Delete the local hook, point imports at @quanticjs/react-ui, and keep your old storage key readable by passing it via storageKey (or let users re-pick once under the default quantic.theme).

Form primitives — FormField, Input, Textarea, Select, Checkbox, Radio/RadioGroup, Switch

Presentational, token-styled, RTL-safe form controls plus an accessible field wrapper. They contain no form logic — pair them with @quanticjs/react-forms (useForm, fieldError) for validation and server error mapping.

import { FormField, Input } from '@quanticjs/react-ui';

<FormField label="Email" description="Work email preferred" error={fieldError(form, 'email')}>
  <Input type="email" {...form.register('email')} />
</FormField>

FormField renders the label, optional description, and error, generates stable ids via useId, and provides them through FormFieldContext. The control inside consumes the context automatically and receives:

  • id linked to the <label> (so getByLabelText finds it),
  • aria-describedby referencing the description and error ids (only the ones actually rendered; the error id first so AT announces the problem before the hint),
  • aria-invalid="true" iff error is set,
  • aria-required when required is set (which also renders an aria-hidden visual indicator, default *, overridable via labels.requiredIndicator / catalog key ui.formField.requiredIndicator).

Explicit props always win over context values, and every control works standalone (outside FormField) with consumer-supplied id/aria-*. The error renders in a role="alert" region (text-destructive); the description is text-muted-foreground. One control per FormField — a second context consumer dev-warns.

Custom widgets can join the contract by calling useFormField() (returns FormFieldContextValue | null).

Per-control notes:

  • Inputsize: 'sm' | 'md' | 'lg' (heights h-8/h-9/h-10, matching Button; the native size attribute is type-excluded), invalid for standalone use, and startAdornment/endAdornment ReactNode slots positioned with logical properties. Adornments are decorative by default (aria-hidden, click-through so clicking the chrome focuses the input); interactive adornments (a <button>, an element with onClick/href) automatically stay visible to AT and clickable. type passes through untouched.
  • Textarea — same recipe, invalid prop, no size variants.
  • Select — styled native <select> (size/invalid like Input). appearance-none plus an end-*-positioned chevron, so the OS arrow is never double-rendered. A rich combobox is out of scope (future component).
  • Checkbox / Radio — native inputs (keyboard, forms, and AT support come from the platform), styled via appearance-none + checked: token classes with an inline glyph.
  • RadioGroup — renders role="radiogroup", labelled by the surrounding FormField (or aria-label standalone), and distributes name/checked state/onValueChange/disabled to child Radios via context. Same-name radios get native arrow-key movement. Give each Radio its own per-option <label>.
  • Switch — native checkbox with role="switch": reports its checked state, toggles with Space; the thumb travels via inset-inline-start so it flips under RTL.

All primitives accept className, forward refs, use semantic tokens and the canonical focus ring only, carry disabled:opacity-50 disabled:cursor-not-allowed, and put motion-reduce:transition-none on their transitions.

Field errors for fields without a FormField (e.g. _root from server validation) are not FormField's job — render them via QueryErrorPanel or an inline alert (see the react-forms README).

cn() Utility

Combines clsx and tailwind-merge for conditional class names:

import { cn } from '@quanticjs/react-ui';

cn('px-4 py-2', isActive && 'bg-primary', className);

Utilities

Shared formatters and hooks so consuming packages stop re-implementing them. All formatters are pure functions backed by Intl, accept Date | string | number, and return '' for null/undefined/invalid dates (never "Invalid Date", never a throw). Intl formatter instances are cached per (locale, options), so they are safe to call in table cells.

import {
  formatDate,
  formatDateTime,
  formatRelativeTime,
  formatDuration,
  formatBytes,
  setDefaultLocale,
  useDebounce,
} from '@quanticjs/react-ui';

formatDate('2026-06-10T12:00:00Z');                  // "Jun 10, 2026"  ({ dateStyle: 'medium' })
formatDateTime('2026-06-10T12:00:00Z');              // "Jun 10, 2026, 12:00 PM"  (adds { timeStyle: 'short' })
formatRelativeTime(Date.now() - 180_000);            // "3 minutes ago"  ("in 2 days" for future values)
formatDuration(4_980_000);                           // "1h 23m"  (compact; zero minor units omitted: "1h", "45s")
formatBytes(1_400_000);                              // "1.4 MB"  (decimal units by default)
formatBytes(1_536, { binary: true });                // "1.5 KiB" (1024-based units)
formatDate(value, { locale: 'de-DE' });              // per-call locale override

Locale handling

Every formatter takes an optional opts.locale. Without it, the module-level default applies — set it once at app bootstrap:

import { setDefaultLocale } from '@quanticjs/react-ui';

setDefaultLocale(userProfile.locale); // e.g. 'de-DE'

Until setDefaultLocale is called, Intl uses the runtime locale. Formatters are pure functions called per render — changing the default later affects the next render but does not trigger a re-render by itself, so set it before mounting.

For a reactive locale, wrap the app in a TranslationProvider with a locale prop: components in this package and the domain packages read it via useLocale() and pass it to the formatters, so a locale switch re-renders with the new formatting. setDefaultLocale remains the non-React fallback.

formatBytes options

| Option | Default | Behavior | |---|---|---| | binary | false | true switches to 1024-based units (KiB, MiB, …) | | maximumFractionDigits | 1 | Passed to Intl.NumberFormat |

Zero and negative values render 0 B (negative warns in development).

useDebounce

const debouncedSearch = useDebounce(search, 300); // delayMs defaults to 300

Returns the value once it has stayed unchanged for delayMs. Rapid changes coalesce; changing the delay mid-flight restarts the timer; the pending timer is cleaned up on unmount.

Internationalization

TranslationProvider injects a translation catalog and a locale once, app-wide — no more threading labels props through every call site. The catalog is a plain typed object keyed by package namespace; every component resolves each label with the precedence explicit labels prop > provider catalog > English default, per key. No provider in the tree means English defaults and setDefaultLocale-based formatting — byte-identical to previous behavior.

import { TranslationProvider } from '@quanticjs/react-ui';

<TranslationProvider
  locale="de-DE"
  translations={{
    ui: {
      dataTable: { empty: 'Keine Daten' },
      queryErrorPanel: { retry: 'Erneut versuchen' },
      spinner: { label: 'Wird geladen' },
    },
    // Domain packages register their own namespaces:
    files: { upload: { preparing: 'Upload wird vorbereitet…' } },
    layouts: { shell: { logout: 'Abmelden' } },
  }}
>
  <App />
</TranslationProvider>

The ui namespace (UiTranslations) covers: dataTable (incl. pagination keys), themeToggle, spinner, errorBoundary, toast, queryErrorPanel (QueryErrorPanelLabels: forbiddenTitle, forbiddenBody, notFoundTitle, notFoundBody, errorTitle, retry).

Hooks and helpers

  • useLocale(): string | undefined — the nearest provider's locale; undefined outside a provider (formatters then fall back to setDefaultLocale / runtime locale).
  • useTranslations(namespace) — the catalog slice for a registry namespace, typed via TranslationRegistry.
  • resolveLabels(defaults, ...overrides) — per-key merge (later sources win, undefined skipped, '' is a valid override). Component authors resolve in one line:
const labels = resolveLabels(DEFAULT_LABELS, useTranslations('ui')?.dataTable, props.labels);

Registry augmentation

TranslationRegistry is an augmentable interface — domain packages add their namespace:

declare module '@quanticjs/react-ui' {
  interface TranslationRegistry {
    files?: FilesTranslations;
  }
}

Notes

  • Nested providers deep-merge their catalog over the parent's at leaf level; an unset child locale inherits the parent's. Useful for overriding one namespace in a sub-tree without re-declaring the app catalog.
  • Function-valued labels (e.g. PaginationLabels.range, ThemeToggleLabels.switchTo) handle interpolation/pluralization without an ICU dependency. JSON-only translation pipelines wrap them: range: (s, e, t) => t('ui.range', { s, e, t }). ICU/message-syntax parsing is out of scope.
  • RTL/dir is out of scope — the provider does not set dir.

Accessibility

Dialog keyboard map

| Key | Behavior | |---|---| | Tab | Moves to the next focusable element inside the dialog; wraps from the last to the first | | Shift+Tab | Moves to the previous focusable element; wraps from the first to the last | | Escape | Closes the dialog |

On open, focus moves to the first element marked data-autofocus, else the first focusable element, else the dialog panel itself. Focus cannot leave the dialog while it is open (focus moved out programmatically is pulled back). On close, focus returns to the element that was focused before the dialog opened (or document.body if it was removed). Nested dialogs stack — the innermost dialog holds the trap, and closing it hands focus back to the outer one.

Labelling guidance

  • Give every Dialog an accessible name: use a DialogHeader (wired up automatically via aria-labelledby) or pass aria-label.
  • All built-in accessible strings are customizable for i18n: Spinner label, ToastProvider/per-toast dismissLabel, ErrorBoundary title and retryLabel. Defaults are the English strings listed above.

useFocusTrap

The focus trap is exported for sibling packages building other overlay surfaces (drawers, popovers). It is internal-grade API — minimal and unstyled:

import { useFocusTrap } from '@quanticjs/react-ui';

const ref = useRef<HTMLDivElement>(null);
useFocusTrap(ref, isOpen); // trap while active; restores focus on deactivation

Give the container tabIndex={-1} so it can receive focus when it has no focusable children.

useExitAnimation

Keeps an overlay mounted until its exit animation finishes, so close choreography (Dialog, Toast, drawers, dropdowns) can play out before unmount. Used by every framework overlay and exported for sibling packages and app-level overlay surfaces:

import { useExitAnimation } from '@quanticjs/react-ui';

const { mounted, state, onAnimationEnd } = useExitAnimation(open);
if (!mounted) return null;
return (
  <div
    data-state={state} // 'open' | 'closed'
    onAnimationEnd={onAnimationEnd}
    className={state === 'open' ? 'animate-pop-in' : 'animate-pop-out [animation-fill-mode:forwards]'}
  >
    …
  </div>
);
  • open: truemounted: true, state: 'open' immediately; open: falsestate: 'closed', unmount on the element's animationend (or a ~290ms safety timeout — covers jsdom and animation: none user styles).
  • Re-opening during an exit cancels the unmount without remounting.
  • Key focus traps and scroll locks off open (release on close start), not off mounted, so keyboard users are never trapped during an exit.
  • Under prefers-reduced-motion, the preset's global rule shrinks animations to 0.01ms, so animationend still fires and close is effectively instant.

Testing

This package's components are covered by axe assertions in unit tests. Consuming apps should still run axe (e.g. @axe-core/playwright) in their own E2E suites — page-level issues like color contrast and heading structure can only be caught in the real app.

RTL & reduced motion

  • RTL is attribute-driven. Components use Tailwind logical properties (start-*, ps-*, text-start, border-e, …) exclusively — set dir="rtl" on <html> (or any ancestor) and the toast stack, table alignment, and pagination mirror automatically. There is no DirectionProvider or runtime API. Pagination chevrons flip with the reading direction (rtl:-scale-x-100); ColumnDef.align takes direction-aware 'start' | 'end'.
  • Reduced motion. Spinner, Skeleton, and the DataTable refetch bar carry motion-reduce:animate-none. A frozen Spinner remains perceivable: the static ring plus role="status"/aria-label still communicate loading. The preset's theme.css additionally disables all animation/transition durations globally under prefers-reduced-motion: reduce.
  • A guard test in this package (logical-properties.test.ts) scans every sibling package and fails CI when a physical direction utility (left-*, pl-*, text-left, …) or a literal shadow/z-index (shadow-md/lg/xl, z-10, z-50, z-[60]) is introduced — elevation and stacking must go through the preset's shadow-surface|raised|overlay and z-(--z-*) scales. Justified exceptions go in its ALLOWLIST with a reason.

Exports

cn
Button, type ButtonProps, type ButtonVariant, type ButtonSize
Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter
type CardProps, type CardTitleProps, type CardDescriptionProps
DescriptionList, DescriptionItem, type DescriptionListProps, type DescriptionItemProps
StatusBadge, type StatusBadgeProps, type StatusBadgeVariant, type StatusBadgeAppearance
Avatar, type AvatarProps, type AvatarSize
Progress, type ProgressProps, type ProgressTone, type ProgressLabels
StatCard, type StatCardProps, type StatTrend
Breadcrumb, type BreadcrumbProps, type BreadcrumbItem, type BreadcrumbRenderLinkProps, type BreadcrumbLabels
TranslationProvider, useLocale, useTranslations, resolveLabels
type TranslationProviderProps, type TranslationRegistry, type UiTranslations
formatDate, formatDateTime, formatRelativeTime, formatDuration, formatBytes
setDefaultLocale, type FormatOptions, type BytesOptions
useDebounce
useFocusTrap
useExitAnimation, type ExitAnimationState
Spinner, type SpinnerProps
Skeleton, type SkeletonProps
ToastProvider, useToast, type ToastProviderProps
Dialog, DialogHeader, DialogBody, DialogFooter, type DialogProps
EmptyState, type EmptyStateProps
DataTable, type DataTableProps, type DataTableLabels
Pagination, type PaginationProps, type PaginationLabels
type ColumnDef, type SortState, type PaginationState
QueryErrorPanel, type QueryErrorPanelProps, type QueryErrorPanelLabels
ErrorBoundary, type ErrorBoundaryProps
useTheme, type ThemePreference, type UseThemeOptions, type UseThemeResult
ThemeToggle, type ThemeToggleProps, type ThemeToggleLabels
FormField, useFormField, type FormFieldProps, type FormFieldLabels, type FormFieldContextValue
Input, type InputProps
Textarea, type TextareaProps
Select, type SelectProps
Checkbox, type CheckboxProps
Radio, RadioGroup, type RadioProps, type RadioGroupProps
Switch, type SwitchProps