Headless React components, and the first UI library that is Async React-native.
React 19 gave us transitions, actions, useOptimistic, Suspense for data and <ViewTransition>. Component libraries still mostly hand you onClick and onChange and leave the rest to you. FUNSTACK UI starts from the other end: the component owns the transition, shows the optimistic state right away, exposes the pending state as a data attribute, and keeps suspending content on screen until it is ready.
Click the button below. The star flips at once, the button shows a spinner while the fake save runs, and clicking again while it is pending simply wins.
"use client";
import { Toggle } from "@funstack/ui/toggle";
import { fakeSave } from "./fake-api.js";
// Uncontrolled Toggle with an async action. The star flips at once (optimistic), the button
// carries data-pending for the 800 ms the save takes, and a second click while pending wins.
export function ToggleFavorite() {
return (
<Toggle className="demo-toggle" pressedChangeAction={(next) => fakeSave(`favorite=${next}`)}>
{({ pressed, pending }) => (
<>
{pending ? (
<span className="demo-spinner" aria-hidden />
) : (
<span aria-hidden>{pressed ? "★" : "☆"}</span>
)}
Favorite
</>
)}
</Toggle>
);
}/* Toggle: data attributes carry library state; :hover / :focus-visible come from the platform. */
.demo-toggle {
display: inline-flex;
align-items: center;
gap: 0.4em;
padding: 0.4em 0.9em;
border: 1px solid var(--color-border);
border-radius: 999px;
background: var(--color-bg);
color: var(--color-text);
font: inherit;
cursor: pointer;
/* The optimistic flip is a sync commit, so it animates with a plain CSS transition. */
transition:
background-color 120ms,
color 120ms,
border-color 120ms;
}
.demo-toggle[data-pressed] {
background: var(--color-primary);
border-color: var(--color-primary);
color: var(--color-on-primary);
}
.demo-toggle[data-pending] {
cursor: progress;
}
.demo-toggle[data-disabled] {
opacity: 0.5;
cursor: not-allowed;
}
.demo-toggle:focus-visible {
outline: 2px solid var(--color-primary);
outline-offset: 2px;
}
@media (prefers-reduced-motion: reduce) {
.demo-toggle {
transition: none;
}
}The components are headless: they render accessible HTML with data attributes and no styles. You bring the CSS.
Content that suspends is the normal case. Panels keep the old content on screen while the new content loads; no fallback flashes.
Every state change the library makes is a React transition. You do not opt in; urgent updates would be the opt-out.
Interactive props are actions (pressedChangeAction, valueChangeAction). They may be async; the component awaits them and shows pending state.
Every part whose content is driven by library state wraps itself in <ViewTransition>. Pass a transition prop and the change animates.
Popover API, CSS Anchor Positioning, <dialog> with Invoker Commands, native form controls, <details name>, hidden=until-found, ::details-content, interestfor. Where HTML can do the job, it does.
Components follow the WAI-ARIA Authoring Practices and are tested for keyboard and assistive-technology behaviour.
The latest stable version of each major engine (Blink, WebKit, Gecko). No fallbacks. Being modern is the point. Platform features that have not shipped in all three engines are still used, but only behind experimental_* props that you opt into per call site. See Experimental features.
FUNSTACK UI is part of the FUNSTACK family, which aims to be the most modern React stack:
The first four primitives settled the API conventions: Toggle, Tabs, Accordion and Tooltip. The second wave brings overlays and native form controls: Dialog, Switch, Checkbox, RadioGroup and CheckboxGroup. A larger set follows; see the roadmap.
npm install @funstack/ui@alpha