cubeui

A shadcn registry of 97 items, authored in React Native and compiled to plain DOM components. One palette, one component vocabulary, two platforms — and no react-native-web in the web half.

See it

Every item, drawn on one page per platform: scroll it instead of opening a story at a time.

Both are stories in the Storybook, under Gallery, where each item also has stories of its own with its props and its tests.

Install

Add one line to your components.json. The item names are the same on both sides — card is card — and the URL is what decides which half you get.

// a DOM app
"registries": { "@cubeui": "https://cubicecho.github.io/cubeui/r/{name}.json" }

// an Expo app
"registries": { "@cubeui": "https://cubicecho.github.io/cubeui/r/native/{name}.json" }
npx shadcn@latest add @cubeui/tokens @cubeui/button

@cubeui/tokens is the one every consumer installs: it is the palette, emitted from a single source as oklch() for the DOM and as hex for React Native, whose style engine cannot parse oklch() at runtime.

On a Vite app, put compilerOptions.paths in the root tsconfig.json too. npm create vite writes them into tsconfig.app.json; the shadcn CLI reads only the root file, finds no @/ alias, and resolves it as a relative path — so the install writes a literal @/ directory at your project root and reports success.
Upgrading from the web-only cubeui? @cubeui/form is now the Form component, installed at components/ui/form.tsx; the old bundle of form items is @cubeui/form-set. The primitives now install over shadcn's own. The migration notes list every change.

Items

83 on both platforms · 14 web-only · 0 native-only. Anything not marked both has no half on the other platform.

