Skip to content

raft-ui

React UI components for Raft.

raft-ui-guide

140 linesSource

Build with rUI: native CSS foundations for plain HTML and React components using Base UI and Tailwind CSS v4. Use for design tokens, styling, component selection, composition, and API lookup.

raft-ui

The package is raft-ui. Plain HTML can use foundation.css directly; the component guidance below applies to React. The React components are built on Base UI (@base-ui/react), Tailwind CSS v4, and tailwind-variants. Chat/workspace product domain: rails, sidebars, conversation panels, composers, task boards, activity inboxes, and search.

The rule files are also published at https://rui.botiverse.build/guides/overview. Do not register or install skills in a consumer project unless requested.

Working sequence

  1. Read only the rule files the current surface needs (table below); do not load every rule file by default.
  2. Verify unrecognized props, parts, variants, and tokens with rules/lookup.md.
  3. Implement the smallest composition that preserves native semantics and uses raft-ui where its behavior or visual contract is needed.
  4. Apply the review checklist after implementation, not as a reason to add wrappers before they are needed.

When this skill is loaded through TanStack Intent, run intent load raft-ui#raft-ui-guide --path before opening a linked rule file, then resolve the link relative to the returned SKILL.md directory.

Override your priors first

raft-ui is not shadcn/ui, not Radix, not MUI. These defaults are wrong here:

Your instinctReality in raft-ui
<Button variant="destructive">variant="danger". Full set: primary default information muted accent warning success outline ghost link danger danger-secondary danger-outline
<Button size="icon">size="icon-md|icon-lg"; icon-xs and icon-sm render as icon-md. Text sizes are md lg xl inline; xs and sm render as md
className="text-gray-500 bg-slate-50"Tailwind's default palette is banned. Use oklch semantic tokens: text-foreground-muted, bg-fill-muted
Flat props: <Dialog title="x" description="y">Compound parts: <Dialog><DialogContent><DialogHeader><DialogTitle>… — DialogContent portals itself
Radix API (asChild)Base UI API. Use the render prop, not asChild
Add a kind/state/emphasis prop for a structural differenceUse the dedicated part or component instead
<Button loading> doesn't exist, roll a spinnerloading and loadingLabel are real props on Button

Never guess a prop or variant name. The tables below are orientation, not an API reference — see rules/lookup.md.

Principles

  • Composition over configuration. Visible structure lives in children and named parts, never in a prop that switches layouts.
  • Prefer semantic tokens over raw color. Use inline oklch() only for a one-off precision value, then tokenize repeated values.
  • Base UI owns headless behavior (open state, focus, keyboard, collections). Product state (upload lifecycle, permissions, workflow) belongs in your app, not in a component prop.
  • Raft is the only visual language, in light and dark modes; ThemeProvider switches the mode globally. Components adapt themselves.
  • Yellow and pink split roles: primary (Source Yellow) is for hints and interactive expression (indicators, the active tab); accent is for core actions (a dialog's confirm, the composer's send). Never use yellow as decoration or background wash — see rules/styling.md. Focus rings are ink, not yellow: a 2px paper gap and a 2px ink ring (shadow-raft-focus).

Common mistakes

MistakeFix
asChild on a trigger or close partBase UI: render={<Button … />}
Actions menu built out of SelectDropdownMenu — see rules/forms.md
Persistent condition announced with toast.info()Banner — toasts expire, conditions don't
Buttons or links inside a TooltipPopover — tooltip content is unreachable
Destructive confirm as a styled DialogAlertDialog with AlertDialogAction / AlertDialogCancel
Validation copy disconnected from its controlField + FieldError
PanelHeader where you meant the bare framePanelHeaderRoot — PanelHeader is the composed identity chrome, see rules/layout.md
Re-created raft-ui divider, badge, or type stylingSeparator, Badge, or Text; keep native semantics when intentional

Which control? Which feedback channel?

plaintext
Choosing an option control:
Is it an action, not a value? ──────────── DropdownMenu / ContextMenu
└─ No: on/off?
   ├─ Applies immediately ──────────────── Switch
   ├─ Submitted with a form ────────────── Checkbox
   └─ No: one of several?
      ├─ 2–5, form answer ──────────────── RadioGroup
      ├─ 2–5, switches a view in place ─── SegmentedControl
      ├─ Swaps visible content panels ──── Tabs
      ├─ One of many ──────────────────── Select
      └─ Searchable or multi-select ────── Combobox (chips for multi)

Delivering a message:
Caused by the user's last action, safe to miss? ── toast.info() / toast.success() / toast.warning() / toast.error()
Condition that is still true? ──────────────────── Banner
Happened elsewhere, must persist until read? ───── NotificationCenter
Loading? ── known layout → Skeleton · unknown duration → Spinner · known % → Progress

Intent names are not consistent across components — negative is danger on Button/Badge/Status/Progress, destructive on Banner, and error on toast objects and NotificationCenterItemIcon. Never carry a variant value from one component to another.

Quick component map

NeedUse
Callout / inline status messageBanner
Transient notificationtoast.info() etc. + ToastProvider
Confirm a destructive actionAlertDialog
Generic modalDialog
Edge-anchored panelDrawer
Rich hover previewPreviewCard
Plain hover hintTooltip
Right-click menuContextMenu
Button-triggered menuDropdownMenu
Workspace search and grouped resultsSearchShellRoot + the applicable Search*Result parts
Activity or saved-item inboxActivityInboxPanel + InboxItem
Label, help, or validation around a form controlField + the applicable field parts
Design-system dividerSeparator; keep native semantics when styling is intentionally inherited
Loading placeholderSkeleton
Inline busy indicatorSpinner
Determinate progressProgress
Small count / tagBadge
Presence or state dotStatus
Nothing-here stateEmptyState
Keyboard shortcutKbd / KbdGroup
raft-ui typography treatmentText.Sans / Text.Heading / Text.Mono

Read these when relevant

FileRead it when
rules/lookup.mdYou need a prop, variant value, or token — read this before guessing
rules/composition.mdAssembling compound parts, overlay containers, business actions, or wrappers
rules/forms.mdAny input, option picker, or tabs — includes Select vs DropdownMenu
rules/overlays.mdDialogs, drawers, popovers, menus, tooltips
rules/feedback.mdBanners, toasts, loading, empty states, badges
rules/display.mdTypography, code, avatars, files, keyboard hints
rules/layout.mdShell, panels, navigation, resizing, task surfaces
rules/conversation.mdChat, sidebar, activity inbox, workspace search, composer
rules/styling.mdWriting any className, color, shadow, or tv() recipe
rules/technical-overview.mdTechnical foundations, package contents, styles, and providers

Review checklist

When reviewing or editing code that uses raft-ui, check:

  • No variant value carried across components — verify against the declarations
  • Controls with raft-ui label, description, or validation treatment use Field; simpler native associations remain explicit and accessible
  • No Tailwind default palette (gray-*, slate-*, …), hex, rgb(), or hsl() — semantic tokens, raw oklch() only for one-offs
  • No asChild — element substitution uses render
  • Do not recreate a raft-ui component's visual or behavioral contract with ad hoc markup; native semantic elements remain valid when no library contract is needed
  • Every Avatar with an AvatarImage also has an AvatarFallback; every blocking overlay has a Title
  • Menu rows are …Item parts, not raw <button>s — raw buttons drop out of keyboard navigation
  • Product state (upload, permissions, read/unread) lives in the app; the component renders parts, not a state prop
  • No shadow written as shadow-[var(--…)] — named utility or fully literal

Stability

raft-ui is the stable surface; raft-ui/wip is experimental. Import cn from raft-ui/cn.