Install the package, import a component from its subpath, and style it with CSS. There is no provider, no theme and no stylesheet to import.
npm install @funstack/ui@alphaReact 19.3 or later is a peer dependency (react and react-dom >=19.3.0). The library relies on React 19.3 features such as <ViewTransition> and addTransitionType. The package is ESM only and has no other runtime dependencies.
Every component has its own subpath, which is the documented form. The root entry re-exports everything, and both resolve to the same files.
import { Toggle } from "@funstack/ui/toggle";
import { Tabs } from "@funstack/ui/tabs";
import { Accordion } from "@funstack/ui/accordion";
import { Tooltip } from "@funstack/ui/tooltip";
// Also works:
import { Tabs, Toggle } from "@funstack/ui";
// Shared types live in the root entry.
import type { ActionDetails, ChangeAction } from "@funstack/ui";Single-element components are plain components (Toggle). Compound components are a namespace of parts (Tabs.Root, Tabs.Tab, ...). The namespace itself is not a component.
"use client";
import { Toggle } from "@funstack/ui/toggle";
import { saveFavorite } from "./api";
export function FavoriteButton({ postId, initial }: { postId: string; initial: boolean }) {
return (
<Toggle
className="favorite"
defaultPressed={initial}
pressedChangeAction={(pressed) => saveFavorite(postId, pressed)}
>
Favorite
</Toggle>
);
}pressedChangeAction runs inside a transition. While the returned promise is pending, the button already shows the new pressed state (optimistic) and carries data-pending. If it rejects, the pressed state rolls back and the error goes to the nearest Error Boundary. The Action props page explains the details.
Components render plain, accessible HTML and ship no styles. Library state appears as data-* attributes, emitted by presence (data-pending="" when true, absent when false). Enum-like states carry a value (data-orientation="vertical").
.favorite[data-pressed] { background: gold; }
.favorite[data-pending] { cursor: progress; opacity: 0.8; }
.favorite[data-disabled] { opacity: 0.5; }
/* Interaction state comes from the platform, not from attributes. */
.favorite:hover { filter: brightness(1.05); }
.favorite:focus-visible { outline: 2px solid; }Hover, focus and active state are deliberately not attributes: use :hover, :focus-visible and :active. Tailwind's data-* variants work as is: data-pressed:bg-amber-300 data-pending:opacity-80.
When CSS is not enough, className, style and children also accept a function of the part's state:
<Toggle
className={({ pressed, pending }) => (pending ? "fav busy" : pressed ? "fav on" : "fav")}
>
{({ pressed, pending }) => (pending ? <Spinner /> : pressed ? <StarFilled /> : <StarOutline />)}
</Toggle>renderEvery part that renders an element accepts render. The element form replaces the default element and merges the library's props into it; the function form hands you the props and the state.
// Element form: a design-system button. It must accept `ref` and spread its props.
<Toggle render={<MyButton variant="ghost" />}>Bold</Toggle>
// Function form: full control.
<Toggle render={(props, state) => <button {...props}>{state.pressed ? "On" : "Off"}</button>} />Merge rules for the element form:
className values are concatenated; style objects are merged (the element wins). Event handlers the part installs run after the element's own.role, ARIA state, ids, data-* state) always win. ARIA id lists (aria-describedby, aria-labelledby, aria-controls) are merged instead.type="button" is only added when the rendered element is a <button>.| Pattern | Meaning |
|---|---|
x / defaultX / xChangeAction | Controlled value, initial uncontrolled value, and the action called on every change. |
data-pending | An action dispatched through this component is in flight. |
transition | Forwarded to the part's <ViewTransition default>. Default "none". |
render | Element polymorphism (no asChild, no as). |
ref | An ordinary prop on every part that renders an element. |
experimental_* | Opt-in use of a platform feature that is not in every engine yet. |