FUNSTACK UI

HomeGetting startedConceptsComponentsRoadmap
GitHub

FUNSTACK UI is experimental. Part of the FUNSTACK family with FUNSTACK Router and FUNSTACK Static, which build this site.

FUNSTACK UI

Headless React components, and the first UI library that is Async React-native.

Get startedLearn the concepts

Experimental

FUNSTACK UI is experimental. APIs change without notice; releases are published on npm under the alpha dist-tag. Feedback and issues are welcome on GitHub.

What it is

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.

Show code
ToggleFavorite.tsx
"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.css
/* 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.

Principles

  • Suspense-native

    Content that suspends is the normal case. Panels keep the old content on screen while the new content loads; no fallback flashes.

  • Transition by default

    Every state change the library makes is a React transition. You do not opt in; urgent updates would be the opt-out.

  • Action props, not event handlers

    Interactive props are actions (pressedChangeAction, valueChangeAction). They may be async; the component awaits them and shows pending state.

  • <ViewTransition> built in

    Every part whose content is driven by library state wraps itself in <ViewTransition>. Pass a transition prop and the change animates.

  • Greedy about the platform

    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.

  • Accessibility is not optional

    Components follow the WAI-ARIA Authoring Practices and are tested for keyboard and assistive-technology behaviour.

Browser support

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.

The FUNSTACK family

FUNSTACK UI is part of the FUNSTACK family, which aims to be the most modern React stack:

  • FUNSTACK Router: a React router built on the Navigation API.
  • FUNSTACK Static: a React Server Components static site framework. This site is built with it.

Components

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