ItemPlatformsWhat it is
@cubeui/action-button both An icon button with a required accessible name (`label`), and a tooltip that survives being disabled: `disabled` is `aria-disabled`, so the `hint` saying why is still reachable. `onClick` on the web, `onPress` on device, where the tooltip opens on a long press.
@cubeui/alert both A callout: a tinted, bordered box with an icon, a title, a line under it and an optional action. `default`, `info`, `warning` or `destructive`; only `destructive` is announced as an `alert`, the rest as a polite `status`.
@cubeui/alert-dialog both shadcn's modal that interrupts to ask, with the same part names on both halves: radix's `AlertDialog` on the web, the native `Dialog` as an `alertdialog` with no close button on device.
@cubeui/app-form web The TanStack Form binding: useAppForm, plus one-element fields — InputField, TextareaField, SelectField, CheckboxField, SwitchField — that take the form and a checked field name instead of a render prop, and SubmitButton.
@cubeui/badge both A pill naming what something is or what state it is in — and, with no label, the dot that says the same thing in a tighter row. `onRemove` adds a trailing ✕ for a tag the user can take off.
@cubeui/button both A Pressable-backed button whose variants split into container and text halves, because native inherits no text colour.
@cubeui/calendar both A month of single-date selection: react-day-picker on web, a date-fns grid on device.
@cubeui/card both A card with typed text slots, an optional accent bar, and a whole-card press target that stays reachable by keyboard.
@cubeui/card-layout both A card with its slots already placed: icon, title, description, header action, body, and a footer split between prose and buttons.
@cubeui/centered-layout both A page that is one card, centred both ways: a sign-in, a token gate, a check-your-email screen. Full height, padded off the edges, a `max-w-sm` `CardLayout` taking every card slot; on device a scroll view that keeps the form above the keyboard.
@cubeui/checkbox both Radix on web, with its whole prop surface; a Pressable announced through role and aria-checked on device. Controlled or uncontrolled on both.
@cubeui/code both Monospaced text: Code, the inline span, and CodeBlock, a bordered block of preformatted text with an action slot for a CopyButton, a wrap prop to wrap long lines instead of scrolling sideways, and a maxHeight that scrolls.
@cubeui/color both Derives an accent and a desaturated tint from a user-chosen hex colour.
@cubeui/color-bar both The left-edge accent stripe a card wears, readable in both themes and labelled for anyone who cannot see it.
@cubeui/color-dot both A small swatch standing in for a colour-coded thing in a list.
@cubeui/color-field web A colour picker as one line, over a field the compiler checked holds a string.
@cubeui/color-picker both Swatches plus a hex field, replacing `<input type="color">` — which has no native counterpart.
@cubeui/color-picker-field both `ColorPicker` bound to a hex-string form field, its swatch row named by the field's label. `ColorField`, under an item name that does not collide with the web-only `color-field`.
@cubeui/command both A searchable list of choices: shadcn's Command parts over cmdk on web (arrow keys, Enter, fuzzy ranking); on device a search Input over pressable rows, filtered on every typed word, chosen by press. The same part names and the props MultiSelect uses on both.
@cubeui/confirm both One confirmation prompt for both platforms, as a promise: `if (await confirm({…})) remove()`.
@cubeui/confirm-button both A destructive button that asks first, with a required description of what is lost: an `ActionButton` that opens `ConfirmDialog`, with `requireText` to ask for the name before a big delete.
@cubeui/confirm-dialog both A destructive-action confirmation dialog: Cancel plus one destructive button.
@cubeui/control web Every cubeui control in one install.
@cubeui/copy-button both An icon button that copies `value` to the clipboard and turns into a tick for a moment, named `Copy` (or `label`) and then `Copied`.
@cubeui/date-field web A date and a date range as one line each, over fields the compiler checked hold a Date and a DateRange.
@cubeui/date-picker both A date and optionally a time behind a popover, plus a range picker that closes when the range is complete. Clearing is a real button, not one nested in another.
@cubeui/date-time-field both `DateTimeInput` bound to a `Date | null` form field, named by its label and its date: "Due September 15th, 2026". Its own item for the weight of the calendar.
@cubeui/date-time-input both A date popover and a time field that commit one Date back — or, with `clearable mode="date"`, an optional date only.
@cubeui/description-list both Read-only facts: `DescriptionList` over `PropertyRow`s, each a label, a value, an optional hint under the value and an optional action beside it. A `<dl>` of `<dt>`/`<dd>` groups on the web, `role="list"` on device; `layout="inline"` puts labels beside values and wraps to stacked when narrow.
@cubeui/detail-header both The header row of a detail view: back, colour dot, title, badge, subtitle, actions.
@cubeui/detail-page both The page shell plus a loading / not-found guard that narrows the entity for its `contentSlot`.
@cubeui/dialog both A modal. Radix on web with its focus trap and scroll lock; a transparent native Modal with a dimmed backdrop and a centred card.
@cubeui/dialog-layout both A dialog whose body scrolls under a header that does not, and which asks before closing on unsaved work. Composes the header/content/footer chassis rather than owning a second copy.
@cubeui/disclosure both A titled part of a screen whose body shows and hides: "Show completed (3)", "Raw output". The header is one button with a turning chevron and `aria-expanded`, an optional `actionSlot` sits beside it rather than inside it, and `contentSlot` is mounted only while open. Uncontrolled with `defaultOpen`, or controlled with `open`/`onOpenChange`.
@cubeui/disclosure-row both A list row that opens: the whole heading is the button, the actions sit outside it, and the body is Item's footer. Built on the same Item every other row in the list is, with `Disclosure`'s button: `aria-expanded`, a turning chevron, and `contentSlot` mounted only while open. Controlled with `open`/`onOpenChange`.
@cubeui/download-button both An icon button that saves `source` as a file named `filename`, disabled with a spinner while a fetched `source` is pending, and `downloadBlob()`, the function it calls. The browser's own download on the web; on device the share sheet or a folder the person picks, chosen by `destination` or, by default, from a menu on the button.
@cubeui/empty both shadcn's `Empty` parts on both halves, drawn as `EmptyState` is: the muted icon bubble, the title, the line under it and what to do about it. `EmptyState` is built on them.
@cubeui/field both Label, description and error chrome around a control. The unbound half of the form set.
@cubeui/field-row web Fields side by side on one row, wrapping when they no longer fit rather than squeezing to a width the values no longer read at.
@cubeui/file-picker both A drop zone, or a button, over a hidden file input on web. The native half is a placeholder, not an implementation.
@cubeui/form both Fields bound to TanStack Form: label, control and error wired once, for both platforms. `useAppForm`, the text, select, checkbox and switch fields, `SubmitButton`, and `bindToForm` for the rest.
@cubeui/form-dialog both Dialog chrome for a form, with a footer that leaves the submit control to the caller.
@cubeui/form-element both A real `<form>` on web so Enter submits; a `View` on native, where nothing raises a submit event.
@cubeui/form-field web One field: a label wired to its control, a description that sits inline or behind a help icon, an error the control is described by, and a skeleton the right height for what is loading. Presentational, so the TanStack binding sits on it rather than beside it.
@cubeui/form-set web Every cubeui form component in one install, TanStack binding included. Named `form-set` rather than `form`: that name is the React Native `Form` component in this registry, and the shadcn CLI resolves a cross-item import by basename, so the two cannot share it.
@cubeui/format both How a count, a size in bytes, a duration and a date are written for a reader, and how a row of them is joined: formatCount, formatBytes, formatDuration, formatDate, formatAgo and joinStats, on Intl with no dependency.
@cubeui/header-content-footer both The page chassis: chrome that stays, a body that scrolls, floors that let it. Ships the sticky preset alongside the base.
@cubeui/icons both The icon set, per-icon imports on native and a tree-shaken barrel on web, with the context that gives native icons a colour to inherit.
@cubeui/input both Single-line text entry, with an optional icon at its start (`leadingSlot`) or an icon-sized control at its end (`trailingSlot`). Keeps a real DOM input on web so time, number and min/max survive; a TextInput on device.
@cubeui/item both shadcn's list-row parts — `Item`, `ItemMedia`, `ItemContent`, `ItemTitle`, `ItemDescription`, `ItemActions`, `ItemHeader`, `ItemFooter`, `ItemGroup`, `ItemSeparator` — on both halves, with shadcn's names, props and metrics. For a row `ListItem` does not fit: a header or footer line, a badge beside the title.
@cubeui/label both A caption for a control. Radix on web so clicking it focuses the control; plain text on native, where that association does not exist.
@cubeui/layout web Every cubeui layout shell in one install.
@cubeui/list-item both One row of a list: `leadingSlot` (an avatar, a checkbox), a truncating `title` over a `description`, `meta` and `actionSlot` at the far end. With `onPress` the middle is one button and `leadingSlot` and `actionSlot` stay separate controls beside it, never nested in it.
@cubeui/markdown web A Markdown string rendered from the theme's tokens and the registry's own primitives: a fenced block is a CodeBlock, a table the Table, a rule a Separator, a task item a Checkbox. GitHub-flavoured, raw HTML never rendered, URLs filtered. Optional wikilinks, and a resolveLink for links between an app's documents with the pending and broken looks built in.
@cubeui/markdown-editor web A Markdown source and its rendering: a textarea beside a Markdown preview, and a toggle between edit, split and preview that the caller may hold or leave to the component.
@cubeui/menu both A popover of actions: radix DropdownMenu on web (arrow keys, typeahead, focus back on the trigger); the popover's centred sheet with menu and menuitem roles on device. A row takes an icon, a label, a trailing node and a destructive colour, and the menu closes when one is chosen. Checkbox rows toggle and keep the menu open; radio rows in a group choose one and close it.
@cubeui/multi-select both A combobox that selects more than one thing, over Popover and Command: searchable on every word, with an optional create row and coloured chips. On the web radix and cmdk make it portalled and keyboard-operable; on device it opens the native sheet, and each chip is also removable with its ✕.
@cubeui/multi-select-field web The multi-select as one line of a form, over a field the compiler checked holds a list of strings.
@cubeui/option-select both A select taking its choices as options rather than seven primitives to assemble, with groups, separators and notes, and the id and aria props landing on the trigger. Radix's listbox on the web, the Select sheet on device; with searchable, a search box above the list in a popover.
@cubeui/page both The page shell, its header, the card grid and the empty state — the four pieces every route repeats.
@cubeui/page-header both The title block a page wears in a header slot: an optional trail, an icon and title at a chosen heading level, a description, the page's buttons at the far end, and a search or filter row beneath. Carries the inset the page chassis leaves it.
@cubeui/page-layout both A whole page: a PageHeader pinned above a body that scrolls under it, in a named column.
@cubeui/password-field web A password as one line, with a reveal button none of the nine hand-written password inputs has.
@cubeui/password-input both A password box with a reveal button that is named, changes its name, and does not submit the form: Input with type=password (secureTextEntry on device) and a show/hide button in its trailing slot.
@cubeui/popover both Radix anchored to its trigger on web; a centred native sheet on device, because anchoring is a pointer-era affordance.
@cubeui/primitive web The shadcn primitives cubeui publishes, so a change made here reaches every project.
@cubeui/progress both A horizontal bar showing how much of something is done: `role="progressbar"` with its value announced, `value` from 0 to `max` (100 by default) as shadcn's, clamped. `label` names it; `valueLabel` reads the value in words.
@cubeui/query-state both The three rungs a list of rows climbs before it is a list of rows — failed, pending, empty — with the error card and the row placeholders exported on their own.
@cubeui/radio-group both An exclusive choice, one source for both platforms: `row` (circle, label, description), `card` (a tile with an icon) or `segmented` (a full-width framed row, icon-only segments allowed), with one tab stop and arrow keys that move and select handled inside.
@cubeui/radio-group-field both A set of exclusive choices bound to a TanStack form field, the options on the page and each able to carry a line saying what it means. The group is named by its title through aria-labelledby.
@cubeui/readable-text-color both Picks black or white ink for text sitting on a colour the user chose, by WCAG contrast.
@cubeui/route-error both What shows when the tree below a route has thrown, with nothing in it that could have been the cause. The whole error boundary in one element: an alert with a title, a friendly line, an optional copyable raw message (`details`) and extra buttons beside Try again (`actionsSlot`).
@cubeui/search-input both A search box: Input with a Search icon inside at its start, type=search (a searchbox), named by `label` (default Search), and a named clear button while it holds text.
@cubeui/section both A heading over a group of fields or rows: title, description, an action at the far end, an optional rule and an optional card surface. The title is a heading of rank `level` (2 by default), so a long settings page is navigable.
@cubeui/section-heading both The small muted heading above a section of content, with an optional heading `level`.
@cubeui/segmented both A segmented control: the pill, with an optional icon before its label; the named row it sits in, which can hold the current value and drop the labels to icons under a width; and the two class helpers a router link needs instead.
@cubeui/select both A radix listbox under its trigger on web; the options in a Modal sheet on device, which is what every native platform does.
@cubeui/separator both shadcn's one-pixel rule, `orientation` and `decorative` as shadcn's: hidden from assistive tech by default, a `role="separator"` with `decorative={false}`.
@cubeui/setting-row both One row of a settings page: a `title` and `description` at the start, the control that changes it in `actionSlot` at the end, dropping under the text when the row is too narrow for both. `actionSlot` may be a function handed the title's id, so a switch or select is named by the title through `aria-labelledby`.
@cubeui/sidebar both An app's sidebar: a header, a body that scrolls and a footer on the sidebar palette at a fixed width, with an optional titled list of rows and the nav row itself — a link with an icon, a truncating label, a count and aria-current on the active page. Ships BarNavItem, the same place as an icon-only link for an app bar: a required name that is also its tooltip, the count as a badge on the icon and a status as a dot. A controlled collapsed state folds the sidebar to a rail of icons, with SidebarCollapseButton and useSidebar for what the app puts in it.
@cubeui/skeleton both shadcn's loading placeholder: a rounded `bg-accent` block the caller sizes, pulsing on both halves (`animate-pulse` on the web, the same opacity curve in `Animated` on device).
@cubeui/spinner both A loading indicator: the LoaderCircle glyph turning, one `role="status"` named by `label`, sized and coloured by `className`.
@cubeui/split-layout both Two panes side by side on a named width scale, with the floors that keep one wide child from pushing the other off the screen. Ships SidebarLayout, the preset for the common case where one pane is the screen and the other is beside it — and, with sidebarHideBelow, the app bar (brand, nav, status, action) that stands in for a navigation rail on a narrow screen.
@cubeui/stat-tile both One number on a card: a `label`, a large tabular `value`, an optional `hint` under it and an `iconSlot` before the label. Given `onPress` the tile is a button, and with `selected` a toggle (`aria-pressed`) for the filter tile over a list. `loading` holds the figure's place.
@cubeui/switch both Radix on web; a Pressable track with a moving thumb on device, rather than RN's Switch, whose raw colour props would not follow the theme.
@cubeui/switch-field both A switch and its caption on one row, with the whole row in the hit target on both platforms.
@cubeui/table web shadcn's data table, re-published from here so the table every admin page draws is edited in one place: row and column headers scoped, and a container a keyboard can scroll when the columns do not fit.
@cubeui/tabs both Radix on web; a row of Pressables over a context on device. Controlled (value/onValueChange) or uncontrolled (defaultValue) on both. A trigger can carry an icon beside its label, in the tab's active or inactive colour, and a dot or a count at its far end as `trailingSlot`. A list with more tabs than fit scrolls sideways and keeps the selected tab in view.
@cubeui/textarea both Multi-line text entry. Keeps a real DOM textarea on web; a multiline TextInput on device. Takes a submit key, a ref, and maxRows to grow with its text.
@cubeui/theme-picker both Light, Dark and System as radio tiles, or with variant="compact" as a full-width row of icon-only radios for a sidebar or header, and useThemePreference(), which stores the choice and applies it: a class on <html> on the web, Appearance.setColorScheme on device, where the app passes its own storage. With palettes={PALETTE_PREFERENCES} it adds a palette choice beside the theme (default, or the dark-only monokai), stored by usePalettePreference() and applied as data-palette on <html> on the web and by PaletteProvider on device. THEME_PRE_PAINT_SCRIPT is the inline <script> that paints the stored theme and palette before React mounts.
@cubeui/toast both Transient messages for mutations fired outside a form — the failures that otherwise reach only the console.
@cubeui/toggle-chip both A small on/off pill — a filter chip, a day toggle, a swatch — with its colour split across container and label.
@cubeui/tokens both The palette as a Tailwind stylesheet, emitted from the same source as the native one so the two cannot drift.
@cubeui/tooltip both Radix on hover and focus on web; long-press on device, which is the platform's own way to ask what a control does.
@cubeui/top-bar-layout both An app shell with a bar across the top and no sidebar: brand at the start, navigation after it that scrolls sideways when narrow, actions at the end, and the page below — a banner and a main landmark, the bar sticky on the web.
@cubeui/unsaved-changes-guard both useUnsavedChangesGuard() and UnsavedChangesDialog: one question for the three ways out of a page with unsaved edits. It takes the app's router blocker as it comes (TanStack Router's or React Router's, with no router imported), guards the page's own Close button through leave(), and holds a beforeunload listener on the web while there are changes.
@cubeui/utils both The class merger every component calls, the one hover helper that has to be gated on platform, and the `webAs` declaration the RN→web compiler reads.

Storybook

Every item with a story is at cubicecho.github.io/cubeui/storybook/, rendered in a real browser — the same stories the test run drives, with the a11y addon set to fail rather than warn.

A consuming app can put them in its own sidebar with one entry, so someone reading Domain/UploadPanel can click through to the Button it is built from:

// .storybook/main.ts
refs: { cubeui: { title: "cubeui", url: "https://cubicecho.github.io/cubeui/storybook" } }
Composed stories render here, under these tokens. That makes them documentation, not a test of your theme — the question "does Button still pass contrast after our index.css override" is only answered by a story compiled in your app — which is what the -stories items are.

Installing @cubeui/<name>-stories puts that component's stories beside it in your tree, importing it as @/components/ui/<name>, so they render under your stylesheet and run as tests under your addon-vitest. They need Storybook 9 or later on @storybook/react-vite and declare no npm dependencies. Available for: badge, button, card, section-heading, segmented, sidebar, theme-picker, toggle-chip.

npx shadcn add @cubeui/button-stories