# GPUIxUI
> The documentation for GPUIxUI, a native UI component library for GPUix and React. Components render through GPUix host elements, not the DOM.
This is every page the site serves, as Markdown, in the order the sidebar
shows them. `/llms.txt` is the same list as an index, one line per page.
The site is a browser application documenting a native library, so a browser
preview is a labelled approximation of the component unless the page says it
mounts the real renderer (`/live`, the wasm build). `.agents/slop/COMPONENTS.md` in the
repository — not a page's existence — is the status record for every
component.
---
# Guide
Source: https://gpuixui.sudokw.com/getting-started
Add a component, render a window, pick a theme.
## Install [#install]
```sh
npx shadcn@latest add @sudokw/primitives-button
```
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@gpuix/react"
}
}
```
React 19. `@gpuix/native` ships prebuilt for Apple Silicon, Linux x64 and
Windows. Any other host builds the binding once, as
[`apps/playground/README.md`](https://github.com/Cyberistic/GPUIXUI/blob/master/apps/playground/README.md#the-binding-on-this-host)
describes.
## First component [#first-component]
```tsx
import { render } from "@gpuix/react";
import { Button } from "@gpuixui/primitives";
import { darkTheme } from "@gpuixui/tokens";
import { ThemeProvider } from "@gpuixui/tokens/react";
render(
Save changes
,
);
```
`render` takes **one** element: a fragment at the root paints only its last
child.
## Theme [#theme]
```tsx
import { getTheme, platformTheme } from "@gpuixui/tokens";
import { ThemeProvider } from "@gpuixui/tokens/react";
const theme = platformTheme(getTheme("dark"), "win32");
{app} ;
```
Modes: `light`, `dark`. Contrast: `default`, `high`. Platforms: `darwin`,
`win32`, `linux`, `default`.
## Next [#next]
- [Components](/components)
- [Blocks](/blocks)
- [Themes](/themes)
- [Registry](/registry)
- [Reference](/reference)
---
# Omarchy
Source: https://gpuixui.sudokw.com/getting-started/omarchy
Develop on Hyprland/Arch, and follow the theme the desktop is running.
## Run [#run]
```sh
git clone https://github.com/Cyberistic/GPUIXUI.git
cd GPUIXUI
nub install
nub run dev:playground
```
## Follow the live theme [#follow-the-live-theme]
```tsx
import { readFileSync } from "node:fs";
import { decodeOmarchyPalette, paletteToTokens } from "@gpuixui/tokens";
import { ThemeProvider } from "@gpuixui/tokens/react";
const file = decodeOmarchyPalette(
readFileSync(
`${process.env.HOME}/.local/state/omarchy/current/theme/colors.toml`,
"utf8",
),
);
{app} ;
```
---
# Build prompt
Source: https://gpuixui.sudokw.com/getting-started/prompt
A copyable build prompt, generated from the pages
Paste this into an agent before it starts. It is generated at build time from the
pages the site ships: component rows from the Components index, block rows from
the block pages' frontmatter.
---
# Terminal
Source: https://gpuixui.sudokw.com/getting-started/terminal
Run the same view inside kitty or Ghostty instead of a window.
`render` takes `terminal: true`, and the runtime paints the app into the terminal it was started from:
```tsx
import { render } from "@gpuix/react";
render( , { terminal: true });
```
`GPUIX_TERMINAL=1` sets the same option, so an unchanged entrypoint runs either way:
```sh
GPUIX_TERMINAL=1 nub src/main.tsx
```
The playground in this repository:
```sh
nub run --filter playground build
GPUIX_TERMINAL=1 nub apps/playground/dist/main.js
```
---
# Examples
Source: https://gpuixui.sudokw.com/examples
Every worked example a package ships, and the file to copy.
Each package keeps a worked example beside its components, generated from `packages/*/examples/*.example.tsx`. They are GPUix modules, not browser components: the page shows the source, not a rendering.
## Running one [#running-one]
```bash
nub run --filter @gpuixui/primitives test:runtime
```
```bash
nub run --filter playground build
nub apps/playground/dist/smoke/run.js --only examples-mount
```
The scenario's checks are in `apps/playground/README.md`.
## Adding one [#adding-one]
Create `packages//examples/.example.tsx` exporting one component with a ` example: …` header, and add it to that package's `examples/index.ts`.
---
# Introduction
Source: https://gpuixui.sudokw.com/introduction
Native React components for GPUix
**Native components, open source, built on GPUix.**
Every component renders through GPUix host elements: no DOM, CSS or browser runtime.
```tsx
import { Badge } from "@gpuixui/components/badge";
import { lightTheme } from "@gpuixui/tokens";
import { ThemeProvider } from "@gpuixui/tokens/react";
Connected
;
```
Compound parts validate their placement: a `DialogTitle` inside `Dialog` but
outside `DialogContent` throws, and the message names the parent and what to do
instead.
```tsx
Rename project
Rename project
Used everywhere the project is named.
Cancel
Save
;
```
## Start here [#start-here]
Clone the workspace, render the first component and pick a theme.
Every component the library ships, with its keyboard contract and API.
Whole windows assembled from the library's parts.
What the pinned runtime provides, what it does not, and what to do instead.
---
# API reference
Source: https://gpuixui.sudokw.com/reference
Every export the workspace publishes
The public API is each package's `exports` map: an import outside it is internal and may move without a major version.
Generated from this workspace's source, on `@gpuix/native` 0.11.0 and `@gpuix/react` 0.11.0.
The filter travels in the URL (`?q=createTheme`).
[`/llms.txt`](/llms.txt) lists every page, grouped the way the sidebar groups it.
[`/llms-full.txt`](/llms-full.txt) is the same pages as one Markdown document:
prose, API tables and limits.
## Where the signatures live [#where-the-signatures-live]
| Question | Where |
| -------- | ----- |
| The props, defaults and consequences of a part | The component's page under [Components](/components) or [Primitives](/primitives) |
| Whether a component ships, and what the runtime measured | [`.agents/slop/COMPONENTS.md`](https://github.com/Cyberistic/GPUIXUI/blob/master/.agents/slop/COMPONENTS.md) |
| Colour, type, spacing, radius, density and platform styles | The [guide](/getting-started) and [`docs/tokens.md`](https://github.com/Cyberistic/GPUIXUI/blob/master/docs/tokens.md) |
| What the runtime provides and refuses | [`docs/gpuix-capabilities.md`](https://github.com/Cyberistic/GPUIXUI/blob/master/docs/gpuix-capabilities.md) |
---
# Registry
Source: https://gpuixui.sudokw.com/registry
Install a component's source with the shadcn CLI.
Every component is a [shadcn registry](https://ui.shadcn.com/docs/registry)
item. `shadcn add` copies the source into your project; it renders through
GPUix, not the DOM.
## Install [#install]
```sh
npx shadcn@latest add @sudokw/primitives-button
```
Or by direct address:
```sh
npx shadcn@latest add https://gpuixui.sudokw.com/primitives-button.json
```
That installs the component, its parts, the `@gpuixui/*` runtime and the npm
packages they import. Items are `-`; list them with
`npx shadcn@latest search @sudokw`.
## Set the JSX runtime [#set-the-jsx-runtime]
```json
{
"compilerOptions": {
"jsx": "react-jsx",
"jsxImportSource": "@gpuix/react"
}
}
```
## Render it [#render-it]
```tsx
import { render } from "@gpuix/react";
import { Button } from "@/components/ui/gpuixui/primitives/components/Button";
import { ThemeProvider } from "@/components/ui/gpuixui/primitives/components/ThemeProvider";
import { darkTheme } from "@/components/ui/gpuixui/tokens";
render(
Save changes
,
);
```
`ThemeProvider` is required. `@gpuix/native` ships binaries for Apple Silicon,
Linux x64 and Windows.
Files land under your `ui` alias in `gpuixui/`:
```
src/components/ui/gpuixui/primitives/components/Button.tsx
src/components/ui/gpuixui/tokens/index.ts
```
The runtime items (`gpuix-tokens`, `gpuix-tw`, `gpuix-motion`,
`gpuix-primitives`) install once per project. The site serves the index at
[`/registry.json`](/registry.json) and one document per item at `/.json`.
---
# Themes
Source: https://gpuixui.sudokw.com/themes
Edit the tokens live, pick a look, import a shadcn theme.
## Reading the audit [#reading-the-audit]
`mutedForeground on muted — 3.90:1 (needs 4.5:1)`. A ratio that **cannot be
computed** prints **unmeasurable** — a wash, not a pass; the prefix names the
report it came from.
Switch box:
`switch height — 22 cannot hold a knob that needs 32`
`switch width — 0 cannot be laid out (needs a size greater than zero)`
Edits are stored per mode over the platform style; a browser that refuses site
storage keeps the editor. **Reset** clears them; **Copy theme** prints it.
## The looks [#the-looks]
## Bring a shadcn theme [#bring-a-shadcn-theme]
```tsx
import { shadcnTheme } from "@gpuixui/tokens";
const theme = shadcnTheme(css, "dark");
```
**Default** is shadcn's neutral theme, audited as warnings.
## styles.css [#stylescss]
## The style reference [#the-style-reference]
### Type scale [#type-scale]
### Spacing, radius and density [#spacing-radius-and-density]
## Blocks [#blocks]
---
# Components
Source: https://gpuixui.sudokw.com/components
Every component the library ships
---
# Accordion
Source: https://gpuixui.sudokw.com/components/accordion
Stacked sections, one or several open
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Accordion,
AccordionContent,
AccordionItem,
AccordionTrigger,
} from "@gpuixui/components/accordion";
What is a GPUix surface?
A tree of hosted elements the renderer lays out and paints.
Why does the panel animate?
Its measured height is the tween target.
;
```
## Composition [#composition]
```text
Accordion
├── AccordionItem
│ ├── AccordionTrigger
│ └── AccordionContent
```
## API reference [#api-reference]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Accordion` | `type` | `"single" \| "multiple"` | required |
| `Accordion` | `collapsible` | `boolean` — single mode only | `false` |
| `Accordion` | `value` | `string \| null` (single) · `ReadonlyArray` (multiple) | — ; a stored open set that is not a list means uncontrolled |
| `Accordion` | `defaultValue` | the same union, for the uncontrolled form | `null` · `[]` — a stored default that is not a list opens no sections |
| `Accordion` | `onValueChange` | `(value: string \| null) => void` · `(values: ReadonlyArray) => void` | — |
| `AccordionItem` | `value` | `string` | required |
| `AccordionItem` | `disabled` | `boolean` | `false` |
| `AccordionTrigger` | `style` | `StateStyle<{ open, disabled, focused }>` | the recipe |
| `AccordionContent` | `children` | `ReactNode` | — |
| any part | `className` | `string` | — |
---
# Alert
Source: https://gpuixui.sudokw.com/components/alert
A persistent, in-flow message about the current state
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Alert,
AlertContent,
AlertDescription,
AlertTitle,
} from "@gpuixui/components/alert";
3 tokens are missing
The platform themes fall back to the default palette.
;
```
## Composition [#composition]
```text
Alert
├── AlertIcon
├── AlertContent
│ ├── AlertTitle
│ ├── AlertDescription
│ └── AlertLink
└── AlertDismiss
```
## API reference [#api-reference]
| Part | Prop | Type | Default | Consequence |
| ---- | ---- | ---- | ------- | ----------- |
| `Alert` | `tone` | `"info" \| "success" \| "warning" \| "danger"` | `"info"` — a stored name the union does not carry | the surface, edge and readable colour every part inherits |
| `Alert` | `children` | parts | — | the composition; a part outside the root throws |
| any part | `className` | `string` | — | joined after that part's recipe; `style` still merges last |
| `AlertLink` | `onSelect` | `() => void` | — | the text action's handler, on click, `Enter` or `Space` |
| `AlertDismiss` | `onDismiss` | `() => void` (required) | — | runs on click, `Enter` or `Space`; there is no optional dismiss |
| `AlertDismiss` | `label` | `string` | `"Dismiss"` | the control's accessible name |
---
# Aspect ratio
Source: https://gpuixui.sudokw.com/components/aspect-ratio
A box whose height follows its width through one ratio
## Usage [#usage]
```tsx
import { AspectRatio } from "@gpuixui/components/aspect-ratio";
export const Still = () => (
);
```
## Composition [#composition]
```text
AspectRatio
└── content
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `AspectRatio` | `ratio` | `number` — width divided by height | `1` |
| `AspectRatio` | `className` | `string` | — |
| `AspectRatio` | `style` | `StyleDesc` — merged last | — |
| `AspectRatio` | `children` | `ReactNode` — the content that fills the box | — |
| `AspectRatio` | `testId` | `string` | — |
---
# Auth
Source: https://gpuixui.sudokw.com/components/auth
Sign in, sign up, reset and one-time codes
### Sign in [#sign-in]
### Sign up [#sign-up]
### Forgot password [#forgot-password]
### One-time code [#one-time-code]
## Installation [#installation]
## Usage [#usage]
```tsx
import { Button, Input, Label, Separator, Stack, Text } from "@gpuixui/primitives";
import {
Card,
CardContent,
CardDescription,
CardHeader,
CardTitle,
} from "@gpuixui/components/card";
export function SignInCard() {
return (
Login to your account
Enter your email below to login to your account
Email
Password
Sign in
GitHub
Don't have an account?
);
}
```
## Composition [#composition]
| Part | Responsibility |
| ---- | -------------- |
| `Card` | the centred surface, `variant="outline"` |
| `CardHeader` / `CardTitle` / `CardDescription` | the mark, the title and the sentence under it |
| `CardContent` | the field column, the actions and the divider |
| `CardFooter` | the route to the neighbouring card |
| `Label` / `Input` | one named field |
| `Checkbox` | the terms gate and the remembered session |
| `OTPField` | the six code cells |
| `Separator` | the "or continue with" divider |
| `Button` | the primary action, the providers, the links and the resend control |
| `Text` | the footer sentence and the inline note |
## API reference [#api-reference]
Each recipe adds its own callback set.
## Recipes [#recipes]
| Member | Contract |
| ------ | -------- |
| `LoginPage` | `onSubmit`, `onProvider`, `pending`, `error` |
| `SignupPage` | `onSubmit`, `pending`, `error` |
| `ForgotPasswordPage` | `onSubmit`, `pending` |
| `OtpPage` | `onSubmit`, `onResend` |
| `SignInPage` | the full page — navbar, form card and quote panel — over the same callbacks |
## Keyboard [#keyboard]
| Key | Contract |
| --- | -------- |
| `Tab` | walks fields, links, the primary action, both providers and the footer in order |
| `Enter` | submits the focused form; the primary action carries the busy state |
| `Space` | toggles the focused checkbox; the terms gate is submitted with the form |
| `Backspace` | in an empty one-time-code cell, moves focus to the previous cell |
| `Tab` from a code cell | leaves the group; the cells are one tab stop each, not a roving group |
---
# Auto height
Source: https://gpuixui.sudokw.com/components/auto-height
A reveal that grows to its content's measured height
## Usage [#usage]
```tsx
import { AutoHeight } from "@gpuixui/components/auto-height";
Retry budget: 5 attempts
Timeout: 30 seconds
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | — |
| `open` | `boolean` (required) | — |
| `level` | `MotionLevel` | `"normal"` |
| `timing` | `SurfaceTiming` | the `level`'s |
| `className` | `string` | — |
| `contentClassName` | `string` | — |
| `style` | `StateStyle` | — |
## Motion [#motion]
The box grows to the content's measured height on the `normal` level (200 ms): it eases out on the way open and in on the way back. A caller that moves a box between two resting states passes its own `timing`, so the height and the fade paint on one curve. The content fades in `20 ms` after the height starts, so the two do not race, and it fades out with the closing box. The subtree stays mounted for the exit window and unmounts after it. Under `MotionProvider preference="reduced"` the duration is zero and the measured height paints at once.
---
# Avatar
Source: https://gpuixui.sudokw.com/components/avatar
A person or team as a picture, falling back to initials
## Installation [#installation]
## Usage [#usage]
```tsx
import { Avatar } from "@gpuixui/components/avatar";
;
// Without a picture the initials are derived from the name.
;
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `name` | `string` | — (required) | the accessible label **and** the initials' source |
| `src` | `string` | — | a path, data URL or `http(s)` URL; without it the initials paint |
| `size` | `"xs" \| "sm" \| "md" \| "lg"` | `"md"` | the diameter from `avatarSize`; a stored name the scale does not carry falls back to `md` |
| `status` | `"online" \| "away" \| "busy" \| "offline"` | — | paints the dot and becomes the node's description; a stored name the union does not carry paints no dot |
| `color` | `string` | the muted role | fills the disc |
| `initials` | `boolean` | `true` | `false` paints a plain colour dot; the name stays the accessible one |
| `radius` | `number` | a circle | a rounded square in place of the circle |
| `clickThrough` | `boolean` | on, unless the avatar has its own press handler | lets a press fall through to the row, card or trigger the avatar sits in; an avatar given `onClick` keeps its press |
| `className` | `string` | — | joined after the root recipe |
| `style` | `StyleDesc \| (state) => StyleDesc` | — | merged last |
---
# Avatar group
Source: https://gpuixui.sudokw.com/components/avatar-group
A facepile of overlapping avatars with an overflow chip
## Usage [#usage]
```tsx
import { AvatarGroup } from "@gpuixui/components/avatar-group";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `members` | `ReadonlyArray` (a value that is not a list reads as none; an entry without a text `name` is dropped) | — |
| `max` | `number` | everyone |
| `size` | `"xs" \| "sm" \| "md" \| "lg"` | `"md"` |
| `overlap` | `number` | quarter of the diameter |
| `ringColor` | `string` | theme `background` |
| `label` | `string` | `${count} avatars` |
| `stepMs` | `number` | `40` |
| `rise` | `number` | `6` |
| `level` | `MotionLevel` | `"normal"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Badge
Source: https://gpuixui.sudokw.com/components/badge
A short status label on a pill, never a control
## Installation [#installation]
## Usage [#usage]
```tsx
import { Badge } from "@gpuixui/components/badge";
Passed
;
main ;
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `variant` | `"neutral" \| "primary" \| "success" \| "warning" \| "danger" \| "info" \| "outline"` | `"neutral"` — a stored name the union does not carry | the fill, ink and edge in **Variants** below |
| `size` | `"sm" \| "md"` | `"md"` — a stored name the union does not carry | the fixed type step and the pill's height in **Sizes** below |
| `className` | `string` | none | joined last |
| `style` | `StyleDesc` | none | merged last |
| `children` | `ReactNode` | — | the label |
**Variants**
| Variant | Fill | Ink | Edge |
| ------- | ---- | --- | ---- |
| `neutral` | `muted` | `mutedForeground` | none |
| `primary` | `primary` | `primaryForeground` | none |
| `success` | `successSoft` | `successSoftForeground` | `success` at 30% |
| `warning` | `warningSoft` | `warningSoftForeground` | `warning` at 30% |
| `danger` | `destructiveSoft` | `destructiveSoftForeground` | `destructive` at 30% |
| `info` | `infoSoft` | `infoSoftForeground` | `info` at 30% |
| `outline` | none | `mutedForeground` | `borderStrong` |
**Sizes**
| Size | Type | Pill height |
| ---- | ---- | ----------- |
| `sm` | 12/16, weight 400 | 20px |
| `md` | 14/20, weight 500 | 24px |
---
# Breadcrumb
Source: https://gpuixui.sudokw.com/components/breadcrumb
The trail to the current page, with a collapsing middle
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Breadcrumb,
BreadcrumbItem,
BreadcrumbLink,
BreadcrumbList,
BreadcrumbPage,
BreadcrumbSeparator,
} from "@gpuixui/components/breadcrumb";
go("Projects")}>Projects
go("GPUIXUI")}>GPUIXUI
breadcrumb.tsx
;
```
## Composition [#composition]
```text
Breadcrumb
└── BreadcrumbList
└── BreadcrumbItem
├── BreadcrumbLink
├── BreadcrumbPage
└── BreadcrumbSeparator
```
## API reference [#api-reference]
| Part | Prop | Type | Default | Consequence |
| ---- | ---- | ---- | ------- | ----------- |
| `Breadcrumb` | `maxItems` | `number` | — | rendered entries to keep; below `3` is clamped to `3`, absent means no collapse |
| `Breadcrumb` | `separator` | `ReactNode` | `/` | the glyph between entries; a node, so a chevron works |
| `Breadcrumb` | `children` | parts | — | `BreadcrumbList` |
| `BreadcrumbList` | `children` | parts | — | the items and any authored separators |
| `BreadcrumbItem` | `children` | parts | — | `BreadcrumbLink` or `BreadcrumbPage` |
| `BreadcrumbLink` | `onSelect` | `() => void` | — | runs on click, `Enter` or `Space` |
| `BreadcrumbLink` | `current` | `boolean` | `false` | renders the current page instead of a link |
| `BreadcrumbPage` | `children` | text | — | the current page; not focusable |
| `BreadcrumbSeparator` | `children` | text | `/` | the default glyph when none is authored |
| all parts | `className` | `string` | — | an extra recipe class, joined last |
---
# Bubble
Source: https://gpuixui.sudokw.com/components/bubble
The message surface, with variants and reactions
A bubble is the surface one message wears. It is independent of `Message`: use
it for a reply, a quoted message, a suggestion or a notification. `Message` owns
the author, avatar, timestamp and row layout; `Bubble` owns the painted box.
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
Bubble,
BubbleAction,
BubbleActions,
BubbleContent,
} from "@gpuixui/components";
Roll it out to the whole team.
;
```
## Composition [#composition]
```text
Bubble
├── BubbleContent
├── BubbleActions
│ └── BubbleAction
└── BubbleReactions
```
`BubbleGroup` stacks consecutive bubbles from one sender:
```text
BubbleGroup
├── Bubble group="first"
├── Bubble group="middle"
└── Bubble group="last"
```
## Variants [#variants]
| Variant | Description |
| ------------- | ---------------------------------------------------------- |
| `default` | The primary fill, usually the reader's own message. |
| `secondary` | The neutral secondary fill. |
| `muted` | A quiet supporting surface. |
| `tinted` | A soft primary tint. |
| `outline` | A bordered surface on the page fill. |
| `ghost` | Unframed prose; it takes the whole row instead of 80%. |
| `destructive` | A soft destructive surface for a failed turn or an error. |
A bubble is at most 80% of its row (`ghost` is 100%). Pass `maxWidth` to change
the cap.
## Grouping [#grouping]
`BubbleGroup` tightens the gap to the theme's `md` (8px). Each bubble names its
place with `group`: `first` joins below, `middle` joins both sides, `last`
joins above and keeps a short tail on the sender's corner, and `single` is the
whole radius. The joined corner is the theme's `sm` step and the outer corners
keep `xl`, so a run reads as one column without losing its frame.
## Shape [#shape]
The surface is shadcn's: `px-3 py-2` (12px by 8px), the `body` type role
(14px/20px), and the theme's `xl` corner. A bubble is at most 80% of its row
(`ghost` is 100%); pass `maxWidth` to change the cap.
## Actions and reactions [#actions-and-reactions]
`BubbleActions` is laid out even while hidden and fades in on pointer-over or
when one of its `BubbleAction`s holds focus, so revealing it never moves the
bubble.
`BubbleReactions` is the pill on the bubble's edge: inset 12px from the
sender's side, a full radius, the `muted` fill and a page-fill ring around it so
it reads as cut out of the surface. It hangs a quarter of its height over the
bubble's bottom edge — `translate-y-3/4`'s crossing, expressed as a negative
inset from inside the column — and the rest of the pill's box lays out below the
edge, so the next row cannot paint over it.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `Bubble` | the row: optional avatar, content column, 80% cap | `variant?`, `align?`, `group?`, `maxWidth?`, `avatar?`, `className?`, `children` |
| `BubbleContent` | the framed surface | `className?`, `children` |
| `BubbleActions` | a named group that reserves its height and fades in | `label?`, `className?`, `children` |
| `BubbleAction` | one small ghost action, and the `Button` busy state | `label` (required), `onClick`, `disabled?`, `className?`, `loading?`, `loadingLabel?`, `leading?`, `trailing?`, `reducedMotion?` |
| `BubbleReactions` | a named pill that hangs over the surface's edge | `label` (required), `className?`, `children` |
| `BubbleGroup` | a tighter column | `gap?`, `className?`, `children` |
---
# Button
Source: https://gpuixui.sudokw.com/components/button
The primary action, with busy and disabled states
## Installation [#installation]
## Usage [#usage]
```tsx
import { Button } from "@gpuixui/primitives";
Save changes
;
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `variant` | `"primary" \| "secondary" \| "outline" \| "ghost" \| "destructive" \| "link"` | `"primary"` | paint from the variant table |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | height and padding from density |
| `square` | `boolean` | `false` | icon-only geometry |
| `fullWidth` | `boolean` | `false` | `width: 100%` |
| `disabled` | `boolean` | `false` | blocks activation, leaves the tab order |
| `tabIndex` | `number` | `0`, or `-1` while `disabled` | the action's Tab stop; a caller's value wins both ways |
| `loading` | `boolean` | `false` | spinner, `Loading` description, blocks activation |
| `loadingLabel` | `string` | the provider's `loading.busy` (`Loading`) | the spinner's accessible name |
| `reducedMotion` | `boolean` | the provider's preference (`false` when none) | lands the busy spinner at rest; the prop wins |
| `leading` / `trailing` | `ReactNode` | none | icon or element beside the label |
| `label` | `string` | none | accessible name; required for an icon-only button |
| `onActivate` | `(event: EventPayload) => void` | none | click or activating key, with that event; not called while busy |
| `onClick` | `(event) => void` | none | raw click, called before `onActivate` |
| `onKeyDown` / `onKeyUp` | `(event: KeyEvent) => void` | none | composed **after** the activation |
| `onFocus` / `onBlur` | `(event) => void` | none | composed before the button's own |
| `onMouseEnter` / `onMouseLeave` / `aria-description` | the tooltip trigger pair and its words | none | a `Tooltip` forwards them; a hover lands on the element itself |
| `ref` | `Ref` | none | the element itself, for a caller that focuses or measures it |
| `className` | `string` | none | joined last |
| `style` | `StyleDesc` | none | merged last |
| `testId` | `string` | none | stable locator |
| `children` | `ReactNode` | none | visible label |
---
# Calendar
Source: https://gpuixui.sudokw.com/components/calendar
A month grid for a day, several days, or a range
## Installation [#installation]
## Usage [#usage]
```tsx
import { Calendar } from "@gpuixui/components/calendar";
import { useState } from "react";
export const ReviewDate = () => {
const [day, setDay] = useState("2026-10-17");
return ;
};
```
```tsx
```
## API reference [#api-reference]
### `Calendar` [#calendar]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `mode` | `"single" \| "multiple" \| "range"` | `"single"` | selects the value shape `selected` and `onSelect` speak |
| `selected` | `string \| null` · `ReadonlyArray` · `{ from, to } \| null` | none | the controlled selection, typed by `mode` |
| `onSelect` | `(day \| days \| range) => void` | none | the next selection; the component never stores it |
| `numberOfMonths` | `1 \| 2` | `1` | months painted side by side, sharing one nav pair |
| `showOutsideDays` | `boolean` | `true` | paints the neighbouring months' days, which are also selectable |
| `captionLayout` | `"label" \| "dropdown"` | `"label"` | a month caption, or month and year jump controls |
| `weekStartsOn` | `0 \| 1` | `1` | the first column: Sunday (`0`) or Monday (`1`) |
| `fromYear` / `toYear` | `number` | today ± 10 | the years a dropdown or a `Shift` page may reach |
| `disabled` | `{ before?, after?, range?, weekdays?, filter? }` | none | refuses days; `before`/`after` also bound paging and the arrows |
| `today` | `string` | the runtime clock | the ISO day painted as today; pass it for a deterministic surface |
| `defaultMonth` | `string` | the selection or today | the ISO day whose month the grid opens on |
| `autoFocus` | `boolean` | `false` | takes focus on mount, for a calendar opened inside a popover |
| `label` | `string` | `"Calendar"` | the grid's accessible name, followed by the selection summary |
| `onDismiss` | `() => void` | none | called by `Escape`; a popover passes its close |
| `className` | `string` | none | joined after the recipe, so it wins conflicts |
---
# Card
Source: https://gpuixui.sudokw.com/components/card
A bounded surface that groups related content
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Card,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from "@gpuixui/components/card";
Pro plan
Everything in Starter, plus seats and invoicing.
{rows}
{actions}
;
```
## Composition [#composition]
```text
Card
├── CardHeader
│ ├── CardTitle
│ └── CardDescription
├── CardContent
└── CardFooter
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `variant` | `"outline" \| "elevated" \| "plain"` | `"outline"` — a stored name the union does not carry | the edge and lift in **Treatments** below |
| `className` | `string` | none | a recipe class string joined after the component's own, so it wins a conflict |
| `style` | `StyleDesc` | none | merged last, after the compiled recipe |
| `children` | `PartChildren` | none | the parts; text and numbers are rejected for every part that hosts parts |
**Treatments**
| Treatment | Edge | Lift |
| --------- | ---- | ---- |
| `outline` | 1px `border` | none |
| `elevated` | none | `elevation.sm` |
| `plain` | none | none |
All three paint the `card` fill and a fixed 8px corner; `outline` edges in
`border-input`, compiled to `border`.
---
# Carousel
Source: https://gpuixui.sudokw.com/components/carousel
Discrete slide stepping, a dot per slide, arrow keys
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Carousel,
CarouselControl,
CarouselIndicators,
CarouselSlide,
CarouselTrack,
} from "@gpuixui/components";
import { useState } from "react";
export const WorkspacePreviews = () => {
const [index, setIndex] = useState(0);
return (
Overview
Activity
Members
);
};
```
## Composition [#composition]
```text
Carousel
├── CarouselTrack
│ └── CarouselSlide
├── CarouselControl
└── CarouselIndicators
```
## API reference [#api-reference]
### `Carousel` [#carousel]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | — | the region's accessible name; required |
| `value` | `number` | — | controlled slide index |
| `defaultValue` | `number` | `0` | the opening slide when uncontrolled |
| `onValueChange` | `(value: number) => void` | none | the next index |
| `loop` | `boolean` | `false` | wraps past both ends and keeps both controls live |
| `className` | `string` | — | joined last |
| `children` | `ReactNode` | none | the track and the controls, directly inside |
### `CarouselTrack` [#carouseltrack]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | none | the slides, as direct children |
| `className` | `string` | — | joined last |
### `CarouselSlide` [#carouselslide]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | none | the slide's content |
| `style` | `StateStyle` | — | `{ active, index }`, merged last |
| `className` | `string` | — | joined last |
### `CarouselControl` [#carouselcontrol]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `direction` | `"previous" \| "next"` | — | which way it steps; required |
| `label` | `string` | `"Previous slide"` / `"Next slide"` | the accessible name |
| `disabled` | `boolean` | `false` | adds to the edge rule; never removes it |
### `CarouselIndicators` [#carouselindicators]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | `"Choose a slide"` | the dot group's accessible name |
| `style` | `StateStyle` | — | `{ active, count }`, merged last |
| `className` | `string` | — | joined last |
---
# Chat
Source: https://gpuixui.sudokw.com/components/chat
A conversation with a following transcript
Chat is the whole conversation: a fixed header, the flexible transcript and a
fixed composer. The transcript is a `MessageScroller`, each turn is a `Message`
around a `Bubble`, and the field is `ChatComposer`. The transport is yours; the
status machine is shared so the composer and the header cannot disagree.
Every turn wears a bubble — the reader's on the `default` fill and the end edge,
the agent's `muted` on the start — and the row supplies the avatar, author and
timestamp. Only the row that starts a run paints its avatar and meta line; the
rows after it are a `MessageGroup` (6px apart) whose bubbles name `group="first"`
and `group="last"` so their inner corners join.
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
Bubble,
BubbleContent,
ChatComposer,
ChatHeader,
ChatMessages,
ChatRoot,
Message,
MessageAvatar,
MessageContent,
MessageHeader,
chatMove,
} from "@gpuixui/components";
{messages.map((message) => (
{message.body}
))}
;
```
## Composition [#composition]
```text
ChatRoot
├── ChatHeader
├── ChatMessages the MessageScroller
│ ├── MessageScrollerViewport
│ │ └── MessageScrollerContent
│ │ └── MessageScrollerItem
│ └── MessageScrollerJumpToLatest
└── ChatComposer
```
## The fixed rows [#the-fixed-rows]
`ChatRoot` is a column whose middle grows. `ChatHeader` and `ChatComposer` do
not shrink; `ChatMessages` is `flex: 1 1 0` with `min-height: 0`, so a row
arriving, a reply streaming or the jump control appearing changes nothing above
or below the transcript. Give `ChatRoot` a bounded height — directly, or through
a sized flex parent — because the transcript needs one to scroll.
The passed-in `ChatMessages` children are rows. Wrap each turn in a `Message`
(or a `MessageGroup` for consecutive rows from one sender). A `Bubble` is always
a child of `MessageContent`; the message never builds one.
## Status [#status]
The four states are `idle`, `submitting`, `streaming` and `error`, and
`chatMove` is the only rule set. Send is refused while a reply is open, the
refused draft stays in the field, Stop emits the stop intent, and Escape stops a
reply. The header announces the state; the failure sentence replaces the status
line in place, so a failed reply does not take height from the transcript.
## While a reply is open [#while-a-reply-is-open]
- Send is replaced by Stop.
- Enter still arrives as the field's image submit; the machine refuses it, so
the draft is kept.
- A reply's row can grow token by token; the transcript follows it only while
the reader is at the end.
## Failed replies [#failed-replies]
The failure copy is one line in the header with the full sentence in
`aria-description`; Retry is beside it. It used to be a row between the viewport
and the composer, which shrank the viewport when it appeared — the layout shift
this release removes.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `ChatRoot` | `role="region"` named by `label` | `status?`, `defaultStatus?`, `onStatusChange?`, `onSubmit` (required), `onStop?`, `onRetry?`, `error?`, `label?`, `className?`, `children` |
| `ChatHeader` | a fixed row: title, status/error, actions | `title?`, `description?`, `actions?`, `className?` |
| `ChatMessages` | the scroller, the empty panel and the jump rail | `empty?`, `emptyTitle?`, `emptyDescription?`, `className?`, `children` |
| `ChatComposer` | a field and its Send/Stop control | `value?`, `defaultValue?`, `onValueChange?`, `label?`, `placeholder?`, `submitLabel?`, `stopLabel?`, `hint?`, `attachments?`, `disabled?`, `className?`, `style?` |
`ChatMessages` is also the transcript's public seam: render
`MessageScrollerItem` rows inside it (through `Message`) and pass
`MessageScrollerItem scrollAnchor` on a turn's first row if you want that turn
placed near the top.
---
# Checkbox
Source: https://gpuixui.sudokw.com/components/checkbox
An independent yes/no with an optional mixed state
## Installation [#installation]
## Usage [#usage]
```tsx
import { Checkbox } from "@gpuixui/primitives";
import { useState } from "react";
export const Digest = () => {
const [email, setEmail] = useState(true);
return (
);
};
```
## Composition [#composition]
```text
Checkbox
└── CheckboxIndicator
CheckboxGroup
└── Checkbox
└── CheckboxIndicator
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `checked` | `boolean` | — | controlled state; omit to let the box keep its own |
| `defaultChecked` | `boolean` | `false` | initial state when uncontrolled |
| `onCheckedChange` | `(next: boolean) => void` | none | the next boolean, only when it changed |
| `onChange` | `(next: boolean) => void` | none | deprecated alias for `onCheckedChange`; the new name wins when both are passed |
| `children` | `ReactNode` | the default glyph | the mark's content |
| `indeterminate` | `boolean` | `false` | paints a dash and announces `mixed`; overrides `checked` |
| `disabled` | `boolean` | `false` | refuses pointer and keys and leaves the tab order |
| `invalid` | `boolean` | `false` | the empty box takes the destructive edge |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | the drawing's side from `iconSize`; the target stays 24px |
| `accent` | `"primary" \| "destructive"` | `"primary"` | the fill and mark roles |
| `label` | `string` | none | the accessible name; a visible `Label` does not supply it |
| `description` | `string` | none | announced help (`aria-description`); wins over a tooltip's text |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `style` | `StateStyle` | — | resolved against the live state and merged last |
| `testId` | `string` | — | stable locator |
| `ref` / `aria-id` / `onLayout` | host address | none | the box is the addressed element: focus it, measure it, and name it for automation |
| `onMouseEnter` / `onMouseLeave` / `aria-description` | the tooltip trigger pair and its words | none | forwarded to the box, so a checkbox can be a `Tooltip` trigger; the tip's words arrive as `aria-description` |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the box's own; yours runs first |
### `CheckboxIndicator` [#checkboxindicator]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | the default check/dash | the glyph |
| `className` | `string` | — | compiled through the recipe registry after the part's rules |
| `style` | `StyleDesc` | — | merged last |
---
# Code block
Source: https://gpuixui.sudokw.com/components/code-block
Source or output, tokenized by @tanstack/highlight
## Usage [#usage]
```tsx
import { CodeBlock } from "@gpuixui/components/code-block";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `code` | `string` | — |
| `title` | `string` | falls back to `path` |
| `path` | `string` | — |
| `language` | `string` | path's extension |
| `lineNumbers` | `boolean` | `false` |
| `height` | `number` | — |
| `level` | `SurfaceLevel` | `"normal"` |
| `emptyLabel` | `string` | `"No code"` |
| `copyLabel` | `string` | `"Copy"` |
| `copiedLabel` | `string` | `"Copied"` |
| `onCopy` | `(code: string) => void` | — |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Collapsible
Source: https://gpuixui.sudokw.com/components/collapsible
One section that reveals its body on demand
For several sections at once, use [Accordion](/components/accordion); it prerenders its bodies.
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Collapsible,
CollapsibleContent,
CollapsibleTrigger,
} from "@gpuixui/components/collapsible";
Advanced options
Retry budget: 3 attempts
;
```
## Composition [#composition]
```text
Collapsible
├── CollapsibleTrigger
└── CollapsibleContent
```
## API reference [#api-reference]
| Part | Prop | Type | Default | Consequence |
| ---- | ---- | ---- | ------- | ----------- |
| `Collapsible` | `open` | `boolean` | — | controlled state; omit it to let the root keep its own |
| `Collapsible` | `defaultOpen` | `boolean` | `false` | the uncontrolled starting state, open on the first frame |
| `Collapsible` | `onOpenChange` | `(open: boolean) => void` | none | the next state the trigger reports |
| `Collapsible` | `children` | `CollapsibleTrigger` and `CollapsibleContent` | — | the direct parts; a misplaced family part throws |
| `CollapsibleTrigger` | `disabled` | `boolean` | `false` | leaves the tab order, dims, and refuses click, `Enter` and `Space` |
| `CollapsibleTrigger` | `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `CollapsibleTrigger` | `style` | `StateStyle` | the recipe | resolved against the live state and merged last |
| `CollapsibleContent` | `className` | `string` | — | joined after the recipe on the measured box |
| `CollapsibleContent` | `children` | `ReactNode` | — | the body; mounted only while open, and during the exit |
---
# Color picker
Source: https://gpuixui.sudokw.com/components/color-picker
A swatch that opens the saturation, hue and alpha rails
## Installation [#installation]
## Usage [#usage]
```tsx
import { useState } from "react";
import { ColorPicker } from "@gpuixui/components";
const [accent, setAccent] = useState("#2F6BFF");
;
```
## API reference [#api-reference]
### `ColorPicker` [#colorpicker]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | — | the trigger and panel's name; a blank value reads as `"Color picker"` so the trigger and the ` hex value` field are always named |
| `value` | `string` | — | the controlled colour; `undefined` is uncontrolled, and a stored value that is not text means uncontrolled |
| `defaultValue` | `string` | `"#000000"` | the uncontrolled starting colour; a stored default that is not text paints `"#000000"` |
| `onValueChange` | `(hex: string) => void` | — | every committed colour, as uppercase hex with two alpha digits when not opaque |
| `background` | `string` | the theme's page | the surface the contrast readout measures against; an unreadable value leaves the readout at `1.00:1` |
| `swatches` | `ReadonlyArray` | six theme roles | the preset row, in order; a stored value that is not a list paints the theme's defaults |
| `disabled` | `boolean` | `false` | the trigger leaves the tab order and no part accepts a change |
| `className` | `string` | — | compiled through `@gpuixui/tw`, joined after the trigger's recipe |
| `style` | `StateStyle` | — | resolved against `{ disabled, valid }`, merged last |
| `testId` | `string` | — | the root's locator; the trigger uses it, and its parts derive `-swatch`, `-panel`, `-done`, `-area`, `-hue`, `-alpha`, `-hex`, `-error` and `-preset` (each preset adds `-`) |
---
# Color swatches
Source: https://gpuixui.sudokw.com/components/color-swatches
One colour chosen from a fixed row
## Usage [#usage]
```tsx
import { ColorSwatches } from "@gpuixui/components/color-swatches";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` — the group's accessible name | — |
| `colors` | `ReadonlyArray` | — |
| `value` | `string` — the chosen colour; it wears the ring | — |
| `onValueChange` | `(color: string) => void` | — |
| `size` | `number` — one swatch's diameter | `16` |
Use [Color picker](/components/color-picker) when any colour is allowed.
---
# Combobox
Source: https://gpuixui.sudokw.com/components/combobox
A searchable single- or multi-select with ranked options
## Installation [#installation]
## Usage [#usage]
```tsx
import { useState } from "react";
import {
Combobox,
ComboboxContent,
ComboboxEmpty,
ComboboxInput,
ComboboxList,
ComboboxTrigger,
} from "@gpuixui/components/combobox";
const people = [
{
value: "ada",
label: "Ada Lovelace",
description: "Engineering · compilers",
},
{
value: "grace",
label: "Grace Hopper",
description: "Platform · releases",
},
];
export const ReviewerField = () => {
const [reviewer, setReviewer] = useState("ada");
return (
setReviewer(typeof value === "string" ? value : null)
}
options={people}
value={reviewer ?? undefined}
>
);
};
```
## Composition [#composition]
```text
Combobox
├── ComboboxTrigger
└── ComboboxContent
├── ComboboxInput
├── ComboboxList
│ └── ComboboxItem
└── ComboboxEmpty
```
## API reference [#api-reference]
### Combobox [#combobox]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `options` | `ReadonlyArray` — `{ value, label, description?, disabled? }` | required; a stored value that is not a list paints no rows |
| `value` | `string \| ReadonlyArray` — controlled | uncontrolled; a stored value that is neither text nor a list means uncontrolled |
| `defaultValue` | `string \| ReadonlyArray` — initial selection when uncontrolled | — |
| `onValueChange` | `(value: string \| null \| ReadonlyArray) => void` | — |
| `multiple` | `boolean` | `false` |
| `disabled` | `boolean` | `false` |
| `onQueryChange` | `(query: string) => void` — every search the popup runs, including the empty one it opens with | — |
| `shouldFilter` | `boolean` — rank `options` locally; `false` keeps the caller's order | `true` |
| `loading` | `boolean` — the caller's fetch is in flight; an empty list says `Searching…` | `false` |
| `onCreate` | `(query: string) => void` — single mode only; the caller adds the option | — |
| `createLabel` | `(query: string) => string` | `Create "…"` |
| `children` | parts — `ComboboxTrigger`, `ComboboxContent` | — |
### ComboboxTrigger [#comboboxtrigger]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `placeholder` | `string` — shown while nothing is selected | `"Select…"` |
| `label` | `string` — the trigger's accessible name | — |
| `className` | `string` — joined after the trigger recipe | — |
| `testId` | `string` | `"combobox-trigger"` |
### ComboboxContent [#comboboxcontent]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` — a stored name the union does not carry |
| `align` | `"start" \| "center" \| "end"` | `"start"` — a stored name the union does not carry |
| `sideOffset` | `number` — the gap to the trigger | `4` |
| `alignOffset` | `number` — added on top of the measured centre correction | `0` |
| `className` | `string` — joined after the surface recipe | — |
| `children` | parts — `ComboboxInput`, `ComboboxList`, `ComboboxEmpty` | — |
### ComboboxInput [#comboboxinput]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `placeholder` | `string` | `"Search…"` |
| `label` | `string` | `"Search options"` |
### ComboboxList [#comboboxlist]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` — the listbox's accessible name | `"Options"` |
| `itemStyle` | `StateStyle` — resolved per row | the recipe |
### ComboboxItem [#comboboxitem]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `string` — the label and description are read from the root's option | required |
| `style` | `StateStyle` — `{ selected, highlighted, disabled }` | the recipe |
### ComboboxEmpty [#comboboxempty]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | `No matches` |
---
# Command menu
Source: https://gpuixui.sudokw.com/components/command-menu
A searchable action list, the ⌘K palette
## Installation [#installation]
## Usage [#usage]
```tsx
import {
CommandMenu,
CommandMenuEmpty,
CommandMenuInput,
CommandMenuList,
} from "@gpuixui/components/command-menu";
const actions = [
{
id: "new-file",
label: "New file",
group: "File",
shortcut: "⌘N",
perform: newFile,
},
{
id: "toggle-sidebar",
label: "Toggle sidebar",
group: "View",
shortcut: "⌘B",
perform: toggleSidebar,
},
];
No matching commands
;
```
## Composition [#composition]
```text
CommandMenu
├── CommandMenuInput
├── CommandMenuList
│ └── CommandMenuItem
└── CommandMenuEmpty
```
## API reference [#api-reference]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `CommandMenu` | the composed `Dialog` surface | `open` (required), `onOpenChange` (required), `actions` (required; a stored value that is not a list paints the empty state), `recents` (a stored value that is not a list carries no recents), `filter`, `children` |
| `CommandMenuInput` | host ` ` | `placeholder` (default `"Type a command…"`; an empty field announces it as `aria-description`), `label` (default `"Search commands"`), `className` |
| `CommandMenuList` | scrolling match column | `className`; the root's ranked matches are its content |
| `CommandMenuItem` | one runnable row | `action` (required), `className` |
| `CommandMenuEmpty` | the no-match message | `children` (default `No matching commands`), `className` |
---
# Confirmation dialog
Source: https://gpuixui.sudokw.com/components/confirm-dialog
The safe workflow for an action that cannot be undone
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Alert,
AlertContent,
AlertDescription,
AlertTitle,
} from "@gpuixui/components/alert";
import { ConfirmDialog } from "@gpuixui/components/confirm-dialog";
import { Button } from "@gpuixui/primitives";
import { useState } from "react";
export const ArchiveProject = () => {
const [open, setOpen] = useState(false);
const [saving, setSaving] = useState(false);
const [error, setError] = useState(null);
const archive = async () => {
setSaving(true);
setError(null);
try {
await archiveProject();
setOpen(false);
} catch {
setError("The workspace service timed out. Nothing was archived.");
} finally {
setSaving(false);
}
};
return (
<>
setOpen(true)}>
Archive project
void archive()}
>
{error === null ? null : (
Archive failed
{error}
)}
>
);
};
```
## Composition [#composition]
```text
ConfirmDialog
├── DialogContent
│ ├── DialogHeader
│ └── footer (actions)
└── children
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `open` | `boolean` (required) | — | the controlled open state |
| `onOpenChange` | `(open: boolean) => void` (required) | — | reports Cancel, Escape and an outside press |
| `title` | `string` (required) | — | the dialog's accessible name and its heading |
| `description` | `string` (required) | — | the consequence, as one paragraph |
| `confirmLabel` | `string` | `"Confirm"` | the confirm action's label |
| `cancelLabel` | `string` | `"Cancel"` | the cancel action's label |
| `tone` | `"default" \| "danger"` | `"default"` | `"danger"` paints the destructive action variant |
| `loading` | `boolean` | `false` | disables both actions, paints the busy mark, and ignores Escape and an outside press |
| `onConfirm` | `() => void` (required) | — | runs on activation; never closes the dialog |
| `children` | `ReactNode` | — | extra body between the description and the actions |
---
# Context menu
Source: https://gpuixui.sudokw.com/components/context-menu
Menu rows at the pointer, or opened from the keyboard
## Installation [#installation]
## Usage [#usage]
```tsx
import {
ContextMenu,
ContextMenuCheckboxItem,
ContextMenuContent,
ContextMenuItem,
ContextMenuSeparator,
ContextMenuSub,
ContextMenuSubContent,
ContextMenuSubTrigger,
ContextMenuTrigger,
} from "@gpuixui/components/context-menu";
release-notes.md
Open
Rename
Move to trash
Share
Copy link
Email
Show hidden files
;
```
## Composition [#composition]
```text
ContextMenu
├── ContextMenuTrigger
└── ContextMenuContent
├── ContextMenuItem
├── ContextMenuCheckboxItem
├── ContextMenuRadioGroup
│ └── ContextMenuRadioItem
├── ContextMenuSeparator
├── ContextMenuLabel
├── ContextMenuGroup
└── ContextMenuSub
├── ContextMenuSubTrigger
└── ContextMenuSubContent
```
## API reference [#api-reference]
### `ContextMenu` [#contextmenu]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `open` | `boolean` | uncontrolled | controlled open state |
| `defaultOpen` | `boolean` | `false` | the initial state when uncontrolled |
| `onOpenChange` | `(open: boolean, details: { reason }) => void` | none | reports the next state and why it changed: `trigger-press`, `item-press`, `escape-key`, `outside-press`, `focus-out` or `pointer-leave`; a controlled menu waits for the caller |
| `children` | parts | none | `ContextMenuTrigger` and `ContextMenuContent` |
### `ContextMenuTrigger` [#contextmenutrigger]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `disabled` | `boolean` | `false` | no aux click, no `Shift+F10`, out of the tab order, dimmed |
| `children` | nodes | none | the area the reader presses |
| `style` | host style | none | merged last over the card-like box |
### `ContextMenuContent` [#contextmenucontent]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | `"Context menu"` | the surface's accessible name |
| `children` | parts | none | rows, groups, labels, separators and submenus |
| `className` | `string` | none | compiled through `@gpuixui/tw` and joined after the recipe |
| `style` | host style | none | merged last on the surface |
### `ContextMenuItem` and `ContextMenuCheckboxItem` [#contextmenuitem-and-contextmenucheckboxitem]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `onSelect` | `() => void` | none | runs once when the row is activated |
| `disabled` | `boolean` | `false` | skipped by arrows and type-ahead; paints the measured disabled pair |
| `tone` | `"default" \| "destructive"` | `"default"` — a stored name the union does not carry | a destructive row paints the status tone's readable role |
| `leading` / `trailing` | `ReactNode` | none | slots before and after the label |
| `textValue` | `string` | the label | the text type-ahead searches |
| `closeOnSelect` | `boolean` | `true` | whether activation closes the menu |
| `checked` | `boolean` | none | the checkbox row's state (required on `ContextMenuCheckboxItem`) |
| `onCheckedChange` | `(checked: boolean) => void` | none | reported when the checkbox row toggles (required) |
| `className` | `string` | none | compiled through `@gpuixui/tw` and joined after the recipe |
| `style` | `MenuItemState` callback | none | `{ disabled, highlighted }`, merged after the row's roles |
### `ContextMenuRadioGroup` and `ContextMenuRadioItem` [#contextmenuradiogroup-and-contextmenuradioitem]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | none | the chosen row's value (required on the group) |
| `onValueChange` | `(value: string) => void` | none | reported with a row's value when it is chosen, never the current one (required) |
| `label` | `string` | none | the choice's accessible name; a painted `ContextMenuLabel` does not name the group |
| `className` | `string` | none | compiled through `@gpuixui/tw`, joined after the recipe |
| `children` | parts | none | the group's `ContextMenuRadioItem` rows |
| `value` | `string` | none | this row's value inside the group (required on `ContextMenuRadioItem`) |
| `closeOnSelect` / `disabled` / `tone` / `leading` / `trailing` / `textValue` | as the action row | — | the row's own slots, tone and type-ahead key |
| `style` | `MenuRadioItemState` callback | none | `{ checked, disabled, highlighted }`, merged after the row's roles |
### `ContextMenuSub`, `ContextMenuSubTrigger`, `ContextMenuSubContent` [#contextmenusub-contextmenusubtrigger-contextmenusubcontent]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | parts | none | `ContextMenuSub` holds a trigger and its content |
| `label` | `string` | none | the second level's accessible name |
| `disabled` / `leading` / `trailing` / `textValue` | the row surface | — | the sub trigger is an ordinary row that opens the second level |
| `className` / `style` | host | none | merged last |
---
# Copy button
Source: https://gpuixui.sudokw.com/components/copy-button
An icon action that copies a value and acknowledges it
## Usage [#usage]
```tsx
import { useState } from "react";
import { CopyButton } from "@gpuixui/components";
const [mirror, setMirror] = useState("");
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `text` | `string` | — |
| `onCopy` | `(text: string) => void` | — |
| `label` | `string` | `"Copy"` |
| `copiedLabel` | `string` | `"Copied"` |
| `acknowledgeMs` | `number` | `1200` |
| `size` | `"sm" \| "md" \| "lg"` | `"sm"` |
| `disabled` | `boolean` | `false` |
| `color` | `string` | `mutedForeground` |
| `level` | `SurfaceLevel` | `"fast"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Countdown
Source: https://gpuixui.sudokw.com/components/countdown
A remaining duration as clock text
## Usage [#usage]
```tsx
import { Countdown } from "@gpuixui/components";
;
// 01:30
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `remainingMs` | `number` (required) | — (negative or unreadable renders `00:00`, expired) |
| `label` | `string` | — |
| `showDays` | `boolean` | `false` |
| `style` | `StateStyle` | — |
| `className` | `string` | — |
---
# Counting number
Source: https://gpuixui.sudokw.com/components/counting-number
A number that counts to its value
## Usage [#usage]
```tsx
import { CountingNumber } from "@gpuixui/components";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `number` (required) | — |
| `from` | `number` | `value` |
| `durationMs` | `number` | `320` |
| `decimals` | `number` | `0` |
| `format` | `(value: number) => string` | — |
| `label` | `string` | the target's text |
| `role` | `TextRole` | `"body"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Date picker
Source: https://gpuixui.sudokw.com/components/date-picker
A day or a span of days, chosen from a Calendar
## Installation [#installation]
## Usage [#usage]
```tsx
import {
DatePicker,
DatePickerCalendar,
DatePickerContent,
DatePickerTrigger,
} from "@gpuixui/components/date-picker";
import { useState } from "react";
export const DeploymentWindow = () => {
const [due, setDue] = useState("2026-10-30");
return (
);
};
```
A span is `mode="range"`: the value is `{ from, to }`, and it closes on the second day.
```tsx
```
## Composition [#composition]
```text
DatePicker
├── DatePickerTrigger
└── DatePickerContent
└── DatePickerCalendar
```
## API reference [#api-reference]
### `DatePicker` [#datepicker]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `mode` | `"single" \| "range"` | `"single"` | selects the value shape `value` and `onValueChange` speak |
| `value` | `string \| null` · `{ from, to } \| null` | none | the controlled value; `undefined` makes the picker uncontrolled |
| `defaultValue` | `string \| null` · `{ from, to } \| null` | `null` | the opening value when uncontrolled |
| `onValueChange` | `(value \| range \| null) => void` | none | the chosen day, the span, or `null` from Clear |
| `weekStartsOn` | `0 \| 1` | `1` | the grid's first column |
| `min` | `string` | unbounded | the first selectable ISO day, inclusive |
| `max` | `string` | unbounded | the last selectable ISO day, inclusive |
| `clearable` | `boolean` | `false` | adds the Clear row while a value is set |
| `disabled` | `boolean` | `false` | leaves the tab order and does not open the surface |
| `today` | `string` | the runtime clock | the ISO day the grid marks as today |
| `numberOfMonths` | `1 \| 2` | `1`, or `2` in range mode | months painted side by side |
| `showOutsideDays` | `boolean` | `true` | paints the neighbouring months' days |
| `captionLayout` | `"label" \| "dropdown"` | `"label"` | a caption, or month and year jump controls |
| `fromYear` / `toYear` | `number` | today ± 10 | the years a dropdown may reach |
| `children` | `ReactNode` | none | the trigger and the content, directly inside |
### `DatePickerTrigger` [#datepickertrigger]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `placeholder` | `string` | `"Pick a date"` | the text painted while no value is set |
| `label` | `string` | — | the trigger's accessible name (the caret is decorative) |
| `variant` | `"primary" \| "secondary" \| "ghost" \| "danger"` | `"secondary"` | the trigger's fill and label role |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | the control height the trigger takes from the theme's density |
| `disabled` | `boolean` | `false` | adds to the root's disabled rule; never removes it |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `testId` | `string` | `"date-picker-trigger"` | the trigger's test identity |
| `onActivate` | `(event) => void` | — | the action's one handler: it opens the surface from a press or an activating key |
| `onClick` | `(event) => void` | — | the deprecated alias; only runs when `onActivate` is absent |
### `DatePickerContent` [#datepickercontent]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` | where the surface anchors relative to the trigger |
| `align` | `"start" \| "center" \| "end"` | `"start"` | the surface's alignment on that side |
| `sideOffset` | `number` | `6` | the gap between trigger and surface, in pixels |
| `className` | `string` | — | joined after the recipe |
| `children` | `ReactNode` | none | the calendar, directly inside |
### `DatePickerCalendar` [#datepickercalendar]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | `"Calendar"` | the grid's accessible name |
---
# Diagram
Source: https://gpuixui.sudokw.com/components/diagram
A Mermaid source, drawn by Pele
A **diagram** is a drawing, and this is the component that draws one. You give
it a Mermaid source and it renders the diagram, themed in the library's own
colours.
The renderer is [Pele](https://pele.run) — a zero-dependency Mermaid renderer
that needs no DOM. Measured in a plain Node process, with `typeof document` and
`typeof window` both `undefined`, a flowchart rendered to a 403×64 SVG.
The drawing travels as one image, so the whole diagram is **one element** in
the frame.
## Usage [#usage]
```tsx
import { Diagram } from "@gpuixui/components/diagram";
B[Component]"}
caption="How a component becomes a picture"
/>;
```
## The source is the API [#the-source-is-the-api]
A diagram is written in Mermaid, and the fence is the whole interface:
````mdx
```mermaid
flowchart LR
A[Author] --> B[Component]
```
````
Every diagram type Mermaid 12.1 knows is supported, because Pele renders all of
them. A source the parser refuses paints the parser's sentence in the
drawing's place — the reader sees what went wrong rather than an empty frame.
## Theming [#theming]
Pele writes its palette as CSS custom properties. A raster cannot read them,
so the component reads the theme and **bakes the values into** the drawing.
| Pele role | The library's role |
| --- | --- |
| `--pele-bg` | `card` |
| `--pele-fg` | `foreground` |
| `--pele-muted` | `muted-foreground` |
| `--pele-line` | `border-strong` |
| `--pele-surface` | `muted` |
| `--pele-surface-alt` | `accent` |
| `--pele-accent` | `primary` |
| `--pele-border` | `border` |
The three roles that are not colours — `--pele-radius`, `--pele-font` and
`--pele-font-mono` — are given literal values, because a raster carries no
font of its own.
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `source` | `string` | — | the Mermaid source; the whole interface |
| `label` | `string` | the host's `aria-label` | the drawing's name for a reader |
| `caption` | `string` | — | a line of words under the drawing |
| `maxWidth` | `number` | — | the width the drawing may use, so a narrow box adapts the layout |
| `fontSize` | `number` | — | the base font size, in pixels |
| `idPrefix` | `string` | — | the prefix on the drawing's ids, so two diagrams on one page do not collide |
| `className` | `string` | — | a class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | the caller's style, merged last |
| `children` | `ReactNode` | — | what a caller paints beside the drawing — a legend, a note |
| `testId` | `string` | — | the test id the drawing and the failure state answer to |
---
# Dialog
Source: https://gpuixui.sudokw.com/components/dialog
A modal surface that takes the window until it is dismissed
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Dialog,
DialogClose,
DialogContent,
DialogDescription,
DialogFooter,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@gpuixui/components/dialog";
import { Button } from "@gpuixui/primitives";
Delete workspace
Delete this workspace?
The workspace and its 42 files are removed for everyone.
Cancel
Delete workspace
;
```
## Composition [#composition]
```text
Dialog
├── DialogTrigger
└── DialogContent
├── DialogHeader
│ ├── DialogTitle
│ └── DialogDescription
├── DialogFooter
│ └── DialogClose
└── (close control)
```
## API reference [#api-reference]
### Dialog [#dialog]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `open` | `boolean` | uncontrolled |
| `defaultOpen` | `boolean` | `false` |
| `onOpenChange` | `(open: boolean) => void` | none |
| `dismissible` | `boolean` | `true` |
| `children` | parts — `DialogTrigger`, `DialogContent`, `DialogClose` | — |
### DialogContent [#dialogcontent]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` (required) | none |
| `role` | `"dialog" \| "alertdialog"` | `"dialog"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` — a stored name the union does not carry |
| `showClose` | `boolean` | `true` |
| `initialFocus` | `() => number \| null` | none |
| `className` | `string` — joined after the surface recipe | — |
| `children` | `ReactNode` — the surface's content | — |
### DialogTrigger and DialogClose [#dialogtrigger-and-dialogclose]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `disabled` | `boolean` | `false` |
| `onClick` | `(event: EventPayload) => void` | none |
| `variant` | `"primary" \| "secondary" \| "ghost" \| "danger"` | `"secondary"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |
| `loading`, `loadingLabel`, `reducedMotion`, `leading`, `trailing` | the shared `Button` busy contract | — |
| `className` | `string` — joined after the button recipe | none |
## Theming [#theming]
The box paints the theme's `elevation.lg` beside its 1px `foreground` ring. Its corner is the recipe's fixed **32px** (`rounded-4xl`); the preview draws the platform radius plus 8px and is the approximation.
---
# Disclosure
Source: https://gpuixui.sudokw.com/components/disclosure
An inspector section headed by a triangle
## Usage [#usage]
```tsx
import { Disclosure } from "@gpuixui/components/disclosure";
;
```
## Composition [#composition]
```text
Disclosure (a Collapsible)
├── trigger — the triangle and the 13-point semibold title
└── content — opened through the collapsible's measured reveal
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `title` | `string` | — |
| `defaultOpen` | `boolean` | `true`, as an inspector opens |
| `testId` | `string` — on the header | — |
| `children` | `ReactNode` — the body | — |
---
# Drawer
Source: https://gpuixui.sudokw.com/components/drawer
A modal panel anchored to any window edge
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Drawer,
DrawerClose,
DrawerContent,
DrawerDescription,
DrawerFooter,
DrawerHeader,
DrawerTitle,
DrawerTrigger,
} from "@gpuixui/components/drawer";
Filters
Filters
Narrow the result set.
Cancel
Apply filters
;
```
## Composition [#composition]
```text
Drawer
├── DrawerTrigger
└── DrawerContent
├── DrawerHeader
│ ├── DrawerTitle
│ └── DrawerDescription
├── DrawerFooter
│ └── DrawerClose
└── (close control)
```
## API reference [#api-reference]
### `Drawer` [#drawer]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `open` | `boolean` | uncontrolled | controlled state; omit to let the drawer keep its own |
| `defaultOpen` | `boolean` | `false` | the initial state when uncontrolled |
| `onOpenChange` | `(open: boolean) => void` | none | reports the next state; a controlled drawer does not move until the caller re-renders |
| `dismissible` | `boolean` | `true` | whether Escape and an outside press close the surface; the close control is unaffected |
| `children` | parts | none | `DrawerTrigger`, `DrawerClose` and `DrawerContent` |
### `DrawerContent` [#drawercontent]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` (required) | none | the dialog's accessible name |
| `side` | `"left" \| "right" \| "top" \| "bottom"` | `"right"` | the edge the surface is anchored to; a stored name the union does not carry opens on the default `"right"` edge |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | the requested width or height, clamped by the window; a stored name the union does not carry paints the house `"md"` box |
| `showClose` | `boolean` | `true` | renders the close control, a `@gpuixui/icons` `x` named "Close drawer" |
| `inset` | `number` | none | a top sheet hangs this far below the window's top, centred and at most the window's width less 80 — AppKit's sheet under the toolbar |
| `grabber` | `boolean` | `true` | paints the vaul handle on a `top` or `bottom` sheet; a column never shows one |
| `initialFocus` | `() => number \| null` | none | the native id to focus after the surface receives focus |
| `className` | `string` | none | compiled through `@gpuixui/tw` and joined after the recipe |
| `children` | parts | none | the header, title, description, body and footer |
---
# Dropdown menu
Source: https://gpuixui.sudokw.com/components/dropdown-menu
A list of actions anchored to the control that opened it
## Installation [#installation]
## Usage [#usage]
```tsx
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@gpuixui/components/dropdown-menu";
Actions
rename(file)}>Rename
Duplicate
remove(file)}>
Delete
;
```
## Composition [#composition]
```text
DropdownMenu
├── DropdownMenuTrigger
└── DropdownMenuContent
├── DropdownMenuItem
├── DropdownMenuCheckboxItem
├── DropdownMenuRadioGroup
│ └── DropdownMenuRadioItem
├── DropdownMenuSeparator
├── DropdownMenuLabel
├── DropdownMenuGroup
└── DropdownMenuSub
├── DropdownMenuSubTrigger
└── DropdownMenuSubContent
```
## API reference [#api-reference]
### DropdownMenu [#dropdownmenu]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `open` | `boolean` | uncontrolled |
| `defaultOpen` | `boolean` | `false` |
| `onOpenChange` | `(open: boolean) => void` | none |
| `children` | parts — a trigger, the content and its rows | — |
### DropdownMenuContent [#dropdownmenucontent]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` (required) | the menu's accessible name |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` — a stored name the union does not carry |
| `align` | `"start" \| "center" \| "end"` | `"start"` — a stored name the union does not carry |
| `sideOffset` | `number` | `4` |
| `collisionPadding` | `number` | `8` |
| `alignOffset` | `number` | `0` — added to the measured centre correction |
| `className` | `string` | none — joined after the surface recipe |
| `children` | parts — the rows, groups and separators | none |
### DropdownMenuItem [#dropdownmenuitem]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `onSelect` | `() => void` | none — runs, then closes |
| `disabled` | `boolean` | `false` |
| `tone` | `"default" \| "destructive"` | `"default"` — a stored name the union does not carry |
| `leading` | `ReactNode` | none — a slot before the label |
| `trailing` | `ReactNode` | none — a slot after it, for a shortcut |
| `textValue` | `string` | the plain text children |
| `closeOnSelect` | `boolean` | `true` |
| `className` | `string` | none — joined after the row recipe |
| `style` | `StyleDesc \| (state: MenuItemState) => StyleDesc` | none |
### DropdownMenuCheckboxItem [#dropdownmenucheckboxitem]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `checked` | `boolean` (required) | none |
| `onCheckedChange` | `(checked: boolean) => void` (required) | none |
| `closeOnSelect` | `boolean` | `false` — the row stays open |
| `onSelect` / `disabled` / `tone` / `leading` / `trailing` / `textValue` | the row surface — see `DropdownMenuItem` | none |
| `className` | `string` | none — joined after the row recipe |
| `style` | `StyleDesc \| (state: MenuCheckboxItemState) => StyleDesc` | none |
### DropdownMenuRadioGroup / DropdownMenuRadioItem [#dropdownmenuradiogroup--dropdownmenuradioitem]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `string` (required, group) | none — the choice the group marks |
| `onValueChange` | `(value: string) => void` (required, group) | none — reports the chosen row's value |
| `label` | `string` | none — the choice's accessible name |
| `className` | `string` | none — compiled through `@gpuixui/tw` |
| `children` | parts — the group's `DropdownMenuRadioItem` rows | none (group) |
| `value` | `string` (required, row) | none — this row's value in the group |
| `closeOnSelect` / `disabled` / `tone` / `leading` / `trailing` / `textValue` | the row surface — see `DropdownMenuItem` | none (row) |
| `style` | `StyleDesc \| (state: MenuRadioItemState) => StyleDesc` | none — resolved against `{ checked, disabled, highlighted }` |
---
# Empty state
Source: https://gpuixui.sudokw.com/components/empty-state
The panel a collection shows when it has no rows
## Installation [#installation]
## Usage [#usage]
```tsx
import { useState } from "react";
import { Card } from "@gpuixui/components";
import { EmptyState } from "@gpuixui/components/empty-state";
import { Button } from "@gpuixui/primitives";
export const ProjectsPanel = () => {
const [creating, setCreating] = useState(false);
if (creating) return Create a project… ;
return (
setCreating(true)}>
New project
}
/>
);
};
```
## Composition [#composition]
```text
EmptyState
├── icon
├── title
├── description
└── action
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `title` | `string` | required | the line a reader scans first; painted in `foreground` |
| `description` | `string` | none | support copy under the title, wrapped at 360px |
| `action` | `ReactNode` | none | the way out, usually a `Button`; the only focusable part |
| `icon` | `ReactNode` | none | a decorative glyph above the title, painted in `mutedForeground` |
| `className` | `string` | none | compiled through `@gpuixui/tw`, joined after the recipe |
| `style` | `StyleDesc` | none | merged last, so a caller overrides any recipe property |
| `testId` | `string` | none | stable locator |
---
# Field and inline validation
Source: https://gpuixui.sudokw.com/components/field
A label, control and one message slot around an input
## Installation [#installation]
## Usage [#usage]
```tsx
import { Effect, Option, Schema } from "effect";
import { useState } from "react";
import { Field, FieldInput } from "@gpuixui/components/field";
import { validate } from "@gpuixui/components/validation/logic";
const WorkspaceSlug = Schema.NonEmptyString.check(
Schema.isPattern(/^[a-z0-9]+(-[a-z0-9]+)*$/, {
identifier: "WorkspaceSlugPattern",
title: "Workspace URL",
message: "Use lowercase letters, numbers and single hyphens",
}),
);
export const WorkspaceForm = () => {
const [slug, setSlug] = useState("");
const [error, setError] = useState();
const submit = () => {
const message = Effect.runSync(validate(WorkspaceSlug)(slug));
setError(Option.getOrUndefined(message));
};
return (
setSlug(event.value ?? "")}
/>
);
};
```
## Composition [#composition]
```text
Field
├── FieldLabel
├── FieldDescription
├── FieldError
└── FieldInput
Fieldset
└── Field
```
## API reference [#api-reference]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Field` | `label` | `string` — the visible label and the input's name | required |
| `Field` | `description` | `string` — hidden while `error` is present | — |
| `Field` | `error` | `string` — the message, which also paints the control invalid | — |
| `Field` | `required` | `boolean` — appends the `*` marker | `false` |
| `Field` | `className` | `string` — joined after the recipe | — |
| `Field` | `children` | the control; only `FieldInput` is accepted | — |
| `FieldLabel` | `required` | `boolean` — the marker without changing the name | `false` |
| `FieldDescription` | `children` | `ReactNode` | — |
| `FieldError` | `children` | `ReactNode` | — |
| `FieldInput` | `invalid` | `boolean` — overrides the field's own error as the validity signal | the field's state |
| `FieldInput` | `disabled` | `boolean` — also inferred from a disabled `Fieldset` | `false` |
| `FieldInput` | `style` | `StateStyle<{ disabled, focused, invalid }>` | the recipe |
| `FieldInput` | `value` / `defaultValue` | `string` — the text; omit `value` to let the field keep its draft | — |
| `FieldInput` | `onValueChange` / `onChange` | `(value: string) => void` · the raw host payload, first | — |
| `FieldInput` | `onSubmit` | `(event: EventPayload) => void` — `Enter` in the input | — |
| `FieldInput` | `placeholder` / `readOnly` / `autoFocus` | `string` · `boolean` · `boolean` | — |
| `FieldInput` | `size` / `fullWidth` / `tabIndex` | `"sm" \| "md" \| "lg"` · `boolean` · `number` | `"md"` · `false` · the field's place |
| `FieldInput` | `testId` | `string` — a stable locator | — |
| `FieldInput` | `ref` / `aria-id` / `onLayout` | the addressed element: focus it, measure it, name it for automation | — |
| `FieldInput` | `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers, composed with the field's own | — |
| `Fieldset` | `legend` | `string` — the visible heading and the group's name | required |
| `Fieldset` | `disabled` | `boolean` — reaches the controls that honour it | `false` |
---
# Fieldset
Source: https://gpuixui.sudokw.com/components/fieldset
A named group of fields with a disabled state
## Usage [#usage]
```tsx
import { Field, FieldInput, Fieldset } from "@gpuixui/components";
setStreet(event.value ?? "")} />
setCity(event.value ?? "")} />
;
```
## Composition [#composition]
```text
Fieldset
└── FieldsetLegend
```
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `Fieldset` | `div` with `role="group"` | `legend` (required), `disabled?`, `className?`, `children` |
| `FieldsetLegend` | `text` | `disabled?`, `className?` |
---
# Form
Source: https://gpuixui.sudokw.com/components/form
Schema validation and a submit lifecycle around fields
## Installation [#installation]
## Usage [#usage]
```tsx
import { Form } from "@gpuixui/components/form";
import { Field, FieldInput } from "@gpuixui/components/field";
import { Button } from "@gpuixui/primitives";
import { Schema } from "effect";
import { useState } from "react";
const Account = Schema.Struct({
email: Schema.NonEmptyString,
name: Schema.NonEmptyString,
});
export const CreateAccount = () => {
const [draft, setDraft] = useState({ email: "", name: "" });
return (
);
};
```
## Composition [#composition]
```text
Form
├── message block
└── children (fields)
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `schema` | `Schema.ConstraintDecoder` | none | decides whether the value may be submitted; its messages are the ones painted |
| `value` | `unknown` | none | the caller's draft; the form never reads a value from the runtime |
| `onSubmit` | `(value: T) => void` | none | receives the **decoded** value, only when the schema accepts it |
| `pending` | `boolean` | `false` | refuses `submit` while true, so one press is one save |
| `label` | `string` | none | makes the root `role="form"` with `aria-label`; without it the root is a plain container |
| `className` | `string` | none | joined after the recipe, so it wins conflicts |
| `children` | `(state: FormState) => ReactNode` | none | the render prop: `errors`, `pending`, `submit`, `form` |
| `style` | `StateStyle` | none | merged last on the root |
---
# Glyph
Source: https://gpuixui.sudokw.com/components/glyph
A wall of generated characters for its own box
## Usage [#usage]
```tsx
import { Glyph } from "@gpuixui/components/glyph";
Open the command deck
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | — |
| `marks` | `string` | `.+*/\<>[]{}~` |
| `cellSize` | `number` | `22` |
| `density` | `number` | `0.55` |
| `seed` | `number` | `1` |
| `color` | `string` | theme `border` |
| `breatheMs` | `number` | `8_000` |
| `level` | `MotionLevel` | `"slow"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Gradient
Source: https://gpuixui.sudokw.com/components/gradient
A two-stop gradient surface that breathes between ramps
## Usage [#usage]
```tsx
import { Gradient } from "@gpuixui/components/gradient";
Ship faster with panels
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `from` | `string` | theme `primary` |
| `to` | `string` | `from` at zero alpha |
| `angle` | `number` | `135` |
| `shift` | `number` | `90` |
| `durationMs` | `number` | `6_000` |
| `radius` | `number` | `0` |
| `level` | `MotionLevel` | `"slow"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
| `children` | `ReactNode` | — |
---
# Gradient text
Source: https://gpuixui.sudokw.com/components/gradient-text
A phrase painted as a per-glyph colour ramp
## Usage [#usage]
```tsx
import { GradientText } from "@gpuixui/components/gradient-text";
;
// Own the colours.
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `text` | `string` (required) | — |
| `from` | `string` | theme `foreground` |
| `to` | `string` | theme `primary` |
| `role` | `TextRole` | `"display"` |
| `level` | `MotionLevel` | `"slow"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Highlight text
Source: https://gpuixui.sudokw.com/components/highlight-text
A phrase whose matches a marker wash sweeps across
## Usage [#usage]
```tsx
import { HighlightText } from "@gpuixui/components/highlight-text";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `text` | `string` (required) | — |
| `query` | `string` | — |
| `ranges` | `ReadonlyArray` | — |
| `caseSensitive` | `boolean` | `false` |
| `wholeWord` | `boolean` | `false` |
| `stepMs` | `number` | `60` |
| `color` | `string` | `findHighlight(theme).color` |
| `activeColor` | `string` | `findHighlight(theme).activeColor` |
| `activeIndex` | `number` | — |
| `role` | `TextRole` | `"body"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
## Ranges [#ranges]
`ranges` names `[start, end)` pairs in the phrase's **UTF-16 units** and wins
over `query`. A pair is the part of it **inside the phrase**: a start below `0`
or an end past the phrase is clamped to it, a fractional index is truncated to
the unit it names, and a pair that is not two numbers or whose ends meet after
that is **no range at all**. A find bar can therefore pass the ranges its own
arithmetic produced — `indexOf` answers `-1` for no match — without sending a
negative offset to the runtime's `highlight`, which reads absolute units.
---
# Hotkey recorder
Source: https://gpuixui.sudokw.com/components/hotkey-recorder
Capture a chord and display it as keycaps
Using [TanStack Hotkeys](https://tanstack.com/hotkeys).
## Usage [#usage]
```tsx
import { HotkeyRecorder } from "@gpuixui/components";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` | — |
| `value` | `string \| null` | `null` |
| `onChange` | `(value: string \| null) => void` | — |
| `onRecordingChange` | `(recording: boolean) => void` | — |
| `placeholder` | `string` | `"Click to record"` |
| `disabled` | `boolean` | `false` |
| `style` | `StateStyle` | — |
| `className` | `string` | — |
## Gating your own bindings [#gating-your-own-bindings]
A recorded key is swallowed with `preventDefault`, which removes the element's
default action — **not** the window's. GPUix still delivers the key to an
application-level `onKeyDown`, measured by the playground's
`hotkey-recorder-lifecycle` probe (`window=shift,k` on the press that stored
`Mod+Shift+K`), so the app owns the gate:
```tsx
const [recording, setRecording] = useState(false);
{
if (recording) return;
dispatchShortcut(event);
}}
>
;
```
---
# Hover card
Source: https://gpuixui.sudokw.com/components/hover-card
A rich surface previewed from a name, on hover or on focus
## Usage [#usage]
```tsx
import {
HoverCard,
HoverCardContent,
HoverCardTrigger,
} from "@gpuixui/components/hover-card";
export const Mention = () => (
@ada
Mathematician · Analytical Engine
);
```
## The trigger is a link [#the-trigger-is-a-link]
shadcn's trigger is an anchor; `0.11.0` has none, so `HoverCardTrigger` is the
shared action painted as the library's **`link`** look — underlined text with no
fill and no edge — rather than the ghost chip it used to wear, which read as a
toggle button sitting in a sentence. It keeps the action's press, focus and
`aria-expanded`, and opens on hover after `openDelay`, on focus at once, or on a
press.
## Composition [#composition]
```text
HoverCard
├── HoverCardTrigger
└── HoverCardContent
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `HoverCard` | `open` / `defaultOpen` | `boolean` | — / `false` |
| `HoverCard` | `onOpenChange` | `(open: boolean) => void` | — |
| `HoverCard` | `openDelay` | `number` — milliseconds of hover before opening | `700` |
| `HoverCard` | `closeDelay` | `number` — milliseconds after the last hold leaves | `300` |
| `HoverCard` | `children` | `PartChildren` | — |
| `HoverCardTrigger` | action props | `variant` (`"link"` by default), `size`, `disabled`, `onClick`, `aria-label`, `style`, `ref`, `testId` — the shared `Button` contract | `size="md"` |
| `HoverCardTrigger` | busy props | `loading`, `loadingLabel`, `reducedMotion`, `leading`, `trailing` — the shared `Button` busy contract | — |
| `HoverCardContent` | `label` | `string` — the surface's accessible name | — |
| `HoverCardContent` | `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` |
| `HoverCardContent` | `align` | `"start" \| "center" \| "end"` | `"center"` |
| `HoverCardContent` | `sideOffset` | `number` | `4` |
| `HoverCardContent` | `collisionPadding` | `number` | `8` |
| `HoverCardContent` | `className` / `style` | `string` / `StyleDesc` | — |
| `HoverCardContent` | `testId` / `children` | `string` / `PartChildren` | — |
---
# Image zoom
Source: https://gpuixui.sudokw.com/components/image-zoom
A frame whose picture magnifies under the pointer
## Usage [#usage]
```tsx
import { ImageZoom } from "@gpuixui/components/image-zoom";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `src` | `string` | — |
| `alt` | `string` (required) | — (a blank value reads as `"Picture"`) |
| `width` | `number` | `240` |
| `height` | `number` | `160` |
| `scale` | `number` | `1.6` |
| `radius` | `number` | `theme.radius.md` (a value that cannot be painted reads as the default; `0` stays square) |
| `level` | `MotionLevel` | `"normal"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Inline edit
Source: https://gpuixui.sudokw.com/components/inline-edit
Rename a value where it is displayed
## Installation [#installation]
## Usage [#usage]
```tsx
import { InlineEdit } from "@gpuixui/components";
import { useState } from "react";
export const RenameProject = () => {
const [name, setName] = useState("Docs site");
return (
{
setName(next);
}}
value={name}
/>
);
};
```
## API reference [#api-reference]
### `InlineEdit` [#inlineedit]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | — | the committed text; required, and the caller owns it |
| `onCommit` | `(value: string) => void` | — | called on a **changed** `Enter` or blur commit, never twice per edit, and never when the caller disables the control mid-edit |
| `label` | `string` | — | the accessible name of both the display and the input |
| `placeholder` | `string` | `"—"` | painted in `mutedForeground` while the value is empty |
| `disabled` | `boolean` | `false` | leaves the tab order, ignores activation and fades to 0.5 |
| `editing` | `boolean` | — | the controlled edit state; `undefined` leaves it to the control |
| `defaultEditing` | `boolean` | `false` | the uncontrolled starting state |
| `onEditingChange` | `(editing: boolean) => void` | — | reports the edit state the control took |
| `style` | `StateStyle` | — | resolved against `{ editing, disabled }`, merged last |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `testId` | `string` | — | stable locator |
| `defaultValue` / `onValueChange` / `onChange` | the field surface it inherits — see `Input` | — | the draft the control keeps before a commit, and every edit it reports |
| `onSubmit` | `(event: EventPayload) => void` | — | `Enter` while editing; the commit has already run |
| `readOnly` / `autoFocus` | `boolean` | `false` | uneditable, or focused on mount |
| `invalid` / `description` | `boolean` · `string` | `false` · none | validation paint, and the help the field announces |
| `size` / `fullWidth` / `tabIndex` | `"sm" \| "md" \| "lg"` · `boolean` · `number` | `"md"` · `false` · the field's place | the input's height, its width, and its tab stop |
| `ref` / `aria-id` / `onLayout` | the addressed element | — | the input while editing: focus it, measure it, name it for automation |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | — | composed with the control's own; yours runs first |
---
# Input group
Source: https://gpuixui.sudokw.com/components/input-group
A field and its addons inside one bordered, focus-ringed row
## Usage [#usage]
```tsx
import {
InputGroup,
InputGroupAddon,
InputGroupInput,
InputGroupText,
} from "@gpuixui/components/input-group";
export const Search = () => (
⌘
K
);
```
## Composition [#composition]
```text
InputGroup
├── label / description / error (the group's own slots)
├── InputGroupAddon align="block-start"
├── InputGroupAddon align="inline-start"
├── InputGroupInput | InputGroupTextarea
├── InputGroupAddon align="inline-end"
├── InputGroupButton
├── InputGroupText
└── InputGroupAddon align="block-end"
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `InputGroup` | `label` | `string` — the visible label and every group field's name | — |
| `InputGroup` | `description` | `string` — supporting copy, hidden while an error shows | — |
| `InputGroup` | `error` | `string` — paints the row invalid and takes the description slot | — |
| `InputGroup` | `invalid` | `boolean` — invalid without a message | `false` |
| `InputGroup` | `className` / `style` | `string` / `StyleDesc` | — |
| `InputGroup` | `testId` / `children` | `string` / `PartChildren` | — |
| `InputGroupAddon` | `align` | `"inline-start" \| "inline-end" \| "block-start" \| "block-end"` | `"inline-start"` |
| `InputGroupAddon` | `className` / `style` | `string` / `StyleDesc` | — |
| `InputGroupAddon` | `testId` / `children` | `string` / `ReactNode` | — |
| `InputGroupButton` | action props | `variant` (`"ghost"` by default), `size`, `disabled`, `onClick` — the shared `Button` contract | `size="sm"` |
| `InputGroupButton` | busy props | `loading`, `loadingLabel`, `reducedMotion`, `leading`, `trailing` — the shared `Button` busy contract | — |
| `InputGroupText` | `className` / `style` | `string` / `StyleDesc` | — |
| `InputGroupText` | `children` | `ReactNode` | — |
| `InputGroupInput` | field props | `value`, `defaultValue`, `onValueChange`, `onChange`, `onSubmit`, `placeholder`, `readOnly`, `disabled`, `autoFocus`, `invalid`, `size`, `fullWidth`, `label`, `tabIndex`, `description`, `onClick`, `onKeyDown`, `onKeyUp`, `onFocus`, `onBlur`, `ref`, `aria-id`, `onLayout`, `className`, `style`, `testId` — the primitives' input contract | — |
| `InputGroupTextarea` | field props | the same as `InputGroupInput` without `onSubmit`, plus `minRows` and `maxRows` | — |
---
# Inset group
Source: https://gpuixui.sudokw.com/components/inset-group
A settings form's grouped rows, as System Settings draws them
## Usage [#usage]
```tsx
import { InsetGroup, InsetGroupRow } from "@gpuixui/components/inset-group";
}
/>
} />
;
```
## Composition [#composition]
```text
InsetGroup
├── title (optional)
└── box — rounded card, hairline edge
└── InsetGroupRow × n — label leading, control trailing, a hairline between rows
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `InsetGroup` | `title` | `string` | — |
| `InsetGroup` | `titleTone` | `"strong" \| "muted"` — a muted caption reads as System Settings' section label | `"strong"` |
| `InsetGroup` | `elevated` | `boolean` — lifts the box on the theme's `md` shadow | `false` |
| `InsetGroup` | `insetDividers` | `boolean` — hairlines stop short of the box's edges | `false` |
| `InsetGroup` | `children` | `ReactNode` — the `InsetGroupRow`s | — |
| `InsetGroupRow` | `label` | `string` | — |
| `InsetGroupRow` | `description` | `string` — a secondary line | — |
| `InsetGroupRow` | `control` | `ReactNode` — a switch, a select, a segmented control | — |
---
# Item
Source: https://gpuixui.sudokw.com/components/item
One row of a list, with media, text and actions
## Usage [#usage]
```tsx
import {
Item,
ItemActions,
ItemContent,
ItemDescription,
ItemGroup,
ItemMedia,
ItemTitle,
} from "@gpuixui/components/item";
export const Files = () => (
-
▤
design-system.md
Tokens and semantic roles.
Open
);
```
## Composition [#composition]
```text
ItemGroup
└── Item
├── ItemHeader (own line, above)
├── ItemMedia (before the content)
├── ItemContent
│ ├── ItemTitle
│ └── ItemDescription
├── ItemActions (after the content)
└── ItemFooter (own line, below)
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Item` | `variant` | `"default" \| "outline" \| "muted"` | `"default"` |
| `Item` | `size` | `"default" \| "sm"` | `"default"` |
| `Item` | `className` | `string` | — |
| `Item` | `children` | `PartChildren` | — |
| `ItemGroup` | `className` / `children` | `string` / `PartChildren` | — |
| `ItemMedia` | `variant` | `"default" \| "icon" \| "image"` | `"default"` |
| `ItemMedia` | `className` / `children` | `string` / `PartChildren` | — |
| `ItemContent` | `className` / `children` | `string` / `PartChildren` | — |
| `ItemTitle` | `className` / `children` | `string` / `ReactNode` | — |
| `ItemDescription` | `className` / `children` | `string` / `ReactNode` | — |
| `ItemActions` | `className` / `children` | `string` / `PartChildren` | — |
| `ItemHeader` | `className` / `children` | `string` / `PartChildren` | — |
| `ItemFooter` | `className` / `children` | `string` / `PartChildren` | — |
---
# Kanban
Source: https://gpuixui.sudokw.com/components/kanban
Columns of cards a person reorders with a pointer or the arrow keys
A board places cards in columns and reports every move. The board owns the flat
placement list in uncontrolled mode, and a controlled `order` keeps it in the
caller's store.
This page is the source, not a live preview. The board is a GPUix surface; the
browser drawing on [the Kanban block](/docs/blocks/kanban) is an illustration
of it.
## Usage [#usage]
```tsx
import { Kanban } from "@gpuixui/components/kanban";
import type {
KanbanCard,
KanbanColumn,
KanbanMove,
} from "@gpuixui/components/kanban";
const columns: ReadonlyArray = [
{ id: "open", title: "Open" },
{ id: "hunting", title: "Hunting" },
{ id: "cleared", title: "Cleared" },
];
const cards: ReadonlyArray = [
{ id: "exam", column: "open", title: "Pass the Hunter Exam" },
{ id: "troupe", column: "hunting", title: "Track the Phantom Troupe" },
];
const move = (next: KanbanMove) => save(next);
open(card.id)}
/>;
```
## Moving [#moving]
A card moves by pointer or keyboard; both routes produce the same `KanbanMove`.
- **Pointer** — press a card, move, release. A single insertion line marks the
slot; the source card stays in place at reduced opacity and no column or card
is tinted.
- **Keyboard** — a focused card moves with `Up`/`Down` inside its column and
`Left`/`Right` into the neighbouring one. The move stops at the ends; it does
not wrap. `Enter` or `Space` presses the card.
`onMove` reports `id`, `from`, `to` and `index`. The index counts the destination
column **without** the moving card, which is the slot the line painted.
`onOrderChange` reports the whole arrangement after an accepted move.
## Controlled order [#controlled-order]
Pass `order` to own the arrangement, or `defaultOrder` to start uncontrolled.
An order saved before a card was added is still readable: a card the order does
not name is appended to its own `column`, and an entry naming a card or column
this render does not carry is dropped.
## Limitations [#limitations]
- `0.11.0` has no `aria-grabbed` or `aria-dropeffect`, so a drag is not
announced as a drag; the card's name and the arrow contract carry the
interaction.
- No live region: a move is not announced. The column counts are painted text.
- The runtime has no `document`, `window` or `getBoundingClientRect`, so
`@dnd-kit/core` cannot run here. Drag is pointer capture plus
`renderer.getElementBounds` — the same model the shipped `Slider` uses.
- There is no auto-scroll while a long column is dragged past its edge.
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Kanban` | `columns` | `ReadonlyArray` — `id` and `title` | — |
| `Kanban` | `cards` | `ReadonlyArray` — `id`, `column`, `title`, optional `description` | — |
| `Kanban` | `order` | `KanbanOrder` — a controlled placement list | — |
| `Kanban` | `defaultOrder` | `KanbanOrder` — the uncontrolled starting list | the cards' own columns |
| `Kanban` | `onOrderChange` | `(order: KanbanOrder) => void` | — |
| `Kanban` | `onMove` | `(move: KanbanMove) => void` | — |
| `Kanban` | `onCardClick` | `(card: KanbanCard) => void` | — |
| `Kanban` | `label` | `string` — the board's accessible name | `Kanban board` |
| `Kanban` | `cardLabel` | `(card: KanbanCard) => string` | the card's title |
| `Kanban` | `renderCard` | `(card, state) => ReactNode` — content inside the drag surface | title and description |
| `Kanban` | `style` | `StyleDesc` — merged last | — |
| `Kanban` | `testId` | `string` — card and column locators extend it | — |
`KanbanMove` is `{ id, from, to, index }`; `KanbanOrder` is
`ReadonlyArray<{ id, column }>` in reading order.
## Related [#related]
- [Kanban block](/docs/blocks/kanban) — the browser drawing of this board
- [Card](/docs/components/card) — the content a card can hold
- [List](/docs/components/list) — a single-column alternative
---
# Keyboard shortcuts
Source: https://gpuixui.sudokw.com/components/keyboard-shortcuts
A cheat sheet of chords as platform keycaps
## Usage [#usage]
```tsx
import {
KeyboardShortcut,
KeyboardShortcuts,
KeyboardShortcutsGroup,
} from "@gpuixui/components";
;
```
## Composition [#composition]
```text
KeyboardShortcuts
└── KeyboardShortcutsGroup
└── KeyboardShortcut
└── Keycap
```
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `KeyboardShortcuts` | `div` with `role="list"` | `label?`, `className?`, `children` |
| `KeyboardShortcutsGroup` | `div` with `role="group"` | `label` (required), `className?`, `children` |
| `KeyboardShortcut` | `div` with `role="listitem"` | `label` (required), `accelerator` (required), `platform?`, `style?`, `className?` |
| `Keycap` | one key cap | `children: string`, `style?`, `className?` |
---
# Knob
Source: https://gpuixui.sudokw.com/components/knob
A rotary control for one value in a range
## Usage [#usage]
```tsx
import { Knob } from "@gpuixui/components/knob";
;
```
Drag upward to turn it up, or focus it and use the arrow keys, Home and End.
The sweep runs through 270°, with the middle of the range at the top.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` — the accessible name | — |
| `value` | `number` | uncontrolled |
| `defaultValue` | `number` | `min` |
| `min`, `max` | `number` | `0`, `100` |
| `step` | `number` | `1` |
| `size` | `number` — the diameter | `48` |
| `disabled` | `boolean` | `false` |
| `indicatorColor` | `string` | the theme's primary |
| `onValueChange` | `(value: number) => void` | — |
| `style` | `StyleDesc` | — |
Use [Slider](/primitives/slider) when the exact position along a range matters.
---
# List
Source: https://gpuixui.sudokw.com/components/list
Rows with one tab stop, type-ahead and a row action
## Installation [#installation]
## Usage [#usage]
```tsx
import {
List,
ListItem,
ListItemDescription,
ListItemTitle,
} from "@gpuixui/components/list";
const files = [
{ value: "readme", title: "README.md", description: "The project's front door" },
{ value: "design", title: "Design notes", description: "docs/design-notes.md" },
];
export const FileList = ({ open }: { open: (value: string) => void }) => (
{files.map((file) => (
{file.title}
{file.description}
))}
);
```
## Composition [#composition]
```text
List
└── ListItem
├── ListItemTitle
├── ListItemDescription
├── leading
└── trailing
```
## API reference [#api-reference]
### List [#list]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `selectionMode` | `"none" \| "single" \| "multiple"` | `"none"` |
| `value` | `string` (single) · `ReadonlyArray` (multiple) — controlled | — |
| `defaultValue` | the same shape, initial selection when uncontrolled | `""` · `[]` |
| `onValueChange` | `(value: string) => void` (single) · `(values: ReadonlyArray) => void` (multiple) | — |
| `onActivate` | `(value: string) => void` — single and `none` only; absent from the multiple variant | — |
| `dense` | `boolean` — a row at `density.rowHeight` instead of `rowHeight + spacing.md` | `false` |
| `children` | parts — `ListItem` rows, one `ListSection` per group | — |
| `className` | `string` — joined after the root recipe | — |
### ListItem [#listitem]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `string` (required) | — |
| `textValue` | `string` — the text type-ahead matches | — |
| `disabled` | `boolean` | `false` |
| `leading` | `ReactNode` — an icon, avatar or marker | — |
| `trailing` | `ReactNode` — metadata, a badge or an action | — |
| `children` | parts — `ListItemTitle`, `ListItemDescription` and the row's slots | — |
| `className` | `string` — joined after the row recipe | — |
| `style` | `StyleDesc` or a function of `{ active, disabled, selected }` | — |
---
# Mention input
Source: https://gpuixui.sudokw.com/components/mention-input
`@name` autocompletion in a text field
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
MentionInput,
MentionInputItem,
MentionInputList,
} from "@gpuixui/components";
notify(name)}
>
{people.map((person) => (
))}
;
```
## Composition [#composition]
```text
MentionInput
├── MentionInputList
│ └── MentionInputItem
```
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `MentionInput` | a `role="group"` field wrapping an ` ` | `label` (required), `value?`, `defaultValue?`, `onValueChange?`, `onMention?`, `maxVisible?`, `disabled?`, `className?`, `children`, `placeholder?`, `readOnly?`, `autoFocus?`, `invalid?`, `size?`, `fullWidth?`, `tabIndex?`, `testId?`, `onClick?`, `onKeyDown?`, `onKeyUp?`, `onFocus?`, `onBlur?`, `ref?`, `aria-id?`, `onLayout?`, `description?` |
| `MentionInputList` | anchored suggestion surface, labelled "Mentions" | `className?`, `children` |
| `MentionInputItem` | a selected button row | `value` (required), `label?`, `style?`, `className?` |
## Motion [#motion]
The suggestion list enters and leaves on the house `normal` level: a fade plus 4px
of travel out from the field's bottom edge. Closing the query — `Escape` or a
selection — keeps the box painted for its exit window, carrying the rows the
reader dismissed rather than re-ranking an empty query mid-fade, and the seam's
leaving guard stops a ghost row taking the press while the row drops its own tab
stop with the box. A list that only narrows to nothing is cut instead: the field
is still open and a ghost of rows the query no longer matches would misreport the
search. A field the caller disables cuts its list too, because the control
refuses every route and there is no gesture to finish.
`MotionProvider preference="reduced"` removes the journey: the box is gone in the
frame it closes.
---
# Message
Source: https://gpuixui.sudokw.com/components/message
A typical agent conversation
`Message` is the layout of one turn. It owns the author, avatar, timestamp,
metadata and alignment; `MessageContent` is the column between the avatar and
the row's edge; a `Bubble` is always a child and is never built here. The data
slots the row carried before — reasoning, tool calls, citations, streaming —
stay as props on `MessageContent` and paint outside the bubble surface.
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
Bubble,
BubbleContent,
Message,
MessageActions,
MessageAvatar,
MessageContent,
MessageFooter,
MessageHeader,
} from "@gpuixui/components";
{message.body}
;
```
## Composition [#composition]
```text
Message
├── MessageAvatar
└── MessageContent
├── MessageHeader
├── Bubble
│ └── BubbleContent
└── MessageFooter
└── MessageActions
```
`MessageGroup` stacks consecutive rows from one sender.
## Author and timestamp [#author-and-timestamp]
`Message` reads the author once and shares it with `MessageHeader` and
`MessageAvatar`, so a row cannot publish two different names. `variant` selects
the default author (`You`, `Assistant`, `System`) and the default side (`end`
for a user, `start` otherwise); `author`, `timestamp` and `meta` override what
the header paints. A timestamp is the caller's formatted string; no locale is
applied.
## Data slots [#data-slots]
Reasoning, tool calls, citations and the streaming marker are data, not
markup: presence is the only switch, and a restored record's absent slot paints
nothing. They sit outside the bubble, so a trace can be read without being
painted into the reply.
## Actions [#actions]
`MessageActions` renders nothing when no handler can serve a control. Put it in
`MessageFooter`; the footer follows the row's side.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `Message` | `role="group"` named by `label` or the author | `variant?`, `align?`, `author?`, `timestamp?`, `meta?`, `avatarSrc?`, `label?`, `className?`, `children` |
| `MessageAvatar` | the shipped `Avatar` at `sm` | `name?`, `src?`, `className?` |
| `MessageHeader` | the author, timestamp and meta line | `className?` |
| `MessageContent` | the slot column between avatar and edge | `reasoning?`, `toolCalls?`, `sources?`, `streaming?`, `streamingLabel?`, `reasoningLabel?`, `sourcesLabel?`, `className?`, `style?`, `children` |
| `MessageFooter` | the under-bubble line, on the row's side | `className?`, `children` |
| `MessageActions` | a named action row, or `null` | `onCopy?`, `onRetry?`, `onFeedback?`, `copyLabel?`, `retryLabel?`, `helpfulLabel?`, `notHelpfulLabel?`, `className?` |
| `MessageGroup` | consecutive rows, tighter | `gap?`, `className?`, `children` |
## Breaking change [#breaking-change]
`MessageRoot` is now `Message`. The old root built the bubble itself; the new
one does not. Every row must place a `Bubble` inside `MessageContent`, and
actions move from `MessageContent` to `MessageFooter`. This is the shadcn
structure the component is modelled on; the split is what lets a bubble be
quoted, grouped or reused on its own.
---
# Message Scroller
Source: https://gpuixui.sudokw.com/components/message-scroller
Follows new content, keeps a scrolled-up reader in place
MessageScroller is the transcript. It follows new rows and a growing streamed
reply while the reader is at the end; the moment they scroll away it stops, keeps
their position and offers the way back with the number of rows that arrived
unseen.
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
MessageScroller,
MessageScrollerContent,
MessageScrollerItem,
MessageScrollerJumpToLatest,
MessageScrollerViewport,
} from "@gpuixui/components";
{messages.map((message) => (
{message.body}
))}
;
```
## Composition [#composition]
```text
MessageScroller
├── MessageScrollerViewport
│ └── MessageScrollerContent
│ └── MessageScrollerItem
└── MessageScrollerJumpToLatest
```
## The height contract [#the-height-contract]
The viewport is the flexible middle of the scroller: it grows, may shrink and
never makes the frame taller. The jump control floats over the viewport's bottom
edge, centred, so showing it does not take height from the transcript or move
anything below the scroller. The root is the positioned box that the control
resolves against. Give the scroller a bounded height (directly, or through a
sized flex parent) because a viewport with no height has nothing to scroll.
The viewport carries no inset of its own: put the transcript's padding on
`MessageScrollerContent` (or the rows), so the scrollbar sits at the edge and
the first and last row keep their inset.
## Following and releasing [#following-and-releasing]
- A reader inside `threshold` (default 24px) of the tail is at the end. New rows
and a reply that grows keep the tail visible.
- Scrolling away releases the view: appends and growth change nothing, and the
floating control counts the rows that arrived.
- Jump to latest returns to the end and re-pins.
- A row that grows through its own state is still followed while pinned: the
item's own `onLayout` size report is the measurement, so no React render is
needed.
- A sibling that re-renders does not move the tail: the follow rule depends on
the row count and on measured sizes, never on a plain render.
- An observation the scroller cannot read (`NaN`) is ignored; the pin state is
kept.
## Turn anchoring [#turn-anchoring]
`MessageScrollerItem scrollAnchor` places a newly mounted row near the top with
`previousItemPeek` of the row above still visible, but only while the reader is
pinned. It is opt-in: a Chat that appends a user row and a reply should mark the
user row as the anchor if it wants shadcn's turn behaviour. An unpinned reader
is never moved.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `MessageScroller` | `role="group"` named by `label` | `label` (required), `threshold?`, `previousItemPeek?`, `pinned?`, `defaultPinned?`, `onPinnedChange?`, `streaming?`, `className?`, `children` |
| `MessageScrollerViewport` | the scroll container and its tail sentinel | `style`, `onScroll?`, `onLayout?`, `ref?`, `className?`, `children` |
| `MessageScrollerContent` | the rows column; publishes `aria-busy` | `className?`, `children` |
| `MessageScrollerItem` | one row, no surface; reports its size | `scrollAnchor?`, `className?`, `children`, `ref?` |
| `MessageScrollerJumpToLatest` | a floating secondary control, centred over the viewport's bottom edge | `label?`, `className?`, `style?` |
---
# MultiStepForm
Source: https://gpuixui.sudokw.com/components/multi-step-form
A gated wizard over Stepper, submitted on the last step
## Usage [#usage]
```tsx
import { MultiStepForm } from "@gpuixui/components";
import * as Option from "effect/Option";
,
gate: () =>
namePattern.test(name)
? Option.none()
: Option.some("Use 3 to 24 lowercase letters, digits or hyphens"),
},
{
id: "port",
title: "Port",
content: ,
gate: () =>
portUsable(port) ? Option.none() : Option.some("Enter a port from 1024 to 65535"),
},
{
id: "review",
title: "Review",
content: ,
gate: () =>
accepted ? Option.none() : Option.some("Accept the checklist first"),
},
]}
onSubmit={create}
/>;
```
## Steps [#steps]
Each entry of `steps` is a `MultiStep`:
| Field | Type | Default | Notes |
| ----- | ---- | ------- | ----- |
| `id` | `string` | required | The step's stable key, shared with its indicator row. |
| `title` | `string` | required | The step's name in the indicator and in the content's announcement; a blank title reads as its position (`Step 2`), and a value a saved record kept as something other than text is no title at all. |
| `description` | `string` | — | One line of supporting copy in the indicator; a blank line paints nothing, and a non-text field paints the same way. |
| `content` | `ReactNode` | required | Mounted only while the step is current. |
| `gate` | `() => Option.Option` | — | Runs before the step moves and before the last step submits; the message it returns keeps the reader put, and a message that says nothing paints the shipped sentence instead of an empty alert. A `gate` that is not a function — the `null` a saved record wrote for a field it never carried — is no gate, so the step moves. |
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `steps` | `ReadonlyArray` | required |
| `onSubmit` | `() => void` | required |
| `step` | `number` | — (uncontrolled) |
| `defaultStep` | `number` | `1` |
| `onStepChange` | `(step: number) => void` | — |
| `pending` | `boolean` | `false` |
| `submitLabel` | `string` | `"Submit"` |
| `nextLabel` | `string` | `"Next"` |
| `backLabel` | `string` | `"Back"` |
| `label` | `string` | — |
| `className` | `string` | — |
| `style` | `StyleDesc` | — |
## Names [#names]
`label` names the wizard group; a blank name is an **absence**, so the root is not published with an empty name. The indicator is always named (`Steps` when the wizard is not), a step whose `title` arrived blank is named by its position (`Step 2`) so no list item is unnamed and no announcement opens with a comma, and a title is published trimmed. That rule is the **stepper's** (`stepName` in `stepper/logic.ts`), so a `StepperStep` written directly and the wizard's content announcement cannot disagree about it. A blank `description` paints no line — an empty string lays out a zero-height run, but a run of spaces would lay out a full line. **A field a record kept as something other than text is no copy at all**: a saved definition's `title: 42` or `description: {}` reads through `textOf` before the blank rule, so the step keeps its position name and paints no description line instead of the render throwing out of `accessibleName`. The three action labels are copy too: a blank `nextLabel`, `backLabel` or `submitLabel` reads as its documented default, because a missing i18n row must not paint a blank box as the wizard's only way forward.
A gate's message is copy on the same rule. `Some("")` still **refuses** the move, so a table with a missing row cannot release a step its author gated, but the painted alert reads `This step is not complete yet.` rather than nothing — an invisible refusal leaves the reader pressing a button that does nothing.
---
# Navigation menu
Source: https://gpuixui.sudokw.com/components/navigation-menu
A horizontal nav that opens one anchored panel at a time
## Installation [#installation]
## Usage [#usage]
```tsx
import {
NavigationMenu,
NavigationMenuContent,
NavigationMenuItem,
NavigationMenuLink,
NavigationMenuList,
NavigationMenuTrigger,
} from "@gpuixui/components/navigation-menu";
Platform
open("overview")}>
Overview
;
```
## Composition [#composition]
```text
NavigationMenu
└── NavigationMenuList
└── NavigationMenuItem
├── NavigationMenuTrigger
└── NavigationMenuContent
└── NavigationMenuLink
```
## API reference [#api-reference]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `NavigationMenu` | `role="navigation"`, named by `label` | `label` (required), `value`, `defaultValue`, `onValueChange`, `openDelay`, `closeDelay`, `className`, `children` |
| `NavigationMenuList` | `role="list"` row | `className`, `children` |
| `NavigationMenuItem` | `role="listitem"` | `value` (required), `className`, `children` |
| `NavigationMenuTrigger` | button carrying `aria-expanded` and a fixed-slot chevron | every `Button` prop, plus `disabled`; `trailing` renders before the chevron; `onActivate` is the action's handler and `onClick` its deprecated alias |
| `NavigationMenuContent` | anchored `role="group"` panel | `className`, `children` |
| `NavigationMenuLink` | full-width ghost button row | `onSelect`; `onActivate` with `onClick` as the deprecated alias; every `Button` prop |
---
# Page transition
Source: https://gpuixui.sudokw.com/components/page-transition
A region that swaps views with the house motion
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { PageTransition } from "@gpuixui/components";
{section === "account" ? : }
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `viewKey` | `string` | — |
| `children` | `ReactNode` | — |
| `direction` | `"forward" \| "back"` | `"forward"` |
| `distance` | `number` | `16` |
| `level` | `SurfaceLevel` | `"normal"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Pagination
Source: https://gpuixui.sudokw.com/components/pagination
Previous and next controls around a stable-width page window
## Installation [#installation]
## Usage [#usage]
```tsx
import { useState } from "react";
import {
Pagination,
PaginationList,
PaginationNext,
PaginationPrevious,
} from "@gpuixui/components/pagination";
const PAGE_SIZE = 5;
export const DeploymentPages = ({ deployments }: { deployments: string[] }) => {
const [page, setPage] = useState(1);
const pageCount = Math.ceil(deployments.length / PAGE_SIZE);
const rows = deployments.slice((page - 1) * PAGE_SIZE, page * PAGE_SIZE);
return (
<>
>
);
};
```
## Composition [#composition]
```text
Pagination
├── PaginationPrevious
├── PaginationList
│ ├── PaginationItem
│ └── PaginationEllipsis
└── PaginationNext
```
## API reference [#api-reference]
| Part | Prop | Type | Default | Consequence |
| ---- | ---- | ---- | ------- | ----------- |
| `Pagination` | `page` | `number` (required) | — | the current page, 1-based; clamped into `[1, pageCount]` |
| `Pagination` | `pageCount` | `number` (required) | — | pages that exist; zero or fewer renders nothing |
| `Pagination` | `onPageChange` | `(page: number) => void` (required) | — | a step or a page press |
| `Pagination` | `siblingCount` | `number` | `1` | pages shown either side of the current one |
| `Pagination` | `showEdges` | `boolean` | `true` | keep the first and last page visible |
| `Pagination` | `children` | parts | — | `PaginationPrevious`, `PaginationList`, `PaginationNext` |
| `PaginationItem` | `page` | `number` (required) | — | the page this button goes to |
| `PaginationItem` | `style` | `StateStyle` | recipe | a style or a function of `{ active }` |
| `PaginationPrevious` / `PaginationNext` | — | — | — | step by one; disabled at their end |
| `PaginationList` / `PaginationEllipsis` | `className` | `string` | — | an extra recipe class, joined last |
---
# Pinned list
Source: https://gpuixui.sudokw.com/components/pinned-list
Headers that stay pinned while rows scroll under them
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { PinnedList } from "@gpuixui/components/pinned-list";
},
{ title: "Notifications", children: },
{ title: "Danger zone", children: },
]}
/>;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `sections` | `ReadonlyArray` (a value that is not a list reads as none; an entry without a text `title` is dropped) | — |
| `headerHeight` | `number` | `32` |
| `surfaceColor` | `string` | theme `background` |
| `className` | `string` | — |
| `style` | `StyleDesc` | — |
| `testId` | `string` | — |
| `aria-label` | `string` | the runtime's "Scroll region" |
| `onScrollChange` | `(event) => void` | — |
| `onLayout` | `(event) => void` | — |
---
# Popover
Source: https://gpuixui.sudokw.com/components/popover
A floating panel anchored to the control that opened it
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Popover,
PopoverContent,
PopoverTrigger,
} from "@gpuixui/components/popover";
Columns
;
```
## Composition [#composition]
```text
Popover
├── PopoverTrigger
└── PopoverContent
```
## Sides [#sides]
## Collision [#collision]
## API reference [#api-reference]
### Popover [#popover]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `open` | `boolean` | uncontrolled |
| `defaultOpen` | `boolean` | `false` |
| `onOpenChange` | `(open: boolean) => void` | none |
| `children` | parts — `PopoverTrigger`, `PopoverContent` | — |
### PopoverContent [#popovercontent]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `side` | `"top" \| "right" \| "bottom" \| "left"` | `"bottom"` — a stored name the union does not carry |
| `align` | `"start" \| "center" \| "end"` | `"center"` — a stored name the union does not carry |
| `sideOffset` | `number` | `6` |
| `collisionPadding` | `number` | `8` |
| `alignOffset` | `number` | `0` |
| `className` | `string` — joined after the surface recipe | — |
| `children` | `ReactNode` — the surface's content | — |
### PopoverTrigger [#popovertrigger]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `onActivate` | `(event) => void` | — |
| `onClick` | `(event) => void` | — |
## Theming [#theming]
The surface paints the theme's `elevation.lg` beside its 1px `foreground` ring. Its corner is the recipe's fixed **24px** (`rounded-3xl`); the preview follows the platform radius and is the approximation.
---
# Preview card
Source: https://gpuixui.sudokw.com/components/preview-card
A rich, hoverable card shown on hover or on a press
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import {
PreviewCard,
PreviewCardContent,
PreviewCardTrigger,
} from "@gpuixui/components";
design-system.md
Tokens, semantic roles and the theme extension mechanism.
;
```
## Composition [#composition]
```text
PreviewCard
├── PreviewCardTrigger
└── PreviewCardContent
```
## Behaviour [#behaviour]
- `PreviewCardContent` takes the popover's `align`: `start` shares the trigger's
left edge, `center` (the default) its centre line and `end` its right edge,
measured against the trigger rather than the runtime's containing block.
- The card is hoverable, so the pointer can leave the trigger and cross the 6px
gap: the close is delayed (`closeDelay`, 150 ms by default) and the card's own
enter cancels it.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `PreviewCard` | a positioning root; owns open state and the timers | `open?`, `defaultOpen?`, `onOpenChange?`, `openDelay?`, `closeDelay?`, `children` |
| `PreviewCardTrigger` | button, `aria-expanded` | button props |
| `PreviewCardContent` | anchored `role="group"` named by `label` | `label` (required), `side?`, `align?` (default `center`), `sideOffset?`, `collisionPadding?`, `alignOffset?`, `className?`, `children` |
---
# Progress
Source: https://gpuixui.sudokw.com/components/progress
A determinate or indeterminate bar in the caller's scale
## Usage [#usage]
```tsx
import {
Progress,
ProgressLabel,
ProgressValue,
} from "@gpuixui/components/progress";
;
```
`value` and `max` are the caller's units: `30 of 120` above paints the same
quarter-full bar as `25 of 100`. Omit `value` — or pass the `null` JSON writes
for a field a record never carried — for the indeterminate segment.
## Composition [#composition]
```text
Progress
├── ProgressLabel
├── ProgressValue
└── ProgressTrack
└── ProgressIndicator
```
The root always paints the track and its fill after the children, so a caller
who composes only the label and the value still gets a bar. The four parts read
the root's state, so a share is computed once.
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Progress` | `value` | `number \| null` | — (indeterminate) |
| `Progress` | `max` | `number` | `100` |
| `Progress` | `tone` | `"primary" \| "success" \| "destructive" \| "muted"` | `"primary"` |
| `Progress` | `size` | `"sm" \| "md"` | `"md"` |
| `Progress` | `label` | `string` | — |
| `Progress` | `formatValue` | `(percent: number) => string` | `` `${percent}%` `` |
| `Progress` | `loadingText` | `string` | the provider's `loading` word |
| `Progress` | `reducedMotion` | `boolean` | the motion provider |
| `Progress` | `children` | `ReactNode` | the track |
| `Progress` | `style` | `StyleDesc` | — |
| `Progress` | `className` | `string` | — |
| `Progress` | `testId` | `string` | — |
| `Progress` | `ref` / `aria-id` / `onLayout` | host props | — |
| `ProgressTrack` | `style`, `className`, `testId` | — | — |
| `ProgressIndicator` | `style`, `className`, `testId` | — | — |
| `ProgressLabel` | `children`, `testId` | `ReactNode`, `string` | the root's `label` |
| `ProgressValue` | `testId` | `string` | — |
---
# Reorderable tabs
Source: https://gpuixui.sudokw.com/components/reorderable-tabs
A tab strip you can drag, close and reorder with the keyboard.
## Usage [#usage]
```tsx
import { ReorderableTabs } from "@/components/docs/reorderable-tabs";
;
```
## Interaction [#interaction]
- Drag a tab to a new slot; the strip shifts its neighbours by the gap and snaps on release.
- Alt + ← /→ moves the focused tab.
- × closes a tab; a closing tab collapses its measured width first.
The caller owns the order and the open tab, so a replay or an explorer press can put a closed tab back. This is a browser drawing, not the native `Tabs` component.
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `tabs` | `ReadonlyArray` | — | the open files, in strip order |
| `active` | `string \| null` | — | the open tab; `null` when every tab is closed |
| `label` | `string` | — | the strip's accessible name |
| `onActivate` | `(id: string) => void` | — | reports the pressed tab |
| `onReorder` | `(tabs: ReadonlyArray) => void` | — | reports the new order |
| `onClose` | `(id: string) => void` | — | reports the closed tab |
| `className` / `tabClassName` / `closeClassName` | `string` | none | joined onto the strip, tab and close button |
---
# Reveal
Source: https://gpuixui.sudokw.com/components/reveal
Content that fades and travels into place
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { Reveal } from "@gpuixui/components/reveal";
Automatic updates are on.
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | — |
| `open` | `boolean` | — |
| `from` | `RevealFrom` | `"below"` |
| `distance` | `number` | `8` |
| `level` | `MotionLevel` | `"normal"` |
| `delayMs` | `number` | `0` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Scroll fade
Source: https://gpuixui.sudokw.com/components/scroll-fade
An edge that fades where more content is hidden
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { ScrollFade } from "@gpuixui/components/scroll-fade";
{notes.map((note) => (
))}
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | — |
| `color` | `string` | theme `background` |
| `edgeSize` | `number` | `32` |
| `label` | `string` | the runtime's "Scroll region" |
| `testId` | `string` | — |
| `className` | `string` | — |
| `style` | `StyleDesc` | — |
---
# Scroll progress
Source: https://gpuixui.sudokw.com/components/scroll-progress
A reading line for how far through content a reader is
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { ScrollProgress } from "@gpuixui/components/scroll-progress";
{settings.map((setting) => (
))}
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `children` | `ReactNode` | — |
| `side` | `"top" \| "bottom"` | `"bottom"` |
| `thickness` | `number` | `3` |
| `color` | `string` | `theme.colors.primary` |
| `trackColor` | `string` | `theme.colors.muted` |
| `label` | `string` | — |
| `testId` | `string` | — |
| `className` | `string` | — |
| `style` | `StyleDesc` | — |
---
# Search input
Source: https://gpuixui.sudokw.com/components/search-input
A single-line search field with a clear affordance
## Installation [#installation]
## Usage [#usage]
```tsx
import { SearchInput } from "@gpuixui/components/search-input";
import { Text } from "@gpuixui/primitives";
import { useState } from "react";
export const FileFilter = () => {
const [query, setQuery] = useState("");
const [last, setLast] = useState("none");
return (
<>
setQuery("")}
/>
Last submitted: {last}
>
);
};
```
## Composition [#composition]
```text
SearchInput
├── leading mark
├── input
└── clear control
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | none | the controlled text; omit it to let the field keep its own |
| `defaultValue` | `string` | `""` | the initial text in uncontrolled mode |
| `onValueChange` | `(value: string) => void` | none | every edit, including the clear and `Escape` paths |
| `onChange` | host change event | none | the raw host payload, called before the value is applied |
| `onSubmit` | `(value: string) => void` | none | `Enter`, with the current text |
| `onClear` | `() => void` | none | the clear control or `Escape`, only when the field held text |
| `placeholder` | `string` | `"Search…"` | the hint the field paints — and announces — while the value is empty |
| `disabled` | `boolean` | `false` | `readOnly` plus the half-opacity paint; no ring, no tab stop, and an inert clear target |
| `className` | `string` | none | joined after the recipe, so it wins conflicts |
| `style` | `StateStyle` | none | resolved against `{ disabled, focused }` and merged last on the field surface |
| `testId` | `string` | `"search-input"` | the input's locator; the painted hint is `${testId}-hint` |
| `label` / `description` | `string` | none | the field's accessible name, and the help it announces |
| `invalid` | `boolean` | `false` | the destructive edge a failed search paints |
| `required` | `boolean` | `false` | the field is required by the form around it |
| `readOnly` / `autoFocus` | `boolean` | `false` | uneditable, or focused on mount |
| `size` / `fullWidth` / `tabIndex` | `"sm" \| "md" \| "lg"` · `boolean` · `number` | `"md"` · `false` · the field's place | height and padding from density, the full width, and the tab order |
| `ref` / `aria-id` / `onLayout` | the addressed element | none | the inner ` `: focus it, measure it, name it for automation |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the field's own; yours runs first |
---
# Segmented control
Source: https://gpuixui.sudokw.com/components/segmented-control
One choice among a few, as a Mac view switcher
## Usage [#usage]
```tsx
import { SegmentedControl } from "@gpuixui/components/segmented-control";
const [view, setView] = useState("list");
;
```
## Composition [#composition]
```text
SegmentedControl (the muted track, one tab stop)
└── segment × n (the primitives' action; the chosen one raised)
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` — the group's accessible name | — |
| `items` | `{ value, label, icon?, disabled? }[]` | — |
| `value` | `string` — the chosen segment | — |
| `onValueChange` | `(value: string) => void` | — |
| `size` | `"sm" \| "md"` | `"sm"` |
## Behaviour [#behaviour]
- The arrow keys **select** as they move and stop at either end, skipping a
disabled segment — a Mac segmented control's rule, not a tab list's. Focus
follows the choice.
- Segments share the track's width equally.
## The chosen segment [#the-chosen-segment]
The chosen segment is the surface raised off the track. In a light theme that is the page's `background`;
in a dark theme the page is darker than the track, so it lifts the track colour toward white instead.
---
# Select
Source: https://gpuixui.sudokw.com/components/select
One choice from a list, in a popover
## Installation [#installation]
## Usage [#usage]
```tsx
import { useState } from "react";
import { Label, Select } from "@gpuixui/primitives";
const regions = [
{
label: "Europe",
options: [
{ value: "eu-west", label: "Europe — West (Ireland)" },
{ value: "eu-central", label: "Europe — Central (Frankfurt)" },
],
},
{
label: "Americas",
options: [
{ value: "us-east", label: "United States — East" },
{ value: "us-west", label: "United States — West" },
],
},
];
export const DeployTarget = () => {
const [region, setRegion] = useState("eu-west");
return (
<>
Region
>
);
};
```
## Composition [#composition]
```text
Select
├── trigger (built in)
├── content
│ └── item
└── group / label / separator
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `options` | `ReadonlyArray` | required | the rows, or named groups of them, in render order |
| `value` | `string` | none | controlled selection |
| `defaultValue` | `string` | none | initial selection when uncontrolled |
| `selectedValue` | `string` | `value ?? defaultValue` | the choice a closed trigger announces — a parent's mirror of an uncontrolled select's value |
| `open` | `boolean` | none | controlled open state |
| `defaultOpen` | `boolean` | `false` | the list's initial state while `open` is omitted |
| `onOpenChange` | `(open: boolean) => void` | none | the open state the runtime reported |
| `onValueChange` | `(value: string) => void` | none | the committed value |
| `onChange` | `(value: string) => void` | none | deprecated alias; `onValueChange` wins when both are passed |
| `placeholder` | `string` | none | the trigger's text before a choice |
| `disabled` | `boolean` | `false` | refuses to open |
| `invalid` | `boolean` | `false` | `destructive` edge on the trigger |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | height and padding from density |
| `fullWidth` | `boolean` | `false` | trigger and list fill the parent |
| `side` | `FloatingSide` | the runtime's choice | which side the list opens on |
| `align` | `FloatingAlign` | GPUix's default | how the list is aligned against the trigger |
| `label` | `string` | none | the accessible name — put a visible `Label` beside it |
| `description` | `string` | none | announced help after the name; a blank one publishes nothing |
| `ref` / `aria-id` / `onLayout` | host props | none | the trigger is the addressed element — the one a form focuses, an automation host names and a layout measures |
| `style` | `StyleDesc` | none | merged last on the wrapper, which paints only its width |
| `triggerStyle` | `StateStyle` | none | resolved against the trigger's live state, merged last |
| `itemStyle` | `StateStyle` | none | resolved per row against that row's state, merged last |
| `testId` | `string` | none | locator for the wrapper — it is **not** the trigger |
| `triggerTestId` | `string` | none | locator for the element that opens the list |
| `itemTestId` | `(value: string) => string` | none | locator for an option row, derived from its value |
---
# Separator
Source: https://gpuixui.sudokw.com/components/separator
A rule between regions, decorative or announced
## Usage [#usage]
```tsx
import { Separator } from "@gpuixui/components/separator";
;
;
;
```
A semantic rule is a `separator` node a reader can find between two regions;
`decorative` keeps a rule in the drawing and out of the accessibility tree. The
default is semantic, because a divider that separates content is content.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
| `decorative` | `boolean` | `false` |
| `inset` | `"2xs" \| "xs" \| "sm" \| "md" \| "lg" \| "xl" \| "2xl" \| "3xl" \| "4xl" \| "5xl"` | — |
| `label` | `string` | — |
| `style` | `StyleDesc` | — |
| `className` | `string` | — |
| `testId` | `string` | — |
| `aria-id` | `string` | — |
---
# Sheet
Source: https://gpuixui.sudokw.com/components/sheet
A modal panel anchored to any of four window edges
The drawing opens on the surface: the frame is the window, the `md` panel is
flush to the right edge it names, and the edge buttons re-open it — `top` and
`bottom` wear the grabber — while the veil washes and blurs the page behind it.
## Usage [#usage]
```tsx
import {
Sheet,
SheetClose,
SheetContent,
SheetDescription,
SheetFooter,
SheetHeader,
SheetTitle,
SheetTrigger,
} from "@gpuixui/components";
```
`Sheet` is shadcn's name for the surface `Drawer` already paints: a veil over
the window, a panel on one edge, a focus trap inside it, Escape and an outside
press to close, and focus returned to the opener. It re-exports that machinery,
so the drawing above is the Drawer page's — there is one implementation, not two.
The sheet **animates in**: the panel slides its own span from the edge it names
(a bottom sheet rises a whole 320px) over the `slow` level, the veil fades, and
the surface is clipped by the window while it travels. A bottom sheet wears the
vaul grabber and rounded inner corners; `MotionProvider preference="reduced"`
paints the settled frame at once. The veil is the page's background at 55%; the
blur the drawing carries has no native route (`StyleDesc` has no `filter`).
```tsx
Filters
Filters
Narrow the result set, then apply.
Cancel
```
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `Sheet` | `open` | `boolean` | uncontrolled |
| `Sheet` | `onOpenChange` | `(open: boolean) => void` | none |
| `Sheet` | `dismissible` | `boolean` | `true` |
| `SheetTrigger` | `disabled` | `boolean` | `false` |
| `SheetClose` | `disabled` | `boolean` | `false` |
| `SheetContent` | `side` | "left" \| "right" \| "top" \| "bottom" | `"right"` |
| `SheetContent` | `size` | "sm" \| "md" \| "lg" | `"md"` |
| `SheetContent` | `label` | `string` (required) | none |
| `SheetContent` | `grabber` | `boolean` — the vaul handle on a top/bottom sheet | `true` |
| `SheetContent` | `initialFocus` | `() => number \| null` | none |
| `SheetHeader` | `children` | `ReactNode` | — |
| `SheetTitle` | `children` | `ReactNode` | — |
| `SheetDescription` | `children` | `ReactNode` | — |
| `SheetFooter` | `children` | `ReactNode` | — |
---
# Shimmer
Source: https://gpuixui.sudokw.com/components/shimmer
A travelling highlight painted into text, or a surface band over children the runtime cannot recolour
## Usage [#usage]
```tsx
import { Shimmer } from "@gpuixui/components/shimmer";
// A plain string is inked one code point at a time from the travelling profile.
Generating a reply…
;
// A finished surface: the text rests and no profile runs.
Done ;
// Composed children cannot be recoloured, so they keep the surface band.
The assistant is reading the conversation.
;
```
A **string** child is a text run: every code point is its own text host and
takes the profile's ink at its place in the line. Any **other** child keeps the
surface band — a row of thin two-stop ramps over the content — because the
runtime cannot recolour a tree it did not build.
## Two drawings, honestly [#two-drawings-honestly]
The browser can put a gradient *inside* the glyphs with `background-clip: text`.
The native runtime cannot: it has no mask and no background-clip, and a ``
host paints one `color`. So the native component does the strongest thing it can
and says what it is:
- **A string child** is split into words and code points. Each code point takes
one colour sampled from the same triangular profile the band uses, blending
from the theme's `mutedForeground` toward the chosen accent. The sheen is discrete
at glyph resolution, not a continuous fill.
- **Any other child** keeps the band: `SHIMMER_STEPS` (16) strips, each a
two-stop ramp between neighbouring samples of that profile, stepping once per
`durationMs / SHIMMER_STEPS` (125 ms at the default).
The two paths are not the same drawing, and neither is presented as the other.
The browser preview puts the gradient in the text; the native string path is the
closest the runtime gets to it.
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | none | a string is inked per code point; any other child keeps the surface band |
| `active` | `boolean` | `true` | whether the profile runs; false rests the text and paints no band |
| `durationMs` | `number` | `2000` | one complete sweep; an unusable value falls back to two seconds |
| `accent` | `"foreground" \| "primary" \| "muted"` | `"foreground"` | the role the highlight paints; a text run blends toward it from the muted role, a band paints it at a low alpha |
| `role` | text role | `"body"` | the typographic role a text run paints |
| `label` | `string` | none | an accessible name; when set, the surface is a `status` node, and a blank name is decoration |
| `reducedMotion` | `boolean` | the motion provider | rests the text, paints no band and schedules no interval |
| `className` | `string` | — | joined after the surface recipe, so it wins conflicts |
| `style` | `StyleState` | — | a style, or a function of the frame's `peak`, `active` and `reducedMotion` |
| `testId` | `string` | — | stable locator; a text run is `testId-ink`, a band is `testId-overlay` |
---
# Sidebar
Source: https://gpuixui.sudokw.com/components/sidebar
The persistent navigation panel, collapsible to icons
## Usage [#usage]
```tsx
import {
Sidebar,
SidebarContent,
SidebarFooter,
SidebarGroup,
SidebarGroupContent,
SidebarGroupLabel,
SidebarHeader,
SidebarInset,
SidebarMenu,
SidebarMenuButton,
SidebarMenuItem,
SidebarProvider,
SidebarRail,
SidebarTrigger,
SidebarUserMenu,
} from "@gpuixui/components";
…
Platform
;
```
## Composition [#composition]
```text
SidebarProvider
├── Sidebar
│ ├── SidebarHeader
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ ├── SidebarMenuButton
│ │ ├── SidebarMenuAction
│ │ └── SidebarMenuBadge
│ ├── SidebarContent
│ │ └── SidebarGroup
│ │ ├── SidebarGroupLabel
│ │ ├── SidebarGroupAction
│ │ └── SidebarGroupContent
│ │ └── SidebarMenu
│ │ └── SidebarMenuItem
│ │ ├── SidebarMenuButton
│ │ └── SidebarMenuSub
│ │ └── SidebarMenuSubItem
│ │ └── SidebarMenuSubButton
│ ├── SidebarFooter
│ │ └── SidebarUserMenu
│ └── SidebarRail
└── SidebarInset
```
`Sidebar` renders its children once. On a window narrower than 768px that tree
is the modal sheet; the parts are the same, so a panel does not need a second
layout for mobile.
## Behaviour [#behaviour]
- **Collapsing** is `collapsible`: `"offcanvas"` parks the panel outside the
window edge, `"icon"` shrinks it to a 48px rail (64px for the floating and
inset variants, which keep their `p-2` gutter), and `"none"` leaves it fixed.
The gap in the flow pays the same width, so the content beside it moves by
exactly the panel it loses.
- **The keyboard** is ⌘/Ctrl + B. `SIDEBAR_KEYBOARD_SHORTCUT` is exported, and
`toggleSidebar()` is on `useSidebar()`. On a narrow window the shortcut opens
and closes the sheet.
- **The open state persists** through `SidebarPreference`: the default jar uses
shadcn's `sidebar_state` key and seven-day lifetime, in process memory. Pass
`preference` to keep it across launches.
- **Icon mode** clips the label and publishes the row's `tooltip`; only one tip
can be open, and the active row keeps its accent. `badge`, `action` and
sub-menus hide with the labels, as shadcn's selectors do.
- **The mobile sheet** is a `role="dialog"` with a scrim: pressing the scrim
dismisses it, Tab wraps inside it, Escape closes it, and focus returns to the
trigger. The desktop state is untouched.
- **The rail** toggles on a press and on an 8px drag toward its edge. It stays
at the window edge while an offcanvas panel is parked, so the panel can be
reopened.
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `SidebarProvider` | a full-size row and the state | `defaultOpen?`, `open?`, `onOpenChange?`, `preference?`, `side?`, `variant?`, `collapsible?`, `width?`, `iconWidth?`, `mobileWidth?`, `className?`, `style?`, `children` |
| `Sidebar` | the panel, or the sheet below 768px | `side?`, `variant?`, `collapsible?`, `className?`, `style?`, `children` |
| `SidebarTrigger` | the toggle action | `size?` |
| `SidebarRail` | the edge strip | `className?`, `style?` |
| `SidebarInset` | `role="main"` content | `className?`, `style?`, `children` |
| `SidebarHeader` | the heading block | `className?`, `children` |
| `SidebarContent` | the scrolling middle | `className?`, `style?`, `children` |
| `SidebarFooter` | the footer block | `className?`, `children` |
| `SidebarSeparator` | a hairline | `className?`, `style?` |
| `SidebarGroup` | a labelled group | `className?`, `children` |
| `SidebarGroupLabel` | the group heading (text) | `children?`, `className?` |
| `SidebarGroupAction` | an icon-only group action | `label`, `children?`, `className?` |
| `SidebarGroupContent` | the rows under a heading | `className?`, `children` |
| `SidebarMenu` | `role="list"` | `className?`, `children` |
| `SidebarMenuItem` | `role="listitem"` | `className?`, `children` |
| `SidebarMenuButton` | a row action | `label?`, `isActive?`, `variant?`, `size?`, `tooltip?`, `disabled?`, `children?`, `trailing?`, `className?`, `onClick?` |
| `SidebarMenuAction` | a row's trailing action | `label`, `showOnHover?`, `children?`, `className?` |
| `SidebarMenuBadge` | a row's count | `children?`, `className?` |
| `SidebarMenuSkeleton` | a loading row | `showIcon?`, `className?` |
| `SidebarMenuSub` | a submenu list | `className?`, `children` |
| `SidebarMenuSubItem` | one submenu row | `className?`, `children` |
| `SidebarMenuSubButton` | a submenu action | `label?`, `isActive?`, `size?`, `disabled?`, `children?`, `className?`, `onClick?` |
| `SidebarInput` | a plain field at the panel height | `value?`, `placeholder?`, `readOnly?`, `disabled?`, `className?`, `style?`, `testId?`, `onChange?` |
| `useSidebar` | the context | returns `state`, `open`, `setOpen`, `openMobile`, `setOpenMobile`, `isMobile`, `toggleSidebar`, `side`, `variant`, `collapsible`, `tooltip`, `showTooltip`, `hideTooltip` |
`SIDEBAR_WIDTH`, `SIDEBAR_WIDTH_ICON`, `SIDEBAR_WIDTH_MOBILE`, `SIDEBAR_BREAKPOINT`
and `SIDEBAR_KEYBOARD_SHORTCUT` are exported from the component module.
## Widths and the shortcut [#widths-and-the-shortcut]
`width` (default 256, shadcn's 16rem), `iconWidth` (48, 3rem) and `mobileWidth`
(288, 18rem) are shadcn's CSS variables as numbers a caller can override. A
non-finite, zero or negative value reads as the default.
The provider hears Cmd/Ctrl+B through the keys that bubble inside its subtree.
GPUix delivers window keys only to the application's render handler, so an app
that wants the shortcut to work with focus outside the sidebar forwards its root
`onKeyDown` through `isSidebarShortcut(event)` — the same rule the provider runs.
---
# Skeleton
Source: https://gpuixui.sudokw.com/components/skeleton
A placeholder block that reserves the box content will paint
## Usage [#usage]
```tsx
import { Skeleton } from "@gpuixui/components/skeleton";
;
;
;
```
`animate` is the shadcn switch. Leaving it out lets the app's `MotionProvider`
decide; `false` paints one frame and schedules no timer, which is the still
image a reduced-motion preference asks for.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `animate` | `boolean` | the motion provider |
| `width` | `number \| string` | `"100%"` |
| `height` | `number \| string` | the body line height |
| `radius` | `"none" \| "sm" \| "md" \| "lg" \| "xl" \| "full"` | `"sm"` |
| `circle` | `boolean` | `false` |
| `style` | `StyleDesc` | — |
| `className` | `string` | — |
| `testId` | `string` | — |
| `ref` / `aria-id` / `onLayout` | host props | — |
## Theming [#theming]
The block paints the theme's `skeleton` role, which the token contract measures
on every strip a placeholder can stand in — the page, a card, a popover and a
muted toolbar — so a switched platform style repaints it without a component
change.
---
# Slider
Source: https://gpuixui.sudokw.com/components/slider
One value on a track, or the two ends of a range
## Usage [#usage]
```tsx
import { Slider } from "@gpuixui/components/slider";
;
// A pair paints two thumbs; the ends are kept in order.
;
```
The shape of `value` chooses the surface: a number is one thumb, a
`[from, to]` pair is a range. A pair passed as `[70, 20]` paints as `20 … 70`,
and each end stops on the other rather than crossing it.
## Keyboard [#keyboard]
| Key | Effect |
| --- | ------ |
| `ArrowRight` / `ArrowUp` | one `step` forward |
| `ArrowLeft` / `ArrowDown` | one `step` back |
| `PageUp` / `PageDown` | ten steps |
| `Home` / `End` | the track's ends |
A disabled slider stays out of the tab order and answers none of these keys.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `number \| readonly [number, number]` | — |
| `defaultValue` | `number \| readonly [number, number]` | — |
| `onValueChange` | `(value: SliderValue) => void` | — |
| `onValueCommit` | `(value: SliderValue) => void` | — |
| `min` | `number` | `0` |
| `max` | `number` | `100` |
| `step` | `number` | `1` (`0` is continuous) |
| `disabled` | `boolean` | `false` |
| `invalid` | `boolean` | `false` |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
| `label` | `string` | — |
| `description` | `string` | — |
| `formatValue` | `(value: number) => string` | `String(value)` |
| `className` | `string` | — |
| `testId` | `string` | — |
| `style` | `StyleDesc` | — |
| `ref` / `aria-id` / `onLayout` | host props | — |
---
# Sliding number
Source: https://gpuixui.sudokw.com/components/sliding-number
A number whose digits roll like a counter
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { SlidingNumber } from "@gpuixui/components/sliding-number";
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `number` | — |
| `decimals` | `number` | `0` |
| `prefix` | `string` | — |
| `suffix` | `string` | — |
| `label` | `string` | the reading |
| `level` | `MotionLevel` | `"normal"` |
| `role` | `TextRole` | `"body"` |
| `className` | `string` | — |
| `style` | `StateStyle<{ value, text, reducedMotion }>` | — |
---
# Spinner
Source: https://gpuixui.sudokw.com/components/spinner
A ring of ticks whose lit arc steps around the circle, in three sizes
## Usage [#usage]
```tsx
import { Spinner } from "@gpuixui/components/spinner";
;
;
;
```
`size` is a step on the theme's icon scale, so a spinner lines up with the text
or glyph beside it. A button takes the same mark through `Button`'s `loading`
prop instead of composing one.
## The anatomy, and why it steps [#the-anatomy-and-why-it-steps]
The reference spinner is a loader circle: one arc of a ring, turning. That
anatomy is what this mark keeps — a ring, a gap, an arc — but the runtime cannot
rotate it. `@gpuix/native@0.11.0` publishes no `transform`, so the ring is
**composed from static ticks** (`spinnerTicks` places 12 of them on a circle) and
the **lit arc** moves instead: the head is opaque, the seven ticks behind it fade
to a fifth, and the other five are empty. One tick advances per interval, so the
gap travels around the circle rather than the ring turning.
`MotionProvider preference="reduced"` — or `reducedMotion` on this spinner —
holds the resting arc and schedules no interval. The preview above is a browser
drawing: it paints the same geometry and steps it on the same cadence.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |
| `label` | `string` | the provider's `loading.busy` (`"Loading"`) |
| `accent` | `"primary" \| "destructive" \| "muted" \| "on-primary" \| "on-secondary" \| "on-destructive" \| "on-accent" \| "on-disabled"` | `"primary"` |
| `reducedMotion` | `boolean` | the motion provider |
| `className` | `string` | — |
| `style` | `StyleDesc` | — |
| `testId` | `string` | — |
| `ref` / `aria-id` / `onLayout` | host props | — |
---
# Stepper
Source: https://gpuixui.sudokw.com/components/stepper
Progress through named steps, from one value
## Installation [#installation]
## Usage [#usage]
```tsx
import { Stepper, StepperStep } from "@gpuixui/components";
;
```
## Composition [#composition]
```text
Stepper
└── StepperStep
```
## API reference [#api-reference]
### `Stepper` [#stepper]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | the current step, 1-based; steps below it are complete |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` — a stored name the union does not carry | a row of steps, or a column for a sidebar |
| `label` | `string` | none | the list's accessible name (`aria-label`) |
| `children` | `ReactNode` | — | `StepperStep` parts, in order |
| `style` | `StyleDesc` | — | merged last |
| `className` | `string` | — | joined last |
| `testId` | `string` | — | stable locator |
### `StepperStep` [#stepperstep]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | this step's position, 1-based, on the root's scale |
| `label` | `string` | — | the step's name and its accessible name |
| `description` | `string` | none | support copy under the name |
| `style` | `StateStyle` | — | merged last |
| `className` | `string` | — | joined last |
| `testId` | `string` | — | stable locator |
---
# Switch
Source: https://gpuixui.sudokw.com/components/switch
An immediate on/off pill with a sliding knob
## Installation [#installation]
## Usage [#usage]
```tsx
import { Switch } from "@gpuixui/primitives";
import { useState } from "react";
export const Theme = () => {
const [dark, setDark] = useState(false);
return (
);
};
```
## Composition [#composition]
```text
Switch
└── SwitchThumb
```
## API reference [#api-reference]
### `Switch` [#switch]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `checked` | `boolean` | — | controlled state; omit to let the switch keep its own |
| `defaultChecked` | `boolean` | `false` | initial state when uncontrolled |
| `onCheckedChange` | `(next: boolean) => void` | none | the next boolean, only when it changed |
| `onChange` | `(next: boolean) => void` | none | deprecated alias for `onCheckedChange`; the new name wins when both are passed |
| `children` | `ReactNode` | none | a glyph on the knob |
| `disabled` | `boolean` | `false` | refuses pointer and keys and leaves the tab order |
| `invalid` | `boolean` | `false` | an off pill takes the destructive edge |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | the pill's height from `iconSize`; the band stays 24px |
| `accent` | `"primary" \| "destructive"` | `"primary"` | the checked fill and the knob's on-colour |
| `label` | `string` | none | the accessible name; a visible `Label` does not supply it |
| `description` | `string` | none | announced help (`aria-description`); wins over a tooltip's text |
| `reducedMotion` | `boolean` | the provider's preference (`false` when none) | the knob lands on its position instead of sliding there; the slide is the timing table's `fast` level (160 ms) on its `standard` curve; the prop wins |
| `style` | `StateStyle` | — | resolved against the live state and merged last |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `testId` | `string` | — | stable locator |
| `ref` / `aria-id` / `onLayout` | host address | none | the press band is the addressed element: focus it, measure it, and name it for automation |
| `onMouseEnter` / `onMouseLeave` / `aria-description` | the tooltip trigger pair and its words | none | forwarded to the press band, so a switch can be a `Tooltip` trigger; the tip's words land on the same help channel |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the switch's own; yours runs first |
### `SwitchThumb` [#switchthumb]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | the plain filled knob | a glyph on the knob |
| `className` | `string` | — | compiled through the recipe, joined after the thumb's rules |
| `style` | `StyleDesc` | — | merged last |
---
# Table
Source: https://gpuixui.sudokw.com/components/table
A bounded grid with a pinned header and sortable columns
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Table,
TableBody,
TableCell,
TableHeader,
TableHeaderCell,
TableRow,
TableViewport,
useColumns,
useTable,
} from "@gpuixui/data";
const columns = useColumns({
columns: [
{ id: "service", label: "Service", width: 260 },
{ id: "environment", label: "Environment", width: 140 },
],
});
const table = useTable({
rows,
keyOf: (row) => row.id,
columns: [
{ id: "service", valueOf: (row) => row.service },
{ id: "environment", valueOf: (row) => row.environment },
],
hiddenColumns: columns.hidden,
});
Service
Environment
{table.rows.map((row) => (
table.press(row.id, event.modifiers)}
selected={table.isSelected(row.id)}
>
{row.service}
{row.environment}
))}
;
```
## Composition [#composition]
```text
Table
└── TableViewport
├── TableHeader
│ └── TableHeaderCell
├── TableBody
│ └── TableRow
│ └── TableCell
├── TableCaption
├── TableEmpty
└── TableSkeleton
```
## API reference [#api-reference]
### Table [#table]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `height` | `number` — a definite height; the header stays painted and the body scrolls | — |
| `children` | `ReactNode` — a `TableHeader`, an optional `TableSkeleton`, one `TableBody` | — |
| `className` | `string` — joined after the recipe, so it wins conflicts | — |
### TableViewport, TableHeader, TableBody [#tableviewport-tableheader-tablebody]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `rows` | `number` — the skeleton's row count when the body has no children | `3` |
| `height` | `number` — the body height `Table` measures and hands down | measured |
| `children` | `ReactNode` — the rows, or the header cells the part owns | — |
| `className` | `string` | — |
### TableRow [#tablerow]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `selected` | `boolean` — the chosen record's wash and label role | `false` |
| `disabled` | `boolean` — no press, no pointer cursor | `false` |
| `instanceRef` | `Ref` — the painted element, for `renderer.focusElement` | — |
| `children` | `ReactNode` — the row's cells | — |
| `className` | `string` | — |
### TableHeaderCell, TableCell [#tableheadercell-tablecell]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `width` | `number` — a fixed track; omitted, the cell shares the leftover width | — |
| `align` | `"left" \| "center" \| "right"` | `"left"` |
| `children` | `ReactNode` — the cell's text or element | — |
| `className` | `string` | — |
### TableSkeleton [#tableskeleton]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `rows` | `number` — bars, one row each | `3` |
| `columns` | `number` — bars per row | `3` |
### TableSortHeader [#tablesortheader]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `field` | `string` — the field a press reports | — |
| `label` | `string` — the visible label and the base of the accessible name | — |
| `sort` | `Option` — the primary sort, for a single-sort consumer | — |
| `order` | `SortOrder` — the whole order, primary first | `sort` alone |
| `initialDirection` | `"asc" \| "desc"` — the direction a first press starts at | `"asc"` |
| `onSort` | `(next: SortState) => void` — the single-column contract | — |
| `onSortPress` | `(field, initialDirection, modifiers) => void` — the raw press, so the controller can compose an order | — |
| `width` | `number` — a fixed track | — |
| `align` | `"left" \| "center" \| "right"` | `"left"` |
| `resize` | `TableColumnResizeProps` — the divider; needs a fixed `width` | — |
| `testId` | `string` | — |
### The sort surface `useTable` exposes [#the-sort-surface-usetable-exposes]
| Member | Type | Consequence |
| ------ | ---- | ----------- |
| `sort` | `Option` | the primary entry, for a single-sort readout or header |
| `sortOrder` | `SortOrder` | every sorted field, primary first — what the rows and the header read |
| `setSort` | `(sort: Option) => void` | replace the order with one entry, or none |
| `toggleSort` | `(field: string, additive?: boolean) => void` | the press cycle; `additive` keeps the order and adds a tie-break |
| `pressSort` | `(field, initialDirection, modifiers?) => void` | the raw press; `modifiers.shift` is what sets `additive` |
| `rows` | `ReadonlyArray` | filtered by the query, then ordered by every entry in turn |
---
# Tabs
Source: https://gpuixui.sudokw.com/components/tabs
A roving-focus tab list and the panels it switches
## Installation [#installation]
## Usage [#usage]
```tsx
import {
Tabs,
TabsContent,
TabsList,
TabsTrigger,
} from "@gpuixui/components/tabs";
Profile
Notifications
…
…
;
```
## Composition [#composition]
```text
Tabs
├── TabsList
│ └── TabsTrigger
└── TabsContent
```
## API reference [#api-reference]
### Tabs [#tabs]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `string` | uncontrolled |
| `defaultValue` | `string` | `""` — no tab selected |
| `onValueChange` | `(value: string) => void` | none |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` — a stored name the union does not carry |
| `activation` | `"automatic" \| "manual"` | `"automatic"` — a stored name the union does not carry |
| `className` | `string` | — |
| `children` | parts — `TabsList`, `TabsContent` | — |
### TabsList [#tabslist]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `label` | `string` | none |
| `variant` | `"pill" \| "line"` | `"pill"`; `"line"` draws a thin bar under the chosen tab |
| `className` | `string` | — |
| `children` | parts — `TabsTrigger` | — |
### TabsTrigger [#tabstrigger]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `value` | `string` (required) | none |
| `disabled` | `boolean` | `false` |
| `style` | `StyleDesc \| (state) => StyleDesc` | none |
| `className` | `string` | — |
---
# Tag input
Source: https://gpuixui.sudokw.com/components/tag-input
Keywords entered through a field, as removable chips
## Installation [#installation]
## Usage [#usage]
```tsx
import {
TagInput,
TagInputChip,
TagInputField,
} from "@gpuixui/components/tag-input";
import { useState } from "react";
export const Keywords = () => {
const [keywords, setKeywords] = useState>(["design"]);
return (
{keywords.map((keyword) => (
))}
);
};
```
## Composition [#composition]
```text
TagInput
├── TagInputChip
└── TagInputField
```
## API reference [#api-reference]
### `TagInput` [#taginput]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `ReadonlyArray` | — | the tokens; the caller owns and renders them, and a stored value that is not a list carries no tokens |
| `onValueChange` | `(value: ReadonlyArray) => void` | — | the list after every accepted edit, including a removal |
| `label` | `string` | — | the group's accessible name |
| `max` | `number` | none | additions past it are refused; the chips stay |
| `allowDuplicates` | `boolean` | `false` | a repeated token is accepted as its own chip |
| `disabled` | `boolean` | `false` | refuses every edit path and renders no removal control |
| `hint` | `string` | none | a quiet note at the end of the field; blank renders nothing |
| `caption` | `string` | none | a caption under the field for the caller's preset or limit; blank renders nothing |
| `children` | `PartChildren` | — | `TagInputChip` and `TagInputField`, directly inside |
| `className` | `string` | — | joined after the outer recipe, so it wins conflicts |
| `style` | `StyleDesc` | — | merged last over the recipe |
| `testId` | `string` | — | stable locator for the group |
### `TagInputField` [#taginputfield]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `placeholder` | `string` | none | painted in the `placeholder` role while the draft is empty |
| `className` | `string` | — | joined after the field recipe |
| `style` | `StateStyle` | — | resolved against `{ disabled }` and merged last |
| `testId` | `string` | — | the input's locator; the painted hint follows it |
| `onKeyDown` / `onFocus` / `onBlur` | host handlers | none | composed with the component's own; yours runs first |
| `label` / `description` / `invalid` | `string` · `string` · `boolean` | none · none · `false` | the field's name, its announced help, and the invalid edge |
| `disabled` / `readOnly` / `autoFocus` | `boolean` | `false` | inherited from the root's `disabled`, an uneditable field, and focus on mount |
| `size` / `fullWidth` / `tabIndex` | `"sm" \| "md" \| "lg"` · `boolean` · `number` | `"md"` · `false` · the field's place | the input's height, its width, and its tab stop |
| `ref` / `aria-id` / `onLayout` | the addressed element | none | the draft input: focus it, measure it, name it for automation |
| `onClick` / `onKeyUp` | host handlers | none | composed with the field's own; yours runs first |
| `onValueChange` / `defaultValue` | the field surface it inherits — see `Input` | none | the draft the field keeps before the tag is committed |
### `TagInputChip` [#taginputchip]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | — | the token this chip removes; also its key |
| `className` | `string` | — | joined after the chip recipe |
| `style` | `StateStyle` | — | resolved against `{ disabled }` and merged last |
| `testId` | `string` | none | the removal control is addressable as `-remove` |
---
# Theme toggler
Source: https://gpuixui.sudokw.com/components/theme-toggler
The control that switches light and dark
## Installation [#installation]
```bash
nub add @gpuixui/components
```
## Usage [#usage]
```tsx
import { useState } from "react";
import {
ThemeProvider,
ThemeToggler,
darkTheme,
lightTheme,
} from "@gpuixui/components";
const [mode, setMode] = useState<"light" | "dark">("dark");
;
```
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `mode` | `"light" \| "dark"` | — |
| `onChange` | `(mode: "light" \| "dark") => void` | — |
| `lightLabel` | `string` | `"Switch to light theme"` |
| `darkLabel` | `string` | `"Switch to dark theme"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |
| `level` | `SurfaceLevel` | `"slow"` |
| `className` | `string` | — |
| `style` | `StateStyle` | — |
---
# Time picker
Source: https://gpuixui.sudokw.com/components/time-picker
A time of day, entered as an HH:mm field
## Installation [#installation]
## Usage [#usage]
```tsx
import {
TimePicker,
TimePickerClear,
TimePickerSegment,
TimePickerSeparator,
} from "@gpuixui/components/time-picker";
import { Button } from "@gpuixui/primitives";
import { useState } from "react";
export const DeploymentWindow = () => {
const [opens, setOpens] = useState("09:30");
const [saved, setSaved] = useState(null);
return (
<>
{
setOpens(next);
setSaved(null);
}}
value={opens}
>
{/* `opens` is `HH:mm` or `null`. */}
setSaved(opens)}>
{saved === null ? "Save window" : `Saved ${saved}`}
>
);
};
```
## Composition [#composition]
```text
TimePicker
├── TimePickerSegment
├── TimePickerSeparator
└── TimePickerClear
```
## API reference [#api-reference]
### `TimePicker` [#timepicker]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string \| null` | — | the controlled `HH:mm`; `undefined` makes the field uncontrolled |
| `defaultValue` | `string \| null` | `null` | the opening time when uncontrolled |
| `onValueChange` | `(value: string \| null) => void` | none | every accepted edit, including the clear path |
| `label` | `string` | `"Time"` | the group's accessible name, and the prefix of each segment's name |
| `minuteStep` | `number` | `1` | the arrow-key step, normalised to an integer in `1..59` |
| `clearable` | `boolean` | `false` | renders the clear control while a time is set |
| `disabled` | `boolean` | `false` | both segments become `readOnly`, leave the tab order, and no clear is rendered |
| `testId` | `string` | `"time-picker"` | the base id of the parts: `-hours`, `-minutes`, `-clear` |
| `className` | `string` | — | joined last |
| `children` | `ReactNode` | none | the segments, separator and clear control, directly inside |
### `TimePickerSegment` [#timepickersegment]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `segment` | `"hours" \| "minutes"` | required | which half this input edits, and the maximum it clamps to |
| `style` | `StateStyle` | the recipe | a style, or a function of `{ disabled, focused }` |
### `TimePickerSeparator` [#timepickerseparator]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `children` | `ReactNode` | `":"` | the painted separator |
### `TimePickerClear` [#timepickerclear]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | `"Clear time"` | the control's accessible name |
---
# Timeline
Source: https://gpuixui.sudokw.com/components/timeline
A read-only rail of events the app marks
## Installation [#installation]
## Usage [#usage]
```tsx
import { Timeline, TimelineItem } from "@gpuixui/components";
;
```
## Composition [#composition]
```text
Timeline
└── TimelineItem
```
## API reference [#api-reference]
### `Timeline` [#timeline]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | none | the list's accessible name (`aria-label`) |
| `children` | `ReactNode` | — | `TimelineItem` parts, passed directly |
| `style` | `StyleDesc` | — | merged last |
| `className` | `string` | — | compiled through `@gpuixui/tw`, joined after the recipe |
| `testId` | `string` | — | stable locator |
### `TimelineItem` [#timelineitem]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `title` | `string` | — | the event's name and its accessible name |
| `description` | `string` | none | one line of support copy under the title |
| `timestamp` | `string` | none | when it happened, as the consumer formats it |
| `status` | `"complete" \| "current" \| "upcoming"` | `"complete"` | the dot's fill, and whether the title is muted |
| `style` | `StateStyle` | — | resolved against `{ status, last }`, merged last |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `testId` | `string` | — | stable locator |
---
# Toast
Source: https://gpuixui.sudokw.com/components/toast
A stacked, auto-dismissing notification queue
## Installation [#installation]
## Usage [#usage]
```tsx
import { Duration } from "effect";
import { Button } from "@gpuixui/primitives";
import {
ToastProvider,
ToastViewport,
useToast,
} from "@gpuixui/components/toast";
const PublishBar = () => {
const toast = useToast();
return (
toast.push(
{
title: "Changes published",
description: "The site rebuilds in about a minute.",
tone: "success",
action: { label: "Undo" },
duration: Duration.seconds(8),
},
{ onAction: () => restore() },
)
}
>
Publish changes
);
};
export const App = () => (
{/* Render the viewport last. */}
);
```
```tsx
const rows = await toast.promise(importRows(file), {
loading: { title: "Importing rows", description: "Reading the file…" },
success: (count) => ({ title: `Imported ${count} rows`, tone: "success" }),
error: (cause) => ({ title: "Import failed", description: String(cause), tone: "danger" }),
});
```
## Composition [#composition]
```text
ToastProvider
└── ToastViewport
```
## API reference [#api-reference]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `ToastProvider` | `max` | `number` — toasts kept before the oldest is dropped | `3` |
| `ToastProvider` | `defaultDuration` | `Duration.Input` | 5 seconds |
| `ToastProvider` | `children` | the tree that may push | — |
| `ToastViewport` | `swipe` | `boolean` — sideways drag to dismiss | `true` |
| `ToastViewport` | `children` | an optional control before the stack | — |
| `ToastViewport` | `className` | `string` — joined after the recipe | — |
| `useToast` | `toasts` | `ReadonlyArray` — oldest first | — |
| `useToast` | `push` | `(draft, options?) => void` — `title` required; `description`, `tone`, `duration`, `action`, `phase` optional | — |
| `useToast` | `promise` | `(p, { loading, success, error }) => Promise` — settles one row | — |
| `useToast` | `dismiss` | `(id: string) => void` | — |
| `useToast` | `dismissAll` | `() => void` | — |
| `useToast` | `pause` | `(id: string) => void` — stop one clock | — |
| `useToast` | `resume` | `(id: string) => void` — restart with the remaining time | — |
| `useToast` | `hold` | `(held: boolean) => void` — the stack's clock | — |
| `useToast` | `actionFor` | `(id: string) => Option<() => void>` — the queued handler | — |
| `ToastInput` | `title` | `string` — non-empty; empty input is dropped | required |
| `ToastInput` | `description` | `string` | — |
| `ToastInput` | `tone` | `"info" \| "success" \| "warning" \| "danger"` | `"info"` |
| `ToastInput` | `phase` | `"loading" \| "settled"` — a loading row has no clock | `"settled"` |
| `ToastInput` | `duration` | `Duration.Input` — `0` keeps it until dismissed | the provider's default |
| `ToastInput` | `action` | `{ label }` — the handler travels beside the draft | — |
---
# Toggle
Source: https://gpuixui.sudokw.com/components/toggle
A button that stays on, for a mode the next action uses
## Usage [#usage]
```tsx
import { Toggle } from "@gpuixui/components/toggle";
Bold
;
// An icon-only toggle still needs a name.
} />;
```
A toggle is not a `Switch`: a switch changes a setting immediately, while a
toggle sets a mode for the next action ("bold", "grid view") and lives in a
toolbar beside other actions.
## Keyboard [#keyboard]
`Enter` activates on the key press and `Space` on the release, the platform's
button contract; a held key does not repeat the activation.
## API [#api]
| Prop | Type | Default |
| ---- | ---- | ------- |
| `pressed` | `boolean` | — (uncontrolled) |
| `defaultPressed` | `boolean` | `false` |
| `onPressedChange` | `(pressed: boolean) => void` | — |
| `variant` | `"default" \| "outline"` | `"default"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` |
| `square` | `boolean` | `false` |
| `disabled` | `boolean` | `false` |
| `loading` | `boolean` | `false` |
| `loadingLabel` | `string` | the provider's `loading.busy` |
| `reducedMotion` | `boolean` | the motion provider |
| `leading` / `trailing` | `ReactNode` | — |
| `label` | `string` | the text children |
| `description` | `string` | — |
| `style` | `StateStyle` | — |
| `className` | `string` | — |
| `testId` | `string` | — |
| `ref` / `aria-id` / `onLayout` | host props | — |
---
# Toggle group
Source: https://gpuixui.sudokw.com/components/toggle-group
A toolbar of related modes with one tab stop
## Usage [#usage]
```tsx
import {
ToggleGroup,
ToggleGroupItem,
} from "@gpuixui/components/toggle-group";
Left
Centre
;
Bold
Italic
;
```
`type="single"` keeps at most one mode on and clears it when the live option is
pressed again; `type="multiple"` lets any number stay on. The group is one tab
stop: the arrow keys move focus between options without pressing them.
## Keyboard [#keyboard]
| Key | Effect |
| --- | ------ |
| `ArrowRight` / `ArrowLeft` | move focus along a horizontal group |
| `ArrowDown` / `ArrowUp` | move focus along a vertical group |
| `Home` / `End` | the first or last enabled option |
| `Enter` / `Space` | press the focused option |
## API [#api]
| Part | Prop | Type | Default |
| ---- | ---- | ---- | ------- |
| `ToggleGroup` | `type` | `"single" \| "multiple"` | `"multiple"` |
| `ToggleGroup` | `value` | `string` for single, `readonly string[]` for multiple | — |
| `ToggleGroup` | `defaultValue` | the same shape as `value` | — |
| `ToggleGroup` | `onValueChange` | `(next: string) => void` or `(next: readonly string[]) => void` | — |
| `ToggleGroup` | `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` |
| `ToggleGroup` | `variant` | `"default" \| "outline"` | `"default"` |
| `ToggleGroup` | `size` | `"sm" \| "md" \| "lg"` | `"md"` |
| `ToggleGroup` | `square` | `boolean` | `false` |
| `ToggleGroup` | `disabled` | `boolean` | `false` |
| `ToggleGroup` | `label` | `string` | — |
| `ToggleGroup` | `description` | `string` | — |
| `ToggleGroup` | `options` / `children` | option data, or the item elements | — |
| `ToggleGroup` | `style` / `itemStyle` | `StateStyle<…>` | — |
| `ToggleGroup` | `className` | `string` | — |
| `ToggleGroup` | `testId` | `string` | — |
| `ToggleGroup` | `itemTestId` | `(value: string) => string` | — |
| `ToggleGroup` | `ref` / `aria-id` / `onLayout` | host props (on the tab stop) | — |
| `ToggleGroupItem` | `value` | `string` (required) | — |
| `ToggleGroupItem` | `label` | `string` (required) | — |
| `ToggleGroupItem` | `description` | `string` | — |
| `ToggleGroupItem` | `disabled` | `boolean` | `false` |
| `ToggleGroupItem` | `children` | `ReactNode` | `label` |
---
# Tooltip
Source: https://gpuixui.sudokw.com/components/tooltip
A short label on hover
## Installation [#installation]
## Usage [#usage]
```tsx
import { Icon } from "@gpuixui/icons";
import { IconButton, Tooltip, useTheme } from "@gpuixui/primitives";
const theme = useTheme();
export const RevenueHelp = () => (
}
label="Revenue definition"
/>
);
```
## Composition [#composition]
```text
Tooltip
├── trigger
└── tip
```
## API reference [#api-reference]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `content` | `ReactNode` | — (required) | the label; keep it short, the runtime does not wrap overlay content |
| `description` | `string` | `content` when it is a string | the trigger's accessible description; required for a non-text tip |
| `children` | `ReactNode` | — (required) | the trigger: a focusable host element, or a control that forwards the trigger pair |
| `side` | `FloatingSide` | `"top"` | the **preferred** side; the runtime moves the layer when it would leave the window |
| `align` | `FloatingAlign` | `"center"` | alignment against the trigger |
| `delay` | `number` | the provider's `0` | the hover delay in milliseconds |
| `open` | `boolean` | the runtime's state | takes the open state; the tip paints only while it is `true` |
| `defaultOpen` | `boolean` | `false` | starts open before any interaction; ignored while `open` is set |
| `onOpenChange` | `(open: boolean) => void` | — | every state the runtime asked for, controlled or not |
| `disableHoverableContent` | `boolean` | `false` | close as soon as the pointer leaves the trigger, instead of letting it cross onto the tip |
| `reducedMotion` | `boolean` | the provider's preference (`false` when none) | removes the journey — the tip lands at once and a close unmounts at once instead of holding the exit; the prop wins |
| `className` | `string` | — | joined after the tip's recipe, so it wins conflicts |
| `style` | `StyleDesc` | — | merged after the recipe **and** `className`, so it is the last word |
| `testId` | `string` | — | lands on the tip's surface |
`TooltipProvider` sets one timing for every tip below it.
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `delay` | `number` | the runtime's `0` | milliseconds before a tip opens; a tip's own `delay` wins |
| `skipDelay` | `number` | the runtime's `300` | the window after a close in which the next tip opens with no wait |
| `disableHoverableContent` | `boolean` | the runtime's `false` | close as soon as the pointer leaves the trigger, for every tip below |
| `children` | `ReactNode` | — (required) | the tree the provider covers |
---
# User menu
Source: https://gpuixui.sudokw.com/components/user-menu
An avatar and name that open the account actions
## Usage [#usage]
```tsx
import { UserMenu, SidebarUserMenu } from "@gpuixui/components";
const user = { name: "Benji", email: "benji@example.com" };
undefined}
onAccount={() => undefined}
onBilling={() => undefined}
onNotifications={() => undefined}
onLogOut={() => undefined}
/>;
```
```tsx
;
```
## API [#api]
| Part | Renders | Props |
| ---- | ------- | ----- |
| `UserMenu` | the trigger and its account menu | `user`, `onUpgrade?`, `onAccount?`, `onBilling?`, `onNotifications?`, `onLogOut?`, `side?`, `align?`, `collapsed?`, `className?`, `testId?`, `menuTestId?` |
| `SidebarUserMenu` | `UserMenu` in `SidebarMenu` / `SidebarMenuItem` | the same, without `collapsed` (the sidebar supplies it) |
`user` is `{ name: string; email: string; avatar?: string }`. Without `avatar`
the avatar paints the initials.
---
# Primitives
Source: https://gpuixui.sudokw.com/primitives
The Tier 1 layer over GPUix host elements
Five primitives live under [Components](/components):
[Button](/components/button), [Checkbox](/components/checkbox),
[Select](/components/select), [Switch](/components/switch) and
[Tooltip](/components/tooltip).
## Layout [#layout]
| Primitive | What it is |
| --------- | ---------- |
| [Box](/primitives/box) | The one container — a semantic surface and flex facts |
| [Stack](/primitives/stack) | Children along one axis, spaced by the theme scale |
| [Grid](/primitives/grid) | Equal tracks in two dimensions |
| [Separator](/primitives/separator) | One theme-coloured rule, with an optional caption |
| [Spacer](/primitives/spacer) | A fixed step, or a flexible filler |
| [ScrollArea](/primitives/scroll-area) | A bounded container that scrolls |
## Type and icons [#type-and-icons]
| Primitive | What it is |
| --------- | ---------- |
| [Text](/primitives/text) | One run of type, named by a theme role and a tone |
| [Label](/primitives/label) | A control's name, required marker and description |
| [Icons](/graphics/icons/icon) | 83 monochrome icons and their size scale |
## Actions [#actions]
| Primitive | What it is |
| --------- | ---------- |
| [Button](/components/button) | The primary action, with busy and disabled states |
| [Icon button](/primitives/icon-button) | A square, icon-only action with a required name |
| [Button group](/primitives/button-group) | A row of related actions |
| [Toggle](/primitives/toggle) | A button with a persistent on/off state |
| [Toggle group](/primitives/toggle-group) | Related modes in one toolbar |
## Text and numeric inputs [#text-and-numeric-inputs]
| Primitive | What it is |
| --------- | ---------- |
| [Input](/primitives/input) | A single-line text field |
| [Text area](/primitives/textarea) | A multi-line text field sized in rows |
| [Number field](/primitives/number-field) | A number field with steps and optional bounds |
| [OTP field](/primitives/otp-field) | A one-time code in length cells |
## Choice [#choice]
| Primitive | What it is |
| --------- | ---------- |
| [Select](/components/select) | One choice from a list |
| [Checkbox](/components/checkbox) | An independent yes/no, optionally indeterminate |
| [Checkbox group](/primitives/checkbox-group) | Several choices sharing one tab stop |
| [Radio group](/primitives/radio-group) | One choice from a small set |
| [Switch](/components/switch) | An immediate on/off |
| [Slider](/primitives/slider) | A value on a track |
| [Range slider](/primitives/range-slider) | The two ends of one interval |
| [Rating](/primitives/rating) | A whole number of stars, or none |
## Value and progress [#value-and-progress]
| Primitive | What it is |
| --------- | ---------- |
| [Progress](/primitives/progress) | A determinate or indeterminate progress bar |
| [Meter](/primitives/meter) | A measurement inside a range |
## Feedback [#feedback]
| Primitive | What it is |
| --------- | ---------- |
| [Skeleton](/primitives/skeleton) | A pulsing placeholder |
| [Spinner](/primitives/spinner) | Three dots, because the runtime cannot rotate |
| [Tooltip](/components/tooltip) | A short label on hover or keyboard focus |
| [Theme provider](/primitives/theme-provider) | Supplies the theme every primitive draws with |
## The icon set [#the-icon-set]
The glyphs live with the rest of the drawings, under
[Icons](/graphics/icons/icon): every icon the package ships, drawn from its own
markup.
---
# Box
Source: https://gpuixui.sudokw.com/primitives/box
A surface with tone, padding, radius, border and flex facts
## Usage [#usage]
```tsx
import { Box, Text } from "@gpuixui/primitives";
A surface with the card role.
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `tone` | `SurfaceTone` | required | the semantic background |
| `padding` | spacing step | none | padding on all four sides |
| `paddingX` / `paddingY` | spacing step | none | per-axis padding, overriding `padding` there |
| `radius` | `none \| sm \| md \| lg \| xl \| full` | none | corner radius; a stored name this release does not carry paints no corner |
| `border` | `boolean` | `false` | one thin border on the `border` role |
| `borderStyle` | `"solid" \| "dashed"` | `"solid"` | the edge style; no effect without `border` |
| `aspectRatio` | `number` | none | width ÷ height; a non-finite, zero or negative ratio is dropped |
| `elevation` | `sm \| md \| lg \| overlay` | none | the theme's shadow at that level; a stored level name this release does not carry paints `md`, the default lift, and the reserved `overlay` paints none |
| `grow` / `shrink` | `boolean` | `false` | flex growth, or allow shrinking below content |
| `fullWidth` / `fullHeight` | `boolean` | `false` | `100%` on that axis |
| `ref` | `Ref` | — | the painted element, for a measurement, a scroll or a focus call |
| `aria-id` | `string` | — | the element's author id, exposed as the platform's automation id |
| `onLayout` | `(event: EventPayload) => void` | — | the painted box after the first paint and each resize |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last, so it wins property by property |
| `children` | `ReactNode` | — | content |
| `testId` | `string` | — | stable locator |
---
# ButtonGroup
Source: https://gpuixui.sudokw.com/primitives/button-group
A row or column of related actions, attached or separated
## Usage [#usage]
```tsx
import { Button, ButtonGroup } from "@gpuixui/primitives";
Left
Center
Right
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `mode` | `"attached" \| "separated"` | `"attached"` | zero gap + clipped group, or a spaced group; a stored name this release does not carry reads as the default (`buttonGroupModeOf`) |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | a row, or a column with one shared width; a stored name reads as the default (`buttonGroupOrientationOf`) |
| `gap` | spacing step, or `null` | `"sm"` | the gap in separated mode; a step the theme does not carry — including the `null` JSON writes for a field a record never set — reads as the documented default (`spaceStep`) |
| `label` | `string` | none | the group's accessible name, written as `aria-label` |
| `ref` | `Ref` | — | the group's own wrapper, the element that clips the two ends — not an action; press one through its `testId` |
| `aria-id` | `string` | — | the wrapper's author id, exposed as the platform's automation id |
| `onLayout` | `(event: EventPayload) => void` | — | the wrapper's painted box after the first paint and each resize |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `testId` | `string` | — | stable locator for the wrapper |
| `children` | `ReactNode` | — | the buttons |
---
# Checkbox group
Source: https://gpuixui.sudokw.com/primitives/checkbox-group
Several independent choices sharing one tab stop
## Usage [#usage]
```tsx
import { CheckboxGroup } from "@gpuixui/primitives";
;
```
## API [#api]
### `CheckboxGroup` [#checkboxgroup]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `options` | `ReadonlyArray` | — | the choices as data; omit with item children |
| `children` | `ReactNode` | — | `CheckboxGroupItem` elements, for rich content |
| `value` | `ReadonlyArray` | — | controlled values |
| `defaultValue` | `ReadonlyArray` | `[]` | the initial values when uncontrolled |
| `onValueChange` | `(next: ReadonlyArray) => void` | none | the on values |
| `onChange` | `(next: ReadonlyArray) => void` | none | deprecated alias for `onValueChange` |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | which arrow pair moves focus; a name this release does not carry reads as `"vertical"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | box side from `iconSize` |
| `accent` | `"primary" \| "destructive"` | `"primary"` | the fill and mark colour; a stored name the table does not carry reads as `primary` |
| `disabled` | `boolean` | `false` | disables every option |
| `invalid` | `boolean` | `false` | the empty box's border and every row's focus ring take the destructive role |
| `label` | `string` | none | the group's accessible name |
| `description` | `string` | none | announced help on the group node |
| `ref` / `aria-id` / `onLayout` | host props | none | land on the row that holds the tab stop |
| `style` | `StateStyle` | — | resolved against the live state, merged last |
| `itemStyle` | `StateStyle` | — | resolved per option row, merged last |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the group's own; yours runs first |
| `className` | `string` | — | compiled after the recipe, so it wins conflicts |
| `testId` | `string` | — | stable locator for the group |
| `itemTestId` | `(value: string) => string` | — | per-row locator, derived from its value |
### `CheckboxGroupItem` [#checkboxgroupitem]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | — | the value this option adds |
| `label` | `string` | — | accessible name, and the visible text unless `children` replaces it |
| `description` | `string` | none | a caption beside the name, announced as the row's help (`aria-description`); a blank one paints and announces nothing |
| `disabled` | `boolean` | `false` | the option cannot be chosen and takes no focus |
| `children` | `ReactNode` | — | rich content drawn instead of the name pair |
---
# Grid
Source: https://gpuixui.sudokw.com/primitives/grid
Equal tracks in two dimensions
## Usage [#usage]
```tsx
import { Grid, Text } from "@gpuixui/primitives";
Width
1280 px
Height
800 px
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `columns` | `number` | required | equal column tracks; floored, minimum 1 |
| `rows` | `number` | auto | equal fixed rows; floored, minimum 1 |
| `gap` | spacing step | none | both axes, from `spacing` |
| `grow` | `boolean` | `false` | takes leftover space on the parent's axis |
| `ref` | `Ref` | — | the painted element, for a measurement, a scroll or a focus call |
| `aria-id` | `string` | — | the element's author id, exposed as the platform's automation id |
| `onLayout` | `(event: EventPayload) => void` | — | the painted box after the first paint and each resize |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `children` | `ReactNode` | — | cells, in source order |
| `testId` | `string` | — | stable locator |
---
# IconButton
Source: https://gpuixui.sudokw.com/primitives/icon-button
A square, icon-only action with a required name
## Usage [#usage]
```tsx
import { IconButton, Stack, useTheme } from "@gpuixui/primitives";
import { Icon } from "@gpuixui/icons";
const theme = useTheme();
}
variant="destructive"
onActivate={removeLayer}
/>
}
onActivate={openSettings}
/>
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `label` | `string` | required | the accessible name; never derived from the glyph |
| `icon` | `ReactNode` | required | the glyph painted inside the button |
| `variant` | `ButtonVariant` | `"ghost"` | the paint from `core/variants.ts` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | square side from `density.controlHeight` |
| `disabled` | `boolean` | `false` | blocks activation; out of the Tab order (`tabIndex: -1`) |
| `loading` | `boolean` | `false` | spinner, busy description, blocks activation |
| `loadingLabel` | `string` | `loading.busy` (`"Loading"`) | the busy word, announced as the description and the spinner's name; a blank one silences both |
| `reducedMotion` | `boolean` | the motion provider | holds the busy spinner's lit dot; the prop overrides the provider |
| `onActivate` | `(event: EventPayload) => void` | none | click or activating key; not called while busy |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the button's own; a click is observed first, an activating key acts first |
| `onMouseEnter` / `onMouseLeave` / `aria-description` | `TooltipTriggerProps` | none | forwarded to the press target, so the control can be a `Tooltip` trigger |
| `ref` | `Ref` | — | the element itself |
| `aria-expanded` / `aria-selected` | `boolean` | — | announced state, forwarded through `ActionHostProps` |
| `tabIndex` | `number` | `0`, or `-1` while `disabled` | the action's Tab stop; see `Button`'s row — a leaving surface names `-1` for the close action it keeps painted |
| `className` | `string` | — | a Tailwind string joined after the recipe |
| `style` | `StateStyle` | — | resolved against `{ disabled, focused }`, merged last |
| `testId` | `string` | — | stable locator |
---
# Input
Source: https://gpuixui.sudokw.com/primitives/input
A single-line text field, controlled or uncontrolled
## Usage [#usage]
```tsx
import { Input, Label, labelledBy } from "@gpuixui/primitives";
const email = "Contact email";
const help = "Where notifications are delivered.";
{email}
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `string` | — | controlled text |
| `defaultValue` | `string` | `""` | initial text when uncontrolled |
| `onValueChange` | `(value: string) => void` | none | called after each edit that changed the value |
| `onChange` | `(value: string) => void` | none | deprecated alias for `onValueChange` |
| `onSubmit` | `(event) => void` | none | Enter; the raw runtime event |
| `placeholder` | `string` | none | empty-state text: painted in the `placeholder` role and announced as the field's description while the box is empty (the host's own hint is a fixed grey no theme reaches); a blank one paints nothing and announces nothing |
| `readOnly` | `boolean` | `false` | accepts focus and selection, refuses edits |
| `disabled` | `boolean` | `false` | read-only, out of the tab order, de-emphasised |
| `autoFocus` | `boolean` | `false` | takes focus on mount |
| `invalid` | `boolean` | `false` | border and focus ring take the destructive role |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | height and padding from density |
| `fullWidth` | `boolean` | `false` | `width: 100%` |
| `label` | `string` | none | accessible name; a visible `Label` does not name the field on its own |
| `description` | `string` | none | announced help after the name (`aria-description`); a caller's permanent help wins over the hint |
| `tabIndex` | `number` | none | the field's place in the tab order |
| `ref` / `aria-id` / `onLayout` | host props | none | the native ` ` is the addressed element |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the field's own; yours runs first |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | resolved against the live state, merged last |
| `testId` | `string` | — | stable locator |
---
# Label
Source: https://gpuixui.sudokw.com/primitives/label
A control's visible name, with a required marker
## Usage [#usage]
```tsx
import { useRef } from "react";
import type { PublicInstance } from "@gpuix/react";
import { Input, Label, labelledBy } from "@gpuixui/primitives";
const name = "Workspace name";
const help = "Shown on invitations and in the app switcher.";
export const NameField = () => {
const ref = useRef(null);
return (
<>
{name}
>
);
};
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `target` | `RefObject` | none | a press on the label focuses this control |
| `disabled` | `boolean` | `false` | drops that press, for a control that is disabled |
| `required` | `boolean` | `false` | appends a destructive `*`; visual only |
| `invalid` | `boolean` | `false` | paints the name in the destructive role |
| `description` | `string` | none | a caption line under the name; a blank one paints nothing, the rule a blank name keeps |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `testId` | `string` | — | stable locator |
| `children` | `ReactNode` | — | the name |
---
# Meter
Source: https://gpuixui.sudokw.com/primitives/meter
A measurement inside a range
## Usage [#usage]
```tsx
import { Meter, Stack, Text } from "@gpuixui/primitives";
Disk usage · 68 of 100 GB
`${v} GB`} />
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | required | the measurement; clamped into the range |
| `min` / `max` | `number` | `0` / `100` | the range the fill is a fraction of; a stored bound that is not a number is that default |
| `tone` | `primary \| success \| destructive \| muted` | `"primary"` | the fill's colour role; a stored name the table does not carry reads as `primary` |
| `size` | `"sm" \| "md"` | `"md"` | track height from `spacing.xs` / `sm`; a stored name the table does not carry reads as `md` |
| `label` | `string` | none | accessible name, and `MeterLabel`'s default text; a blank label is no name, and a caption child that says nothing falls back to it rather than painting a blank line |
| `formatValue` | `(value: number) => string` | `String` | the announced and displayed measurement; a blank answer keeps the number |
| `children` | `ReactNode` | the track | the parts to draw |
| `style` | `StyleDesc` | — | merged last, on the root frame |
| `className` | `string` | — | Tailwind classes, joined last on the root frame |
| `ref` / `aria-id` / `onLayout` | host props | none | the root frame itself: the box a layout measures, the instance a caller holds, and the author id an automation host addresses |
| `testId` | `string` | — | stable locator, on the root |
---
# NumberField
Source: https://gpuixui.sudokw.com/primitives/number-field
A number in a text field, with steps and optional bounds
## Usage [#usage]
```tsx
import { NumberField, Label, Stack } from "@gpuixui/primitives";
import { useState } from "react";
const [copies, setCopies] = useState(1);
Copies
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | controlled value |
| `defaultValue` | `number` | none | start value; the field shows empty without one |
| `onValueChange` | `(value: number) => void` | — | the clamped number, after a valid edit or step |
| `onValueCommit` | `(value: number) => void` | — | the value a gesture ended on: once per stepper press, once per scrub release, once per arrow or page key that moved it, and once when a typed edit ends on a readable value |
| `onChange` | `(value: number) => void` | — | deprecated alias for `onValueChange` |
| `min` / `max` | `number` | none | bound the value; absent means unbounded, and an inverted pair is the range it means |
| `step` | `number` | `1` | arrow step |
| `precision` | `number` | the value's own decimals | decimals painted on commit; it rounds and pads the text, and the value keeps its own. A stored value that is not a number (a record's `null`, text) is no precision at all, so the field paints the value's own decimals rather than rounding it to a whole number |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | field height and padding |
| `disabled` | `boolean` | `false` | read-only, out of the tab order, inert |
| `invalid` | `boolean` | `false` | destructive border |
| `label` | `string` | none | accessible name |
| `onSubmit` | `(event: EventPayload) => void` | — | Enter: the draft commits first, then this reports |
| `description` | `string` | none | announced help on the input (`aria-description`); a caller's permanent help wins over the hint |
| `placeholder` | `string` | none | empty-field hint, painted in the `placeholder` role and announced as the field's description while the box is empty (the host's own hint is a fixed grey no theme reaches) |
| `autoFocus` | `boolean` | `false` | focus on mount |
| `formatValue` | `(value: number) => string` | fixed decimals | the display text, display-only; pair it with `parseValue`. An answer that says nothing (`""` or only spaces — a locale table's missing row) is no answer, so the field paints the value it holds instead of an empty box |
| `parseValue` | `(text: string) => Option` | none | reads the text `formatValue` painted; tried first, then decimal |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | resolved against the live state, merged last |
| `children` | `ReactNode` | input + steppers | the parts to draw |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the field's own; a disabled field keeps them observable |
| `ref` / `aria-id` / `onLayout` | host props | none | the native ` ` is the addressed element |
| `testId` | `string` | — | stable locator on the field's chrome |
### `NumberFieldScrubHandle` [#numberfieldscrubhandle]
The label an inspector drags. Mount it beside a `NumberField` and give both the
same value and bounds: a horizontal drag moves one step per 4px, a held Shift
multiplies that by 10, and `min`/`max` clamp it. A press that does not travel is a
click, so the field beside it stays a text field.
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | the value a drag starts from |
| `min` / `max` | `number` | none | the bounds a drag clamps to |
| `step` | `number` | `1` | the step one scrub count moves |
| `onValueChange` | `(value: number) => void` | — | every frame of a drag, already clamped |
| `onValueCommit` | `(value: number) => void` | — | the value the whole drag ended on |
| `disabled` | `boolean` | `false` | takes no gesture, and paints no resize cursor |
| `children` | `ReactNode` | — | the visible words a reader drags |
| `style` / `className` / `testId` | host props | — | merged after the handle's own |
---
# OTPField
Source: https://gpuixui.sudokw.com/primitives/otp-field
A one-time code in length cells, held as one string
## Usage [#usage]
```tsx
import { OTPField, Label, Stack } from "@gpuixui/primitives";
import { useState } from "react";
const [code, setCode] = useState("");
Verification code
verify(complete)}
/>
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `length` | `number` | `6` | number of cells; a stored value that is not a number is six, and a number that cannot be laid out resolves to one |
| `value` | `string` | — | controlled code, normalised to letters/digits; a stored value that is not text is no code |
| `defaultValue` | `string` | — | initial code when uncontrolled; a stored value that is not text is no code |
| `onValueChange` | `(value: string) => void` | — | the code after every edit that changed it |
| `onChange` | `(value: string) => void` | — | deprecated alias for `onValueChange` |
| `onComplete` | `(value: string) => void` | — | fires when an edit makes the code full |
| `onSubmit` | `(event: EventPayload) => void` | — | Enter in any cell, with the code unchanged |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | cell height and width, from the density scale |
| `disabled` | `boolean` | `false` | read-only, out of the tab order, inert |
| `invalid` | `boolean` | `false` | destructive border on every cell |
| `label` | `string` | none | group name; each cell is named ` digit N` |
| `description` | `string` | none | announced help, carried by every cell (`aria-description`) |
| `autoFocus` | `boolean` | `false` | focuses the first cell on mount |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | on the group; resolved against the live state, merged last |
| `children` | `ReactNode` | `length` cells | the cell parts to draw |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with each cell's own, on every cell |
| `ref` / `aria-id` / `onLayout` | host props | none | land on the first cell; the code belongs to the group |
| `testId` | `string` | — | the group's locator; cells are `-cell-N` |
---
# Progress
Source: https://gpuixui.sudokw.com/primitives/progress
A determinate or indeterminate progress bar
## Usage [#usage]
```tsx
import { Progress, Stack, Text } from "@gpuixui/primitives";
Uploading · 42%
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | 0–100 determinate fill; omit for indeterminate, and a stored `null` — or any value that is not a number — reads as omitted |
| `buffer` | `number` | none | 0–100 held-ahead point; paints the segment between `value` and itself, and a stored value that is not a number paints no segment |
| `tone` | `primary \| success \| destructive \| muted` | `"primary"` | the fill's colour role; a stored name the table does not carry reads as `primary` |
| `size` | `"sm" \| "md"` | `"md"` | track height from `spacing.xs`/`sm`; a stored name the table does not carry reads as `md` |
| `label` | `string` | none | accessible name, and `ProgressLabel`'s default text; a blank label is no name, and a caption child that says nothing falls back to it rather than painting a blank line |
| `formatValue` | `(percent: number) => string` | `` `${percent}%` `` | the announced and painted share; a blank answer keeps the default |
| `loadingText` | `string` | `loading.indeterminate` (`"loading"`) | what an indeterminate bar says instead of a percentage; a blank one keeps the provider's |
| `reducedMotion` | `boolean` | the motion provider | holds the indeterminate segment; no effect with `value`. The prop overrides the provider |
| `children` | `ReactNode` | the track | the parts to draw |
| `style` | `StyleDesc` | — | merged last, on the root frame |
| `className` | `string` | — | Tailwind classes, joined last on the root frame |
| `ref` / `aria-id` / `onLayout` | host props | none | the root frame itself: the box a layout measures, the instance a caller holds, and the author id an automation host addresses |
| `testId` | `string` | — | stable locator, on the root |
A widget record kept in JSON stores a share it never learned as `null` rather than `undefined`. That reads as **absent**, like an omitted prop: the bar paints the moving segment and says the loading word, while `0` remains the empty share it names. A share that is not a number at all is the same absence — text a store wrote, or a field a decoder could not read — because the library does not turn text into a number here (`parseNumberText` is the explicit seam that does). Read as a measurement it would paint `width: "NaN%"` and announce `"NaN%"`: a bar with no fill that still claims a value.
---
# RadioGroup
Source: https://gpuixui.sudokw.com/primitives/radio-group
One choice from a small set, with arrow navigation
## Usage [#usage]
```tsx
import { RadioGroup } from "@gpuixui/primitives";
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `options` | `ReadonlyArray` | — | the choices as data (`{ value, label, description? }`); a description paints a caption beside the name and is announced as the row's help |
| `children` | `ReactNode` | — | `` elements, for rich content |
| `value` | `string` | — | controlled selection; the arrows and the single tab stop continue from the row that holds focus, so an owner that has not applied the choice yet cannot stall navigation |
| `defaultValue` | `string` | `""` | initial selection when uncontrolled |
| `onValueChange` | `(value: string) => void` | none | the selected item's value |
| `onChange` | `(value: string) => void` | none | deprecated alias for `onValueChange` |
| `orientation` | `"vertical" \| "horizontal"` | `"vertical"` | layout and the arrow axis; a name this release does not carry reads as `"vertical"` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | radio circle size |
| `accent` | `"primary" \| "destructive"` | `"primary"` | the dot's fill; a stored name the table does not carry reads as `primary` |
| `disabled` | `boolean` | `false` | disables every option |
| `invalid` | `boolean` | `false` | every empty circle's edge and every row's focus ring take the destructive role; a chosen radio keeps its accent fill |
| `label` | `string` | none | accessible name for the group |
| `description` | `string` | none | announced help for the group (`aria-description`) |
| `ref` / `aria-id` / `onLayout` | host address | none | land on the row that holds the tab stop |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | resolved against the live state, merged last |
| `itemStyle` | `StateStyle` | — | resolved per option row, merged last |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the group's own; yours runs first |
| `testId` | `string` | — | stable locator for the group (the wrapper) |
| `itemTestId` | `(value: string) => string` | — | per-row locator, derived from its value |
---
# RangeSlider
Source: https://gpuixui.sudokw.com/primitives/range-slider
Two values, the ends of one interval, on one track
## Usage [#usage]
```tsx
import { RangeSlider, Stack, Text } from "@gpuixui/primitives";
import { useState } from "react";
const [price, setPrice] = useState([20, 80]);
${price[0]} – ${price[1]}
`$${value}`}
/>
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `readonly [number, number]` | — | controlled span |
| `defaultValue` | `readonly [number, number]` | `[min, max]` | uncontrolled start |
| `onValueChange` | `(value: readonly [number, number]) => void` | — | called with the ordered pair |
| `onValueCommit` | `(value: readonly [number, number]) => void` | — | the span a gesture ended on: once per drag release, once per key that moved an end |
| `onChange` | `(value: readonly [number, number]) => void` | — | deprecated alias for `onValueChange` |
| `min` / `max` | `number` | `0` / `100` | the closed range; a stored bound that is not a number is that default |
| `step` | `number` | `1` | quantisation; `0` is continuous; a stored step that is not a number is `1` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | thumb and track metrics |
| `disabled` | `boolean` | `false` | both thumbs leave the tab order |
| `invalid` | `boolean` | `false` | destructive fill and thumb borders |
| `label` | `string` | none | group name; thumbs become the provider's `rangeEnd` sentence (`" minimum"` / `" maximum"`) |
| `description` | `string` | none | announced help on the group node |
| `formatValue` | `(value: number) => string` | `String` | the announced text of each thumb; a blank answer keeps the number |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | the axis the two values run along; a vertical track needs a definite height |
| `style` | `StateStyle` | — | resolved against the live state, merged last on the track |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `children` | `ReactNode` | — | the parts to draw; omit for the default composition |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the range's own; keys land on a thumb |
| `ref` / `aria-id` | host props | none | land on the thumb that holds the tab stop — the end the reader last used, the start while neither holds focus — because the group node cannot take focus; the address moves with the reader |
| `onLayout` | `(event: EventPayload) => void` | none | the painted box of the thumb holding the tab stop — a thumb's, never the track's — reported on mount and again when the address moves; wrap the slider to measure the whole control |
| `testId` | `string` | — | stable locator for the track |
---
# Rating
Source: https://gpuixui.sudokw.com/primitives/rating
A score in stars, whole or in halves
## Usage [#usage]
```tsx
import { Rating, Stack, Text } from "@gpuixui/primitives";
import { useState } from "react";
const [score, setScore] = useState(0);
{score === 0 ? "Not rated" : `${score} of 5`}
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | controlled rating, `0..max` |
| `defaultValue` | `number` | `0` | uncontrolled start |
| `onValueChange` | `(value: number) => void` | — | the new rating; `0` means cleared |
| `onChange` | `(value: number) => void` | — | deprecated alias for `onValueChange` |
| `max` | `number` | `5` | how many stars to paint; a stored `null` is the default five, while a count that is there and cannot be laid out is one star |
| `allowHalf` | `boolean` | `false` | score in halves |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | star size from `iconSize` |
| `disabled` | `boolean` | `false` | out of the tab order, half opacity, inert |
| `readOnly` | `boolean` | `false` | shows the value, takes no input, no tab stop |
| `label` | `string` | none | the group's name |
| `description` | `string` | none | announced help for the group (`aria-description`) |
| `invalid` | `boolean` | `false` | chosen stars paint destructive |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StateStyle` | — | resolved against the live state, merged last |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the rating's own |
| `ref` / `aria-id` / `onLayout` | host props | none | land on the star that holds the tab stop, so they follow the score; measure a wrapper for the whole control |
| `testId` | `string` | — | stable locator on the group |
---
# ScrollArea
Source: https://gpuixui.sudokw.com/primitives/scroll-area
A bounded container that scrolls on the chosen axes
## Usage [#usage]
```tsx
import { ScrollArea, Stack, Text } from "@gpuixui/primitives";
{rows.map((row) => (
{row}
))}
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `axis` | `"vertical" \| "horizontal" \| "both"` | `"vertical"` | which overflow axes are `scroll`; a stored name this release does not carry reads as the default, in the paint and in the keys (`scrollAxis`) |
| `onScroll` | `(event: EventPayload) => void` | none | wheel/trackpad deltas |
| `onScrollChange` | `(event: EventPayload) => void` | none | the absolute offset after any move |
| `onLayout` | `(event: EventPayload) => void` | none | the scroller's painted box (`width`/`height`), reported on mount and on a size change; a composed `ScrollAreaViewport` defaults to it |
| `padding` | spacing step | none | frame padding; it does not scroll |
| `radius` | radius step | none | frame radius; the scroller is clipped by it, and a stored name this release does not carry paints no corner |
| `border` | `boolean` | `false` | the frame's `border` role |
| `scrollbarColor` | `string` | `colors.scrollbar` | the viewport thumb's colour |
| `scrollbarWidth` | `number` | platform's | the viewport thumb's thickness in px |
| `focusable` | `boolean` | `true` | whether the viewport is a tab stop |
| `label` | `string` | `"Scroll region"` | names the region (`role="group"` + `aria-label`) |
| `style` | `StyleDesc` | — | merged last on the frame; put the bounded size here |
| `className` | `string` | — | Tailwind classes, joined last on the frame |
| `children` | `ReactNode` | — | viewport content, or a composed `ScrollAreaViewport` |
| `testId` | `string` | — | the frame's locator |
| `viewportTestId` | `string` | — | the implicit viewport's locator |
| `viewportRef` | `Ref` | — | the implicit viewport's element |
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `ref` | `Ref` | — | the scroller itself; `scrollTo` and `scrollIntoView` address this element |
---
# Separator
Source: https://gpuixui.sudokw.com/primitives/separator
One theme-coloured rule between regions
## Usage [#usage]
```tsx
import { Separator, Stack, Text } from "@gpuixui/primitives";
Account
Profile and sign-in details.
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `orientation` | `"horizontal" \| "vertical"` | required | which way the rule runs; a stored name this release does not carry draws the **horizontal** rule — read raw, it drew a vertical hairline while keeping the caption row |
| `inset` | spacing step | none | margin at both ends, from `spacing` |
| `label` | `string` | none | a centred caption; ignored for vertical rules, and a blank or whitespace-only caption is no caption (it would paint a full blank line) |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `testId` | `string` | — | stable locator |
---
# Skeleton
Source: https://gpuixui.sudokw.com/primitives/skeleton
A pulsing placeholder for content that has not arrived
## Usage [#usage]
```tsx
import { Skeleton, Stack } from "@gpuixui/primitives";
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `width` | `number \| string` | `"100%"` | the block's width; a side that is not a paintable length (a `NaN` measurement, or a stored `""`) takes the default, because the runtime refuses the whole style batch for a dimension it cannot parse |
| `height` | `number \| string` | body line height | the block's height, with the same rule for a side it cannot paint |
| `radius` | `sm \| md \| lg \| xl \| none` | `"sm"` | corner radius; a stored name this release does not carry keeps `sm` |
| `circle` | `boolean` | `false` | full radius; the missing side mirrors the other |
| `reducedMotion` | `boolean` | the motion provider | paints the full-opacity frame and schedules no pulse; the prop overrides the provider |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `ref` / `aria-id` / `onLayout` | host props | none | the placeholder itself: the box a layout measures (so a placeholder can reserve exactly what it stands in for), the instance a caller holds, and the author id an automation host addresses |
| `testId` | `string` | — | stable locator |
---
# Slider
Source: https://gpuixui.sudokw.com/primitives/slider
A value on a track, by click, drag or keyboard
## Usage [#usage]
```tsx
import { Slider, Stack, Text } from "@gpuixui/primitives";
import { useState } from "react";
const [quality, setQuality] = useState(62);
Quality · {quality}%
`${value}%`}
/>
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `value` | `number` | — | controlled value |
| `defaultValue` | `number` | `min` | uncontrolled start |
| `onValueChange` | `(value: number) => void` | — | called only when the value changed |
| `onValueCommit` | `(value: number) => void` | — | the value a gesture ended on: once per drag release, once per key that moved it |
| `onChange` | `(value: number) => void` | — | deprecated alias for `onValueChange` |
| `min` / `max` | `number` | `0` / `100` | the closed range; a stored bound that is not a number is that default |
| `step` | `number` | `1` | quantisation; `0` is continuous; a stored step that is not a number is `1` |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | thumb `iconSize`, track `density.controlHeight` |
| `disabled` | `boolean` | `false` | out of the tab order, half opacity |
| `invalid` | `boolean` | `false` | the destructive role on the fill and thumb border |
| `label` | `string` | none | accessible name |
| `description` | `string` | none | announced help |
| `formatValue` | `(value: number) => string` | `String` | the announced text; a blank answer keeps the number |
| `orientation` | `"horizontal" \| "vertical"` | `"horizontal"` | the axis the value runs along; a vertical track needs a definite height |
| `style` | `StateStyle` | — | resolved against the live state, merged last on the track |
| `className` | `string` | — | joined after the recipe, so it wins conflicts |
| `children` | `ReactNode` | — | the parts to draw; omit for the default composition |
| `onClick` / `onKeyDown` / `onKeyUp` / `onFocus` / `onBlur` | host handlers | none | composed with the slider's own; the press and keys reach the track |
| `ref` / `aria-id` / `onLayout` | host props | none | land on the track, the element that carries the pointer and the value |
| `testId` | `string` | — | stable locator for the track |
---
# Spacer
Source: https://gpuixui.sudokw.com/primitives/spacer
Empty space as a fixed step or a flexible filler
## Usage [#usage]
```tsx
import { Spacer, Stack, Text } from "@gpuixui/primitives";
Read-only
Reset all
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `size` | spacing step | none (flexible) | a fixed square gap on both axes; `spaceStep` reads a stored step this theme does not carry as the default — a flexible filler — rather than a fixed item with no side |
| `ref` | `Ref` | — | the painted element |
| `aria-id` | `string` | — | the element's author id, exposed as the platform's automation id |
| `onLayout` | `(event: EventPayload) => void` | — | the painted box after the first paint and each resize |
| `className` | `string` | — | a Tailwind class string, joined after the fixed gap so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `testId` | `string` | — | stable locator |
---
# Spinner
Source: https://gpuixui.sudokw.com/primitives/spinner
An activity indicator drawn as a ring whose lit arc steps around the circle, because the runtime cannot rotate
## Usage [#usage]
```tsx
import { Spinner, Stack, Text } from "@gpuixui/primitives";
Refreshing…
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `size` | `IconSize \| number` | `"md"` | the ring's side: a theme step, or raw pixels |
| `accent` | `primary \| destructive \| muted \| on-primary \| on-secondary \| on-destructive \| on-accent \| on-disabled` | `"primary"` | the role every tick paints; a stored name the table does not carry reads as `primary` |
| `label` | `string` | `loading.busy` (`"Loading"`) | accessible name announced as a status; a blank one paints the ring as decoration (no node to walk into) |
| `reducedMotion` | `boolean` | the motion provider | holds the resting arc and schedules no interval; the prop overrides the provider |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `ref` / `aria-id` / `onLayout` | host props | none | the indicator's own box: the group a layout measures, the instance a caller holds, and the author id an automation host addresses |
| `testId` | `string` | — | stable locator |
## How the ring moves [#how-the-ring-moves]
The runtime paints a box and its opacity, and nothing else: it cannot rotate. So
the ring is `SPINNER_RING_TICKS` (12) static ticks placed on a circle by
`spinnerTicks`, and the **lit arc** moves instead — one head fully opaque, the
`SPINNER_ARC_TICKS` (7) behind it fading to `SPINNER_FADE_FLOOR` (0.2), the rest
of the ring empty. The head advances one tick per interval. That gap is what
reads as a turning arc; nothing rotates.
---
# Stack
Source: https://gpuixui.sudokw.com/primitives/stack
Children laid out along one axis, spaced by the theme scale
## Usage [#usage]
```tsx
import { Stack, Text, Button } from "@gpuixui/primitives";
Danger zone
Cancel
Delete
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `direction` | `"row" \| "column"` | required | the main axis; a stored name this release does not carry paints the **column** the recipe has always drawn, in the resolver too (`stackDirection`) |
| `gap` | spacing step | none | distance between children, from `spacing` |
| `align` | `start \| center \| end \| stretch \| baseline` | `stretch` | cross-axis alignment |
| `justify` | `start \| center \| end \| between \| around` | runtime default | main-axis distribution |
| `wrap` | `boolean` | `false` | allows wrapping to a second line |
| `grow` | `boolean` | `false` | takes leftover space on the parent's axis |
| `fullWidth` / `fullHeight` | `boolean` | `false` | `100%` on that axis |
| `ref` | `Ref` | — | the painted element, for a measurement, a scroll or a focus call |
| `aria-id` | `string` | — | the element's author id, exposed as the platform's automation id |
| `onLayout` | `(event: EventPayload) => void` | — | the painted box after the first paint and each resize |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `children` | `ReactNode` | — | content |
| `testId` | `string` | — | stable locator |
---
# Text
Source: https://gpuixui.sudokw.com/primitives/text
One run of type, named by a theme role and a semantic tone
## Usage [#usage]
```tsx
import { Text, Stack } from "@gpuixui/primitives";
Version 0.11.0
Released 17 October 2026.
nub add @gpuixui/primitives
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `role` | `TextRole` | `"body"` | size, leading, weight and family; a stored name the scale does not carry reads as `body` |
| `tone` | `TextTone` | `"default"` | colour role; a stored name the table does not carry reads as `default` |
| `align` | `"start" \| "center" \| "end"` | runtime default (left) | horizontal alignment; the runtime has no writing direction, so `start` paints left and `end` paints right, and a stored name this release does not carry paints the default |
| `truncate` | `boolean` | `false` | one line, clipped with an ellipsis |
| `clamp` | `number` | none | cut to a box N lines tall; a stored `null` is no clamp (the default), while a count that is there and cannot be laid out is one line |
| `selectable` | `boolean` | `false` | `userSelect: "text"` plus the theme's `selection` wash |
| `className` | `string` | — | a Tailwind class string, joined after the recipe so it wins conflicts |
| `style` | `StyleDesc` | — | merged last |
| `ref` / `aria-id` / `onLayout` | host props | none | the `` element itself: the box a measurement reads (its own wrapped height, not a wrapper's), the instance a caller holds, and the author id an automation host addresses |
| `testId` | `string` | — | stable locator |
| `children` | `ReactNode` | — | the string (or inline nodes) |
---
# TextArea
Source: https://gpuixui.sudokw.com/primitives/textarea
A multi-line text field sized in rows
## Usage [#usage]
```tsx
import { Label, TextArea, labelledBy } from "@gpuixui/primitives";
const notes = "Description";
const help = "Markdown is not interpreted.";
{notes}
;
```
## API [#api]
| Prop | Type | Default | Consequence |
| ---- | ---- | ------- | ----------- |
| `minRows` | `number` | runtime default | visible floor, in whole rows: a fraction floors, an unusable count reads as one, and a saved value that is not a number (JSON's `null`, text) leaves the runtime's own height |
| `maxRows` | `number` | runtime default | rows after which the field scrolls, in whole rows: a fraction floors, an unusable count reads as one, a saved value that is not a number leaves the runtime's own height (a cap read as one row collapsed a restored six-line field), and a cap below `minRows` is raised to it |
| `value` | `string` | — | controlled text |
| `defaultValue` | `string` | `""` | initial text when uncontrolled |
| `onValueChange` | `(value: string) => void` | none | called after each edit |
| `onChange` | `(value: string) => void` | none | deprecated alias for `onValueChange` |
| `placeholder` | `string` | none | empty-state text: painted in the `placeholder` role and announced as the field's description while the box is empty (the host's own hint is a fixed grey no theme reaches) |
| `readOnly` | `boolean` | `false` | accepts focus, refuses edits |
| `disabled` | `boolean` | `false` | read-only, out of the tab order |
| `invalid` | `boolean` | `false` | border and focus ring take the destructive role |
| `autoFocus` | `boolean` | `false` | takes focus on mount |
| `size` | `"sm" \| "md" \| "lg"` | `"md"` | row padding from density (a textarea's height is its rows) |
| `fullWidth` | `boolean` | `false` | `width: 100%` |
| `label` | `string` | none | accessible name |
| `description` | `string` | none | announced help (`aria-description`); a caller's permanent help wins over the hint |
| `tabIndex` | `number` | none | the field's place in the tab order; a disabled field sits at `-1` |
| `ref` / `aria-id` / `onLayout` | host props | none | the native `