Accessible product-neutral React Aria primitives with compiled StyleX recipes and portable CSS tokens.
@hraness/ui gives Hraness web products one shared interaction and theme layer. The package owns accessible primitives, finite semantic variants, public styling hooks, and framework-neutral composition seams. Each product keeps control of its content, state, data, layout, and visual identity.
Pin the current immutable release:
{
"dependencies": {
"@hraness/ui": "github:hraness/ui#v0.5.6"
}
}Install it with Bun:
bun install
Import the complete package stylesheet before product rules:
@import "@hraness/ui/styles.css"; /* Optional product-level token overrides and styles follow. */
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, CopyButton, } from "@hraness/ui"; export function PreviewCard() { return ( <Card tone="card"> <CardHeader> <CardTitle>Local preview</CardTitle> <CardDescription>A Vite application running on this computer.</CardDescription> </CardHeader> <CardContent><code>http://localhost:5173</code></CardContent> <CardFooter> <CopyButton copyLabel="Copy preview URL" value="http://localhost:5173" /> </CardFooter> </Card> ); }
That render uses one semantic card tree and one React Aria button. CopyButton writes the exact URL, announces success through a live region, and reserves enough width for both its idle and copied labels. The default stylesheet supplies light and dark tokens, focus treatment, coarse-pointer sizing, reduced-motion behavior, and forced-color fallbacks.
React 18 or 19 and React DOM 18 or 19 are peer dependencies.
| Contract | Checked package fact | Public authority |
|---|---|---|
| Runtime and types | ESM consumers load dist/index.js; TypeScript reads src/index.ts |
package.json exports |
| Style delivery | Six public CSS entry points cover precompiled and compiler-adopter paths | package.json exports |
| Theme surface | 37 namespaced theme roles cover surfaces, text, actions, status, charts, typography, and radius | src/tokens.css |
| Interaction states | Components expose semantic data-slot hooks and React Aria state attributes |
Source types and server-rendered tests |
| Compatibility | React and React DOM 18 through 19; StyleX 0.19 for caller-authored xstyle |
Peer and package dependencies |
The package also exports portable spacing, typography, target-size, motion, elevation, layer, and breakpoint scales. Theme the system through roles such as --ui-background, --ui-primary, and --ui-ring; product code does not need generated StyleX class names.
| Reader task | Public interfaces | Composition boundary |
|---|---|---|
| Trigger an action or navigate | Button, CopyButton, IconButton, Link, LinkButton, IconLink, ToggleButton |
React Aria owns input semantics; the caller owns the action and destination |
| Collect and validate input | Form, fields, checkbox and radio groups, switches, native and React Aria selects, file fields |
The caller owns values, validation policy, and submission |
| Select from a collection | Tabs, disclosures, accordions, toggle groups, segmented controls, list boxes, menus | The primitive owns keyboard behavior; the caller owns the items and state |
| Show status or data | Tags, badges, status dots, alerts, spinners, skeletons, progress, meters, sliders, knobs, avatars, tables | The primitive renders state; the caller supplies the state and meaning |
| Structure a surface | Cards, page intros, empty states, settings cards, toolbars, breadcrumbs, pagination, viewport frames, wrapping rows, quiet-site landmarks | The package supplies bounded structure; the product owns page layout and content |
| Connect application seams | RouterProvider, ToastProvider, AskAiAboutThis, icons |
The package stays framework-neutral and does not own application data |
| Apply presentation | Tokens, reset, bounded legacy CSS, compiled StyleX CSS, typed xstyle |
Product tokens, caller recipes, and native styles remain explicit override layers |
@hraness/ui publishes one UI JavaScript entry point and six public CSS entry points:
@hraness/ui/styles.cssprovides the complete theme, reset, bounded legacy recipes, and compiled StyleX recipes.@hraness/ui/tokens.cssprovides standards-only light and dark tokens.@hraness/ui/reset.cssprovides the standards-only baseline and layer order.@hraness/ui/components.cssprovides the remaining legacy component recipes.@hraness/ui/stylex.cssprovides package-compiled StyleX recipes.@hraness/ui/compiler-foundation.cssprovides tokens, reset, and legacy recipes without precompiled StyleX recipes.
styles.css is the precompiled route. It keeps base below components, then fixes the reviewed standalone component sublayers from lowest to highest as legacy, priority1, priority2, priority3, and priority4. Migrated package declarations win over remaining package recipes without depending on generated class names or import timing. The complete stylesheet is standards-based CSS and does not require a utility-CSS processor.
Set data-theme="dark" or the dark class on a root element to select the dark recipe. Set data-theme="light" for an explicit light island. Override namespaced roles after the imports to apply a product theme.
For a standards-only or narrower integration, import the required layers directly:
@import "@hraness/ui/tokens.css"; @import "@hraness/ui/reset.css"; @import "@hraness/ui/components.css"; @import "@hraness/ui/stylex.css";
The built-in recipes are already compiled, so ordinary consumers do not need a StyleX compiler. Applications that compile local StyleX declarations or pass typed xstyle recipes use @hraness/ui/stylex-build with its /bun, /vite, or production /next adapter and the versioned @hraness/ui/stylex-manifest.json. They register every client, lazy, multi-entry, and SSR graph before building, then finalize once. Every HTML or SSR entry links the returned combined recipe stylesheet. Compiler adopters include every registered package's compiler foundation in the stylesheet graph, directly or transitively, and must not also import styles.css or stylex.css.
Compiler adopters install the package's exact build-tool peers: @babel/core@7.29.7, @stylexjs/babel-plugin@0.19.0, lightningcss@1.33.0, and @types/babel__core@7.20.5. TypeScript projects using the Bun adapter also install @types/bun@1.3.14; Vite adapter projects install Vite 7.3.6 or 8.2.1 and compatible Node types (@types/node@^20.19.0 || >=22.12.0). These peers are optional for ordinary precompiled-stylesheet consumers.
The Vite production matrix checks both pinned versions under Node 24 with TypeScript 6.0.3. It covers native client, lazy, multi-entry and SSR builds, public Bundler and NodeNext declarations, package/caller rule union, hydration and browser interaction. Vite 8 uses Rolldown's public module metadata. Both rollupOptions and rolldownOptions remain subject to the same input, output and external-import restrictions. Source maps must be disabled: hidden, inline, copied and late-enabled maps fail closed. This is not development, HMR, React-plugin or arbitrary Vite-version evidence. Native signal regressions verify child/browser/server collection; successful matrix receipts remain outside disposable consumer directories.
Package authors use the lower-level exports from @hraness/ui/stylex-build inside their own build: createStylexTransformCollector compiles package source, serializeStylexPackageRules produces that package's independently usable CSS under a package-owned components.* namespace, and the artifact and manifest helpers bind the resulting JavaScript, CSS, raw rules, and compilerFoundation without independently serialized StyleX rules. That standalone CSS remains the plugin-free package route. Each package must choose a distinct namespace and publish its manifest with the package.
Final applications do not concatenate those independently compiled recipe sheets. They register every participating package manifest and all application graphs, load each package's compiler foundation, and let the finalizer union the raw package and application rules before the fixed serializer runs once. Combined rules use the reserved components.hraness-stylex namespace after every package's legacy layers. This keeps later package foundations from overriding another package's compiled atoms. Identical cross-package rules are emitted once; conflicting rule identities stop the build. Every foundation stylesheet must precede the one finalized recipe stylesheet in the document. Loading a package's styles.css or stylex.css beside that final asset is an unsupported mixed route and fails the checked adapters.
Generation plans and completion records use schema 2 and bind unionPolicySha256 separately from the unchanged package compiler contract. Existing schema-1 package manifests and their standalone stylesheets remain valid inputs. Old generation plans cannot resume under the new delivery policy; create a fresh generation. The public serializeStylexRuleUnionV1 API accepts raw rules and every registered package's standalone serializer. Package foundations and graph stylesheets must not declare or write the reserved union namespace.
The registered src/client.tsx entry imports @hraness/ui/compiler-foundation.css before product CSS. This one-shot Vite build registers that complete client graph, resolves the package manifest through its public export, seals the generated HTML, and publishes only after the graph and template have settled:
import { writeFile } from "node:fs/promises"; import { resolve } from "node:path"; import { build } from "vite"; import { STYLEX_TEMPLATE_CSS_PLACEHOLDER, createStylexGeneration, finalizeStylexGeneration, prepareStylexProducedTemplate, sealStylexProducedTemplate, } from "@hraness/ui/stylex-build"; import { stylexVite } from "@hraness/ui/stylex-build/vite"; const rootDirectory = process.cwd(); const outputDirectory = resolve(rootDirectory, ".stylex-output"); const generation = await createStylexGeneration({ expectedGraphs: [{ adapter: "vite", entrypoints: ["src/client.tsx"], id: "client", kind: "client", }], generationId: "production", outputDirectory, packageManifests: [import.meta.resolve("@hraness/ui/stylex-manifest.json")], rootDirectory, templates: [{ cssHref: "/stylex.css", graphId: "client", outputPath: "index.html", sourcePath: "index.html", stylesheetGraphId: "client", }], }); const result = await build({ build: { minify: true }, configFile: false, plugins: [stylexVite({ generation, graphId: "client", rootDirectory })], }); const outputs = (Array.isArray(result) ? result : [result]).flatMap(({ output }) => output); const foundations = outputs.filter((item) => item.type === "asset" && item.fileName.endsWith(".css")); const [foundation] = foundations; const client = outputs.find((item) => item.type === "chunk" && item.isEntry); if (foundation === undefined || foundations.length !== 1) throw new Error("The client graph did not emit exactly one complete compiler foundation"); if (client === undefined) throw new Error("The client graph did not emit its entry module"); const template = await prepareStylexProducedTemplate(generation, "index.html"); await writeFile(template.sourcePath, [ "<!doctype html><html><head>", `<link rel="stylesheet" href="/graphs/client/${foundation.fileName}">`, `<link rel="stylesheet" href="${STYLEX_TEMPLATE_CSS_PLACEHOLDER}">`, `</head><body><div id="root"></div><script type="module" src="/graphs/client/${client.fileName}"></script></body></html>`, ].join(""), { flag: "wx" }); await sealStylexProducedTemplate(generation, "index.html"); const publishedDirectory = await finalizeStylexGeneration({ generation, outputDirectory, rootDirectory, }); console.log(publishedDirectory);
graphId records the graph that produced a generated template;
stylesheetGraphId records the graph whose complete emitted stylesheet set the
template must link. They can differ when an SSR graph renders HTML for a client
graph. Finalization rejects a missing, duplicate, or foreign graph stylesheet.
The finalizer validates package and graph identities, rejects missing or stale graph receipts and mixed partial CSS, unions all raw rule metadata, and calls the pinned StyleX serializer once. It preserves the full finite priority inventory rather than mapping it to a fixed range. Build-tool modules remain outside the UI runtime entry, runtime CSS injection stays disabled, and the compiler contract pins property-specificity resolution and its Babel, StyleX, and Lightning CSS versions.
The compiler also binds a narrowly scoped StyleX 0.19.0 parser compatibility repair. It verifies the installed Babel plugin's exact source bytes before accepting an explicit terminal EOF token after a complete media query. The repaired plugin runs in memory without modifying installed dependencies or global loader hooks. Media-query ordering remains enabled, malformed trailing input remains invalid, and the same compiler implementation runs under Bun and Node. Source and repair hashes form part of the compiler identity, so adopters must rebuild package manifests and graph receipts together when upgrading from an earlier compiler release.
Install TypeScript 6.0.3 directly as an adopter development dependency. Production build orchestration supports macOS and Linux, where it owns and collects each native POSIX process group before restoring shared files or releasing build ownership. The adapter uses that application's pinned TypeScript parser and leaves native typechecking enabled in both build passes. Each pass selects a generated sibling configuration through Next's typescript.tsconfigPath; the authored configuration and next-env.d.ts are preserved. Only hash-verified prior generated types can be omitted from later typecheck inputs. Unknown historical files remain inputs and can cause a visible error instead of being silently excluded.
Upstream source maps must already use forward-slash repository-logical paths for sources, file, and any nonempty sourceRoot. Absolute machine paths, URLs, traversal, and encoded aliases are rejected before receipts are written. Accepted mapping segments, source order, names, and embedded contents are preserved; the adapter does not silently redact or rebase their provenance.
Verified installed Next framework inputs may be ordinary hardlinked files, as produced by Bun on Linux. Their pinned creator bytes, package identity, and stable file identity are still checked. Generated SSG and dependency-trace outputs must remain ordinary single-link files; installed-input compatibility does not authorize linked output or following dependency-trace filenames.
Ignore .hraness-stylex-next-*.json beside the selected TypeScript configuration, in addition to the configured evidence and output directories. These attempt-owned configuration projections are retained as evidence. Builds targeting different output directories still serialize TypeScript lifecycle ownership within one physical application root. Do not disable Next's output cleaning or bypass type errors.
If an incomplete native pass leaves generated type files that have no complete graph proof, preserve the failed application root, output, and receipts. Rebuild the same authored source and pinned dependencies in a clean isolated application root with a new attempt identifier. Selecting another output directory alone does not hide unverified historical types, and partial writer observations never become exclusion authority. Automatic in-place recovery of that state is unsupported.
The Next adapter supports exactly Next 16.2.12 with webpack 5 under Node 24. That pinned framework writes a different client-reference manifest path than it reads for a static App Router identity beginning with /index, so the adapter rejects that unsupported route before accepting the manifest. Dynamic descendants such as /index/[slug] remain supported and may statically generate a concrete URL such as /index/example. The adapter runs two ordinary next build --webpack processes because Next owns separate client, Node React Server Component, and edge React Server Component compilers and exposes no supported cross-compiler completion barrier in next.config. The discovery build records every repository JavaScript and TypeScript input, chained source map, raw StyleX rule, registered package manifest, CSS input, and emitted asset. The delivery build must reproduce the same source and transform receipts before it can add the single serialized rule union through a native CSS import. Missing, duplicate, or unexpected targets and inter-pass drift stop the build.
Import every registered package foundation from the application root layout. Do not import its standalone styles.css or stylex.css route:
// app/layout.tsx import "@hraness/ui/compiler-foundation.css";
Wrap the production Next config with the compiled public entry. Node never executes TypeScript from node_modules:
// next.config.mjs import { withStylexNext } from "@hraness/ui/stylex-build/next"; import { PHASE_PRODUCTION_BUILD, PHASE_PRODUCTION_SERVER } from "next/constants.js"; const rootDirectory = process.cwd(); const config = { reactStrictMode: true }; export default function nextConfig(phase) { if (phase === PHASE_PRODUCTION_SERVER) return config; if (phase !== PHASE_PRODUCTION_BUILD) { throw new Error("This configuration supports production build and start only."); } return withStylexNext(config, { packageManifests: [ "node_modules/@hraness/ui/dist/stylex-manifest.json", ], rootDirectory, }); }
Run the checked orchestrator instead of invoking next build directly:
// scripts/build-next.mjs import { runStylexNextBuild } from "@hraness/ui/stylex-build/next"; await runStylexNextBuild({ attemptId: "production", packageManifests: [ "node_modules/@hraness/ui/dist/stylex-manifest.json", ], // This hash-bound plan inventory is the only required-source authority. The // child next.config reads it instead of duplicating an unverified list. requiredSources: { client: ["app/global-error.tsx"], edgeRsc: ["app/edge/page.tsx"], nodeRsc: ["app/layout.tsx"], }, rootDirectory: process.cwd(), });
List the complete repository-owned source census for each production target. Keep all three keys; use an empty array only to declare that the target must compile no repository source, such as an application with no Edge route. The adapter still requires at least one source overall and writes discovery and delivery receipts for an observed-empty target instead of skipping it.
The delivery pass preserves webpack source-map chaining and lets Next own CSS deduplication, splitting, filenames, route manifests, and <link> delivery. The adapter verifies emitted entrypoint-to-CSS links from webpack's public compilation data; it does not edit private Next manifests, install a global module hook, or inject an inline style. The production receipt therefore remains compatible with a strict style-src 'self' policy.
complete means the StyleX graph proof is complete. It is not a whole-build integrity, dependency-safety, or deployment-readiness attestation. Next rewrites server dependency traces after webpack; the adapter records their exact initial artifacts and settled final hashes separately as observation-only auxiliary metadata. Only traces belonging to registered Node server entries qualify. These observations never exempt JavaScript, CSS, source maps, or linked assets from their strict checks, and the adapter never follows trace file lists to select or copy dependencies. Next standalone packaging and provider deployments still require their own dependency-selection, packaging, browser, and deployment acceptance checks.
Development and HMR are a separate, unreceipted boundary. This production adapter deliberately rejects next dev, Turbopack, Rspack, and a build config loaded outside runStylexNextBuild; a development run cannot be presented as production graph evidence. next start uses the unchanged runtime configuration without the build wrapper or private build-attempt environment variables. If you select a custom output directory, use the same distDir for serving it. A compiled local preview requires a successful rebuild, an owned server restart, and manual refresh. Build into a new output generation while an existing generation is being served; do not claim HMR or preserved application state. An uncompiled development surface may continue to use the package's precompiled stylesheet route, but it cannot evaluate product-owned StyleX recipes that require compilation.
Unlayered product CSS retains its existing override authority, except for the shared visually-hidden accessibility recipe. Its offscreen reset uses layered important declarations so conflicting unlayered important rules cannot accidentally expose accessible-only copy. Change the component visibility prop instead of overriding this helper.
Quiet personal and project sites can share the same centered page and footer measure without copying layout rules:
import { AppearanceIcon, QuietSiteFooter, QuietSitePage, SocialIcon, } from "@hraness/ui"; <QuietSitePage> <a href="https://instagram.com/example"> <SocialIcon name="instagram" /> <span>instagram</span> </a> </QuietSitePage> <QuietSiteFooter> <button aria-label="Appearance: System"> <AppearanceIcon name="system" /> </button> </QuietSiteFooter>
SocialIcon provides the finite Bluesky, GitHub, Instagram, LinkedIn,
Substack, Threads, X, and YouTube marks used beside visible profile labels.
AppearanceIcon provides the shared Light, Dark, and System glyphs for
controls that own their accessible names. Icon-only SegmentedControl labels
are centered independently of inline text baselines.
Add one quiet AI handoff to a project or individual content page by passing its canonical absolute HTTPS URL:
import { AskAiAboutThis } from "@hraness/ui"; <AskAiAboutThis url="https://hraness.com/stripe" />;
AskAiAboutThis renders the visible label "Ask AI about this" followed by real
outbound links to ChatGPT, Claude, Perplexity, and Grok. Each provider receives
the minimal prompt Tell me about https://hraness.com/stripe, including the
literal full URL. The component has no client state or framework dependency,
works in server-rendered layouts, wraps on narrow surfaces, and rejects
relative, non-HTTPS, credentialed, or malformed subject URLs.
Use React Aria's onPress event for actions. Action controls use the semantic primary, secondary, quiet, and danger variants and the compact, default, large, and transport sizes. Compact and default controls grow to a 48-by-48-pixel minimum target for coarse pointers; large and transport controls keep their larger block sizes and at least the same inline minimum. Icon-only toggles retain the compact inline size at large and transport densities. CopyButton writes one string to the clipboard, announces success, and temporarily swaps to its copiedLabel; both labels always occupy the same grid cell, so the button keeps the wider intrinsic width throughout the transition. IconButton and IconLink require an accessible name and own their hover/focus tooltip; aria-label supplies the default visible copy, while controls named by aria-labelledby must also provide tooltip. Set IconLink to presentation="inline" when an icon-only destination sits beside typographic content. The inline presentation keeps the link semantics, tooltip, centered 24-pixel glyph target, and focus treatment without persistent action-control chrome, and it intentionally does not accept action sizes or variants or join the coarse action-target family.
Knob is a circular, single-value slider for compact numeric controls. It
requires a visible label and a controlled value or uncontrolled
defaultValue. Pointer and touch gestures may move either right or up to
increase the value; left or down decreases it. Shift-drag makes fine
adjustments. Arrow, Home, End, Page Up, and Page Down keys use React Aria's
native range semantics, and name plus form preserve form submission.
In a horizontally scrollable rack, set touchPan="horizontal". Touch users
then swipe left or right to scroll the rack and drag vertically to adjust the
knob; mouse and trackpad pointers retain both adjustment axes.
Use renderValue when compact visible copy should differ from the formatted
accessible value. For example, a rack may show 83 while formatOptions
keeps the native range announcement at 83%.
ProgressBar, Meter, and Slider keep React Aria's accessible range,
keyboard, orientation, and form behavior while their presentation is compiled
with StyleX. Each root accepts a typed xstyle; native style declarations
remain final. Slider keeps a 20-pixel visible thumb inside a 48-pixel target for
real coarse pointers and the synthetic verification seam. Indeterminate
ProgressBar motion stops under reduced motion, and all three families use
system colors in forced-colors mode.
The native Progress primitive remains server-compatible and normalizes
foreign values into a finite native range. Its root accepts typed xstyle
after the package recipe and keeps native style declarations final. The
label association, optional percentage, native progress element, and
browser-specific value and track pseudo-elements remain intact.
Package-owned motion is compiled with StyleX alongside the recipes that use it. Spinners, skeletons, overlays, dialogs, indeterminate progress, and toasts share deterministic keyframes while each component retains its own duration, easing, direction, iteration, and reduced-motion behavior.
<Knob defaultValue={0} formatOptions={{ maximumFractionDigits: 1 }} label="Pan" max={1} min={-1} name="pan" step={0.1} />
Knob also accepts root xstyle and control-only controlXstyle. Its control
remains 48 by 48 pixels at both densities while the visible dial is 40 pixels
by default and 32 pixels when compact. Caller control recipes replace its
native focus fallback without changing the protected range-input geometry.
Tag is a noninteractive compact label. Its optional icon is decorative because the visible label carries the meaning. Use default for ordinary labels, muted for subdued metadata, and outline for a muted boundary. Add accentColor only when a measured or authored color carries categorical identity. Keep navigation on a native link that contains the tag instead of making the tag itself interactive.
Connect links to a client router once at the application boundary. Internal links then navigate through the router and prefetch once on hover or focus; external, fragment-only, and protocol-relative links never prefetch:
import { RouterProvider } from "@hraness/ui"; <RouterProvider navigate={(href) => router.push(href)} prefetch={(href) => router.prefetch(href)} > <App /> </RouterProvider>
| Boundary | Package authority | Consumer authority |
|---|---|---|
| Interaction | Native elements and React Aria behavior for names, focus, keyboard, pointer, disabled, pending, selected, and invalid states | Business rules, values, validation policy, and side effects |
| Presentation | Portable tokens, finite component recipes, semantic classes, and data-slot values |
Product token overrides, page layout, local composition, and intentional caller styles |
| Routing | A framework-neutral bridge for navigation and intent prefetching | Router choice, route ownership, loading, and error behavior |
| Data | None | Application state, persistence, providers, and access policy |
| Composition | Small primitives and bounded structural surfaces | Product content and higher-level patterns, optionally through @hraness/design-kit |
@hraness/ui is ESM-only and supports React 18 or 19 with the matching React DOM range. It has no dependency on a framework, @hraness/design-kit, or a product repository. Consumers upgrade on their own validation schedule.
Documented package exports, semantic component classes, data-slot values, and public custom properties are the supported integration surface. Generated StyleX class names are implementation details. Base recipes apply first, finite variants and states apply next, a typed caller xstyle applies after them where supported, and a caller's native style remains final.
Interactive primitives preserve React Aria state through data-hovered, data-pressed, data-selected, data-invalid, data-focus-visible, and related attributes. Collapsed disclosure panels remain available to browser find-in-page behavior without retaining expanded-panel inset in the page layout. The shared CSS covers coarse pointers, reduced motion, forced colors, and visible focus.
Override semantic roles after the imports to reskin the whole system without depending on component internals:
:root { --ui-primary: oklch(0.52 0.16 250); --ui-ring: oklch(0.62 0.14 250); --ui-font-heading: "Your Heading Face", ui-monospace, monospace; --ui-radius: 1rem; }
Every primitive accepts className. Actions expose separate wrapper and semantic-control classes plus typed StyleX seams. Button and LinkButton also expose the closed partXstyles.label part for product-owned label layout:
import * as stylex from "@stylexjs/stylex"; const styles = stylex.create({ actionControl: { borderRadius: "var(--radius-lg)" }, actionLabel: { overflow: "hidden", textOverflow: "ellipsis" }, actionWrapper: { maxWidth: "100%" }, }); <Button className="max-w-full" controlClassName="rounded-xl" controlXstyle={styles.actionControl} partXstyles={{ label: styles.actionLabel }} variant="quiet" xstyle={styles.actionWrapper} > Open application </Button>
AskAiAboutThis, Button, CopyButton, IconButton, IconLink, ToggleButton,
LinkButton, Icon, SocialIcon, AppearanceIcon, Avatar, Badge, Tag, StatusDot,
KeyHint, PageIntro, EmptyState, InlineAlert, SettingsCard, DataTable,
ToastProvider, Popover, Tooltip, DialogContent, Menu, MenuItem, MenuSection, MenuSeparator, ListBox, ListBoxItem, ListBoxSection, Link, Card, CardHeader, CardTitle, CardDescription, CardContent,
CardFooter, PressableCard, QuietSitePage, QuietSiteFooter,
SkipLink, Separator, Form, FieldDescription, FieldError, TextField,
TextAreaField, SearchField, NumberField, CheckboxField, CheckboxGroup,
RadioGroup, RadioOption, SwitchField, NativeSelectField, FileField,
SelectField, ViewportFrame, WrappingRow, ThemedSurface, and Toolbar
accept a typed StyleX override. Base declarations are applied first, finite
size, tone, shape, and interaction recipes come next, and the caller recipe is
applied last. CheckboxField exposes controlXstyle separately for its
semantic checkbox label.
ToastProvider exposes separate regionXstyle, toastXstyle, and
closeXstyle seams. Each request-local queue keeps its live-region, content,
action, dismissal, duration, and maximum-visible behavior while the compiled
region, finite tone, mount-arrival, and close-control recipes remain caller
overridable. React Aria Components 1.19 removes a toast immediately on close and
exposes no exit render state, so dismissal is not delayed for a synthetic exit
animation. Toast arrival respects reduced-motion preferences, borders retain
system colors in forced-colors mode, and close targets remain at least 48
pixels for coarse pointers.
Popover and Tooltip accept xstyle on their outer surfaces, with native
style applied last, including React Aria style render functions. Popover
preserves its required accessible label, inner dialog, ref, offsets, and
dismissal behavior. Tooltip remains supplementary to its trigger's accessible
name and preserves controlled state, delay, placement, and disabled behavior.
Popover animations respect reduced motion; both surfaces retain system borders
in forced colors. Shared overlay keyframes remain available to SelectField.
DialogContent exposes xstyle on the modal surface and overlayXstyle on
its backdrop. Its native style, including a React Aria render function,
applies last to the backdrop. The small, medium, and large sizes remain
finite, and the inner dialog retains its title, description, ref, focus trap,
and dismissal behavior. Body and footer render functions receive the same
close callback. Close controls preserve native and React Aria hover/focus
presentation, coarse-pointer targets, and reduced-motion behavior.
Action wrappers accept xstyle, while their nested button or anchor accepts
controlXstyle. Native hover, press, and focus fallbacks remain active for
empty conditional overrides. An effective control recipe selects explicit
React Aria state composition so the caller remains last; native style
declarations still resolve after StyleX. Button and LinkButton accept only
the documented partXstyles.label part.
The shared visually-hidden helper protects accessible-only content without
inline presentation. It owns the CopyButton live region, labeled Spinner,
hidden labels for TextField, TextAreaField, SearchField, NumberField,
CheckboxField, NativeSelectField, FileField, and SelectField, and a
Knob whose outputVisibility is "visually-hidden". These elements retain
the stable hraness-visually-hidden hook before their generated StyleX atoms.
Use showLabel or outputVisibility to change visibility; generated classes
and the helper's important offscreen declarations are implementation details.
SkipLink applies xstyle to its native anchor without changing its hash,
focus-transfer, or native :focus reveal behavior. Native style declarations
resolve after the StyleX recipe.
Separator resolves its physical horizontal or vertical recipe through React
Aria context before applying xstyle. Native style declarations remain last.
Form remains a native React Aria form with its action, method, validation,
submission handler, ref, and inherited context or caller DOM renderer intact.
Its grid recipe keeps a physical zero minimum width and var(--space-6) gap.
Caller xstyle resolves after that recipe, and native style declarations
remain final.
The field family keeps native inputs, textareas, selects, file controls, and
form submission intact while React Aria continues to own field, group, radio,
switch, number-stepper, and SelectField behavior. Each field root accepts
xstyle. Text, textarea, search, number, native-select, and file controls expose
controlXstyle plus their input-specific StyleX seam; checkbox, radio, and
switch controls expose controlXstyle; checkbox and radio groups expose
optionsXstyle; and SelectField exposes triggerXstyle. Field descriptions
and errors also accept a typed xstyle. Base, finite size and surface, and
explicit React Aria state recipes resolve before the corresponding caller
recipe, while native style declarations remain final. Field and number-control
focus-within defaults resolve before controlXstyle, so an ordinary caller
override keeps visible focus and a caller pseudo recipe can replace it.
RadioOption, SwitchField, and SelectField condition their native focus fallback
when React Aria focus state or an effective caller recipe is authoritative.
Internal search-clear and number-stepper controls, plus enabled SelectField
options, keep native hover and focus fallbacks because they expose no caller
StyleX seam.
Compact, default, and large fields preserve their intended geometry under both
the real coarse-pointer media query and the synthetic
--hraness-field-coarse-min verification seam. This includes number-stepper
columns, search clear buttons, radio and switch controls, SelectField triggers
and options, and the native file button. Migrated recipes use explicit
background color and image declarations: ordinary controls clear inherited
images, NativeSelectField retains its two-gradient arrow, and SelectField
retains its SVG chevron. Only the native input placeholder and
::file-selector-button presentation remain in legacy CSS because those
pseudo-elements are not owned by the compiled component recipes.
Quiet-site landmarks and these surfaces also preserve dynamic StyleX inline
values when merging the native style prop, with caller inline declarations
taking precedence.
For logical-size overrides on QuietSitePage, QuietSiteFooter,
ViewportFrame, WrappingRow, and ThemedSurface, use
StyleX's canonical dashed keys such as "inline-size", "max-inline-size",
and "min-inline-size". Under the pinned StyleX 0.19
property-specificity compiler, the camel-case aliases lower to physical
width, max-width, and min-width. These component xstyle types reject
the three camel-case aliases. The canonical keys retain vertical-writing
behavior and share the base recipe's conflict key, so caller-last replacement
remains deterministic. Use width, maxWidth, or minWidth when a physical
override is intentional. ViewportFrame also retains its ordered 100vh,
100svh, and 100dvh height fallbacks in compiled CSS.
Avatar intentionally keeps physical width and height declarations. Its
three finite sizes remain 2rem, 2.5rem, and 3.5rem squares in every
writing mode, equivalent to 32, 40, and 56 pixels at the default root font
size. Caller StyleX recipes can replace either physical dimension before a
native style override is applied.
Badge, Tag, and StatusDot keep finite tone or variant sets and preserve
their compact physical geometry. An outline Tag continues to read the public
--hraness-tag-accent custom property set by accentColor or consumer CSS.
Forced-colors mode replaces Badge and Tag borders with CanvasText.
Card and PressableCard share the finite card, neutral, accent, and
inverse tones plus rounded and rectangular shapes. Their descriptions
inherit the literal public --hraness-card-description custom property, which
callers may override with native style or CSS. One compatibility selector
maps that public property to a private tone value on each Card root, so a nested
Card resets its own description tone. All visual declarations remain in
compiled StyleX. PressableCard remains one semantic React Aria button and
keeps its static string className API. Its native style may be either a
static object or a React Aria state callback; both are merged after compiled
StyleX output. Pointer, pressed, keyboard-focus, disabled, and pending behavior
continue to come from React Aria. Native pseudo-class fallbacks are attached
when a conditional xstyle contributes no compiled presentation. Supplying an
effective xstyle selects the caller-last path: React Aria state recipes remain
active, and the caller recipe resolves after them without a hidden pseudo-class
rule reverting hover, press, or focus values.
Toolbar preserves React Aria's horizontal and vertical arrow-key behavior,
required accessible name, semantic class, slot, ref, and static or render-prop
style. Its native :focus-visible ring is attached when a conditional
xstyle contributes no compiled presentation. Supplying an effective xstyle
removes that fallback so a caller focus recipe remains the last compiled
authority.
KeyHint remains a server-compatible native kbd element. Its semantic class,
slot, ref, attributes, and children remain stable while a typed caller xstyle
recipe resolves after its compact presentation. Dynamic StyleX values merge
before the native style prop, so native declarations remain final.
Breadcrumbs and Pagination remain server-compatible native navigation
landmarks. Breadcrumbs keep a nonempty ancestry, render only the final item as
the current non-link page, and truncate that final label without losing its
separator. Pagination keeps numbered links, disabled boundary spans, ellipses,
and rel="prev" or rel="next" intact while centering on compact viewports.
Both roots accept typed xstyle after their package recipe and keep native
style final. Real and verification coarse-pointer modes apply the
navigation-specific 48-pixel minimum only to links and previous or next
boundaries; ellipses retain the shared compact-target token behavior.
PageIntro, EmptyState, InlineAlert, and SettingsCard remain
server-compatible native content boundaries. They preserve their optional
regions, caller-selected heading levels, semantic classes, slots, refs, native
attributes, and finite tone or shape attributes. Their base recipe resolves
before a tone or shape recipe and a typed caller xstyle; native style
declarations remain final. InlineAlert is non-live by default. With isLive,
danger feedback becomes an assertive alert and the other tones become polite
status regions, while an explicit caller role or aria-live remains
authoritative. Every alert tone uses the system CanvasText border in
forced-colors mode.
DataTable remains a native table inside a horizontally scrollable boundary.
It preserves captions, column headers, finite start, center, and end alignment,
row identities, empty-table semantics, and its table ref. Apply
wrapperXstyle to the overflow and border boundary and xstyle to the native
table. Both caller recipes resolve after their package recipes, while the
table's native style declarations remain final. Cell dividers retain logical
block-end behavior in every writing mode, and the empty cell remains centered
across the complete column span.
ListBox, ListBoxItem, and ListBoxSection preserve React Aria collection,
selection, focus, ref, context, and custom-render behavior. Their typed xstyle
recipes resolve after package orientation and interaction recipes; native
style remains final. Sections also expose headerXstyle for the optional
header. Horizontal layout sizes only direct items and sections, without adding
DOM wrappers or changing dynamic collection children. Section descendants keep
their ordinary grid layout. Focused or hovered items use the accent tokens,
selection adds medium font weight, and disabled opacity remains independent.
Items keep their minimum target under both real and verification coarse-pointer
conditions. Autocomplete continues to own input focus and filtering.
Menu compiles its menu, popover, section, rich item slots, separator, and footer
presentation with StyleX. Use xstyle, popoverXstyle, and footerXstyle on
Menu, xstyle on items and separators, and xstyle plus headerXstyle on
sections. Caller recipes resolve after focus, hover, selection, disabled, and
danger recipes. Item native styles remain final; matchTriggerWidth retains
the trigger-owned minimum width. React Aria keeps collection, keyboard,
typeahead, selection, dismissal, refs, and action behavior. Coarse targets,
reduced-motion animation, and forced-color borders remain compiled contracts.
Link remains an ordinary React Aria destination with a required href, stable
semantic class and slot, optional link ref, native attributes, render-prop
children and style, and router-prefetch behavior on focus or hover intent. Its
base, hovered, and focus-visible presentation is compiled with StyleX. A typed
caller xstyle is composed last inside the state-aware Link presentation
callbacks, so caller hover and focus-visible recipes retain precedence over the
equivalent React Aria state recipes. Native pseudo-class fallbacks stay attached
when a conditional xstyle contributes no compiled presentation and are omitted
for an effective caller recipe. Dynamic StyleX values merge before a static or
state-aware native style prop.
CheckboxField keeps its required label, native checkbox input, form value,
validation, description, React Aria context and render behavior, and stable
field, control, indicator, and label slots. Set showLabel={false} to keep the
required label as the accessible name through the shared visually-hidden
helper. The field root accepts xstyle; the semantic checkbox control accepts
controlXstyle. Both caller recipes resolve after their state recipes, while
the field's native style declarations remain final. The control keeps a
40-pixel minimum target and expands to 48 pixels for coarse pointers. Table and
other product layout stays outside this portable primitive.
import { Search01Icon } from "@hugeicons/core-free-icons"; import * as stylex from "@stylexjs/stylex"; import { Avatar, Card, CardDescription, CheckboxField, Icon, KeyHint, Link, PressableCard, QuietSitePage, StatusDot, Tag, ThemedSurface, Toolbar, WrappingRow, } from "@hraness/ui"; const styles = stylex.create({ avatar: { backgroundColor: "var(--ui-accent)", borderRadius: "var(--radius-sm)", height: "3rem", width: "3rem", }, quietPage: { "max-inline-size": "40rem", }, searchIcon: { display: "block", }, statusPill: { backgroundColor: "var(--ui-accent)", borderColor: "var(--ui-primary)", }, cardOverride: { borderRadius: "var(--radius-sm)", paddingInline: "var(--space-4)", }, checkbox: { gap: "var(--space-3)", }, checkboxControl: { backgroundColor: "var(--ui-secondary)", }, structuralRow: { "inline-size": "16rem", }, texturedSurface: { backgroundImage: "repeating-linear-gradient(135deg, transparent 0 2px, currentColor 2px 3px)", backgroundSize: "4px 4px", }, toolbar: { borderColor: "var(--ui-primary)", ":focus-visible": { outlineColor: "var(--ui-warning)", outlineOffset: "4px", outlineStyle: "solid", outlineWidth: "3px", }, }, keyHint: { backgroundColor: "var(--ui-secondary)", borderColor: "var(--ui-primary)", }, link: { color: "var(--ui-foreground)", ":focus-visible": { outlineColor: "var(--ui-warning)", }, ":hover": { textDecorationThickness: "3px", }, }, }); <Icon icon={Search01Icon} xstyle={styles.searchIcon} />; <Avatar name="Ada Lovelace" xstyle={styles.avatar} />; <Tag accentColor="#D97706" variant="outline" xstyle={styles.statusPill}> Project </Tag>; <StatusDot tone="success" />; <Card tone="accent" xstyle={styles.cardOverride}> <CardDescription>Compiled card presentation</CardDescription> </Card>; <PressableCard onPress={() => openProject()} xstyle={styles.cardOverride}> Open project </PressableCard>; <QuietSitePage xstyle={styles.quietPage}>...</QuietSitePage>; <WrappingRow xstyle={styles.structuralRow}>...</WrappingRow>; <ThemedSurface tone="accent" xstyle={styles.texturedSurface}>...</ThemedSurface>; <Toolbar aria-label="Editor actions" xstyle={styles.toolbar}>...</Toolbar>; <KeyHint xstyle={styles.keyHint}>⌘K</KeyHint>; <Link href="/reference" xstyle={styles.link}>Reference</Link>; <CheckboxField controlXstyle={styles.checkboxControl} label="Include archived projects" showLabel={false} xstyle={styles.checkbox} />;
Keep importing @hraness/ui/styles.css while legacy and StyleX recipes
coexist. It delivers the generated StyleX stylesheet once alongside the legacy
recipes. A release remains gated on downstream consumers proving this CSS
delivery path; generated declarations are not duplicated into components.css.
The package exports cn for conditional consumer class composition while preserving source order. Treat documented component classes and data-slot values as stable styling hooks; prefer token overrides for system-wide changes. Generated StyleX class names are implementation details.
These claims were reviewed on September 2, 2026 against the package manifest, public barrel, token stylesheet, component tests, and checked build scripts.
| Claim | Source of truth | Executable evidence |
|---|---|---|
| Package identity, version, peers, and entry points | package.json and portfolio-inventory.json |
bun run check:portfolio-inventory, bun run test:package |
| Public component and type surface | src/index.ts and exported source modules |
bun run typecheck, bun run test |
| Token names, accessibility fallbacks, and layer order | src/tokens.css, foundations, reset, compiled recipes |
bun run check:stylex-artifacts, bun run test |
| Deterministic package and combined consumer artifacts | Build scripts, versioned metadata, and committed dist |
bun run build, bun run check:committed-dist, bun run check:stylex-compiler-artifacts, bun run check:stylex-determinism, bun run check:stylex-consumer-layers |
| Packed consumer behavior | Packed Bun, Vite, and browser fixtures | bun run test:package, bun run test:vite-adopter, bun run test:vite78-adopter, bun run test:vite-custody, bun run test:packed-bun-browser |
| Pointer, keyboard, writing-mode, and browser cascade behavior | Real gallery scenarios | bun run test:browser |
bun run check runs the complete required sequence. A passing Markdown contract proves that this README matches checked repository facts; it does not replace package, browser, or consumer validation.
No. @hraness/ui has no first-party Tailwind bridge or dependency. Products may use their own utility-CSS tooling independently; the package's complete and narrower CSS entry points are standards-based inputs.
Built-in recipes are already compiled, so ordinary consumers do not. When an application authors local StyleX declarations or passes xstyle recipes, use the public StyleX build contract, one supported adapter for each registered graph, one compiler foundation, and the single finalized recipe stylesheet. Do not combine finalized output with styles.css or stylex.css.
Override public roles such as --ui-background, --ui-primary, --ui-ring, typography roles, and --ui-radius after the package imports. Products can also add local layout and component composition without changing the shared primitive APIs.
Use documented exports, semantic classes, data-slot values, public custom properties, and typed props. Do not depend on generated StyleX class names.
Keep them in the product. @hraness/ui owns portable primitives and tokens, optional @hraness/design-kit owns shared presentation compositions, and each product owns content, state, data, access policy, and local layout.
To consume the package, pin the release shown above, import the complete stylesheet, and render PreviewCard. To propose a primitive or compatibility change, read CONTRIBUTING.md before editing the public surface.
Version 0.2 replaces recipe helpers with semantic, styled primitives and is intentionally breaking. Rename action variants from default, destructive, outline, ghost, and link to the closest role among primary, danger, secondary, and quiet; replace sm, lg, and icon sizes with compact, large, and IconButton/IconLink; and replace TextField isLabelHidden with showLabel={false}. buttonVariants and badgeVariants are no longer exported. Conventional ref values continue to target the semantic button and field root.
Read CONTRIBUTING.md before opening a pull request. Report suspected vulnerabilities privately as described in SECURITY.md.
MIT