# SUI > SUI is a collection of composable React components built on Base UI and Tailwind CSS. These documents include complete examples from the workspace. # CLI Search, inspect, install, and update SUI source with shadcn. Page: https://sui.draco.dev/docs/cli SUI uses the shadcn CLI. Complete the [registry configuration](/docs/installation#prepare-the-consuming-application) in the consuming application, then run commands from that application's directory. ## Search and inspect ```bash bunx --bun shadcn@latest search @sui -q input bunx --bun shadcn@latest view @sui/sensitive-input ``` Search matches registry names and descriptions. View shows a manifest's files and dependencies before you install it. The [registry guide](/docs/registry) explains the catalog and item formats. ## Install source ```bash bunx --bun shadcn@latest add @sui/button @sui/sensitive-input bunx --bun shadcn@latest add @sui/data-table ``` The CLI copies required source files into your configured `ui`, `components`, `lib`, and `hooks` directories, installs package dependencies, and merges SUI styles into the CSS file in `components.json`. Files become part of your application and can be edited locally. Each item carries its local dependency closure. Installing a component does not require first installing the entire library. Use `@sui/sui` for the complete collection, `@sui/sui-style` for shared CSS, or `@sui/sui-theme` for the theme helpers and styles. Editor and the complete collection require the local Monaco worker setup described in [compatibility](/docs/compatibility#editor-workers). ## Review an update ```bash bunx --bun shadcn@latest add @sui/button --dry-run bunx --bun shadcn@latest add @sui/button --diff ``` An update can affect shared dependencies and CSS as well as the requested component. Compare changes with your local edits before accepting an overwrite. Installation is a source copy; installed files do not automatically follow future changes in the SUI repository. ## Common issues | Symptom | Check | | --- | --- | | Unknown `@sui` registry | Run from the consuming application and check its `components.json`. | | GitHub returns `404` | Check the configured URL and whether the requested item is available in the registry. | | Import path differs from a docs example | Follow configured aliases, rather than workspace paths. See [import mapping](/docs/installation#import-installed-source). | | Missing animation or theme styles | Confirm the configured CSS file is imported by the application. | | Monaco worker import fails | Use a compatible `?worker` loader and include the installed declaration file. | For installation through an assistant, see [MCP](/docs/mcp). For API and example text, see [LLMs](/docs/llms-txt). --- # Compatibility Application responsibilities, SSR, worker loading, and browser capabilities. Page: https://sui.draco.dev/docs/compatibility SUI targets React 19 and Tailwind CSS 4 with Base UI primitives. Source installation uses your application's aliases and CSS entrypoint; see [installation](/docs/installation). Modern CSS features, including OKLCH colors and backdrop filters, are part of the visual system. ## SSR and application state The documentation site uses TanStack Start and SSR. Browser-dependent viewers and the editor initialize after mounting, with base content or skeletons while loading. In an application with server and client component boundaries, keep interactive components on the client side and preserve installed `use client` directives. Theme persistence, language routing, validation, and business requests belong to the application. ThemeToggle and LocaleToggle receive controlled values and callbacks. Blocks provide English defaults and accept `labels`; pass translations from your application's locale system. Use [Field](/docs/components/field) to compose labels, descriptions, and validation messages. ## Editor workers [Editor](/docs/components/editor) loads Monaco and its workers locally. Its installed source uses Vite-compatible `?worker` imports. A different bundler requires an equivalent worker integration; installing source does not configure that bundler automatically. Include the installed `worker.d.ts` in TypeScript's source paths. CodeViewer and DiffViewer can still show and copy raw content if syntax highlighting fails. The editor and viewers share the Shiki-based highlighting infrastructure. ## Browser features | Feature | Application consideration | | --- | --- | | Clipboard | Browser permissions and a secure context determine availability; the components provide failure feedback. | | Fullscreen | Browser fullscreen is optional; permission or platform restrictions can prevent entry. | | Glass | CSS + SVG is the default. Optional WebGPU enhancement falls back when capabilities or capture fail. | | Theme transitions | The toggle respects reduced motion and can apply the theme without a view transition. | | HTML preview | HtmlViewer defaults to a sandbox with scripts allowed and without same-origin permission. Editor's built-in HTML preview disables scripts. | See [Glass limitations](/docs/components/glass#limits) for cross-origin images, frames, video, Canvas, and capture cadence. A transparent surface needs to remain readable over its actual background; use an ordinary or denser surface when needed. ## Accessibility and direction Compose visible labels or accessible names for icon-only actions. Keep titles in dialogs, provide image descriptions, and use externally supplied labels for loading and feedback. Keyboard and focus behavior come from the underlying primitives; replacing native controls with custom markup can change those behaviors. Use [Direction](/docs/components/direction) for RTL context and keep the application's `dir` attribute consistent. Loader and theme transitions honor reduced-motion preferences; custom animations added by the application need the same consideration. ## Verification After installation, run the consuming application's type checks and production build. Test its keyboard flow, forms, overlays, copy feedback, themes, and translated labels. The SUI documentation examples exercise components locally; they do not verify your application's routing, persistence, API requests, or deployment. --- # Design guidelines Complete typography, spacing, surface, and interaction rules with recommended and avoid examples for every rule. Page: https://sui.draco.dev/docs/design-guidelines Apply all of these rules when composing application interfaces with SUI. Every rule includes a working visual comparison and copyable code. Styles in the avoid column are teaching examples and should not be used in product interfaces. ## Shared styles and semantic colors Import `@workspace/ui/globals.css` and compose components from `@workspace/ui/components/*`. Style native text elements with Tailwind size and weight utilities. Use semantic tokens such as `bg-background`, `bg-card`, `text-foreground`, `text-muted-foreground`, `bg-primary`, `border-border`, and `ring-ring`; keep destructive actions on their independent `destructive` tokens. The default appearance uses Apple-inspired neutral surfaces and a blue accent. Select shared palettes with `data-color`: `default`, `bamboo`, `mauve`, `mist`, `sand`, `pine`, or `rose`. Remove the attribute or select `default` to reset the appearance; use `dark` for dark mode. Accent, focus, and chart colors change together while backgrounds and body text remain neutral. See [Theming](/docs/theming) for configuration. ```css @import "@workspace/ui/globals.css"; ``` ```html ``` ## Use 14px for content text All content text—body, buttons, data, other interactables—must be 14px in size. 16px and above are restricted to headings and subheadings. ### Example: design-guidelines-content-text-size ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return

{zh ? "内容文字" : "Content text"}

; } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return

{zh ? "内容文字" : "Content text"}

; } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx

Content text

; ``` **Avoid** ```tsx

Content text

; ``` ## Always sentence case headings Never capitalize or uppercase headings. Product names must be title-cased. The avoid column shows both title case and an `uppercase` transformation. ### Example: design-guidelines-heading-case ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample() { return

Recent requests

; } function AvoidSample() { return (

Recent Requests

Recent requests

); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx

Recent requests

; ``` **Avoid** ```tsx

Recent Requests

; ``` ```tsx

Recent requests

; ``` ## Never change the font’s tracking Do not use the `tracking-*` classes to change the spacing between characters. ### Example: design-guidelines-font-tracking ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "项目指标" : "Project metrics"}

); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "项目指标" : "Project metrics"}

); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx

Project metrics

; ``` **Avoid** ```tsx

Project metrics

; ``` ## Never use font-bold Use `font-semibold` for headings and `font-medium` for bold inline text. Never use `font-bold`. ### Example: design-guidelines-font-weight ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( <>

{zh ? "账户设置" : "Account settings"}

{zh ? "必填" : "required"} ); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( <>

{zh ? "账户设置" : "Account settings"}

{zh ? "必填" : "required"} ); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx <>

Account settings

required ; ``` **Avoid** ```tsx <>

Account settings

required ; ``` ## Put related text closer together Related text should have smaller spacing around it than the content it belongs to. ### Example: design-guidelines-related-text-spacing ```tsx import { Button } from "@workspace/ui/components/button"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "网站分析" : "Web analytics"}

{zh ? "无需修改代码即可衡量网站访问量" : "Measure site traffic without changing your code."}

); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "网站分析" : "Web analytics"}

{zh ? "无需修改代码即可衡量网站访问量" : "Measure site traffic without changing your code."}

); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Button } from "@workspace/ui/components/button";

Web analytics

Measure site traffic without changing your code.

; ``` **Avoid** ```tsx import { Button } from "@workspace/ui/components/button";

Web analytics

Measure site traffic without changing your code.

; ``` ## Optically align spacing around text Spacing around text should take into account its line height. Typically this means vertical spacing should be slightly smaller than horizontal. ### Example: design-guidelines-text-spacing ```tsx import { Card } from "@workspace/ui/components/card"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return {zh ? "内容文字" : "Content text"}; } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return {zh ? "内容文字" : "Content text"}; } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Card } from "@workspace/ui/components/card"; Content text; ``` **Avoid** ```tsx import { Card } from "@workspace/ui/components/card"; Content text; ``` ## Never transition colors for hover states Color changes on hover must be immediate. Transitions on fast interactions make the UI feel sluggish. Move the pointer between the two buttons to compare their response. SUI buttons can still transition `transform` and `box-shadow`; the hover color changes immediately. ### Example: design-guidelines-hover-color-transitions ```tsx import { Button } from "@workspace/ui/components/button"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( ); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( ); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Button } from "@workspace/ui/components/button"; ; ``` **Avoid** ```tsx import { Button } from "@workspace/ui/components/button"; ; ``` ## Never use borders with drop shadows Use `ring-1 ring-border` to create a transparent border that maintains sharp edges. Do not combine `border` with a drop shadow on the same surface. SUI Card already supplies a shadow and a subtle ring. The avoid example disables that ring to show the border-and-shadow combination explicitly. ### Example: design-guidelines-shadow-borders ```tsx import { Card } from "@workspace/ui/components/card"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( {zh ? "内容文字" : "Content text"} ); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return ( {zh ? "内容文字" : "Content text"} ); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Card } from "@workspace/ui/components/card"; Content text; ``` **Avoid** ```tsx import { Card } from "@workspace/ui/components/card"; Content text ; ``` ## Use concentric border radii When borders or rings are 8px or less apart, their corner radii must be mathematically concentric: outer radius = inner radius + padding. At the default SUI radius, `rounded-lg` is 10px, `rounded-xl` is 14px, and `p-1` is 4px. The radius scale uses fixed offsets: `xl` adds `0.25rem`, `2xl` adds `0.5rem`, `3xl` adds `0.75rem`, and `4xl` adds `1rem` to the base radius. Changing `--radius` preserves those gaps. `sm` and `md` subtract `0.25rem` and `0.125rem`, clamped at zero. Pair `rounded-xl` outside with `rounded-lg` inside and `p-1`; use `rounded-2xl` outside with `p-2`. For other padding values, calculate the outer radius as the inner radius plus that padding. The scale alone does not make arbitrary combinations concentric. Change the base radius below to compare the two examples. If the outer container uses a `border`, include its width in the distance between the two boxes. A `ring` does not occupy layout space. ### Example: design-guidelines-concentric-border-radius ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (
{zh ? "内容文字" : "Content text"}
); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (
{zh ? "内容文字" : "Content text"}
); } export default function Example({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [radius, setRadius] = useState(10); return (
{zh ? "基准圆角" : "Base radius"} {[0, 4, 10, 16].map((value) => ( ))}
} avoid={} />
); } import { Button } from "@workspace/ui/components/button"; import { type CSSProperties, useState } from "react"; ``` **Recommended** ```tsx
Content text
; ``` **Avoid** ```tsx
Content text
; ``` ## Align icons with the first line of text Inline icons must be optically the same size as and be center-aligned with text. Use `h-lh flex items-center` for multi-line alignment. The avoid column includes both a missing line-height wrapper and vertical centering against the entire paragraph. Decorative icons use `aria-hidden`; icon-only actions need an accessible name. ### Example: design-guidelines-icon-alignment ```tsx import { InfoIcon } from "lucide-react"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "这段文字可能会换行,但图标仍应与第一行保持对齐" : "Text that may wrap onto multiple lines and still align with the icon."}

); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "这段文字可能会换行,但图标仍应与第一行保持对齐" : "Text that may wrap onto multiple lines and still align with the icon."}

); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { InfoIcon } from "lucide-react";

Text that may wrap onto multiple lines and still align with the icon.

; ``` **Avoid** ```tsx import { InfoIcon } from "lucide-react";

Text that may wrap onto multiple lines and still align with the icon.

; ``` ```tsx import { InfoIcon } from "lucide-react";
; ``` ## Reduce the font size of inline monospaced text Monospaced text should have a slightly smaller font size (~0.9em) when mixed with regular text. ### Example: design-guidelines-inline-monospace-size ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "编辑 " : "Edit "} config.ts {zh ? " 后继续" : " to continue."}

); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "编辑 " : "Edit "} config.ts {zh ? " 后继续" : " to continue."}

); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx

Edit config.ts to continue.

; ``` **Avoid** ```tsx

Edit config.ts to continue.

; ``` ## Use a border to separate sticky elements Use `border` to separate sticky elements from the content. Scroll each column to inspect the boundary under its sticky header. A separating border is appropriate here because the header has no drop shadow. ### Example: design-guidelines-sticky-borders ```tsx import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (
{zh ? "最近请求" : "Recent requests"}
{Array.from({ length: 8 }, (_, index) => ({ id: `request-${index + 1}`, number: index + 1, })).map((request) => (

{zh ? "请求" : "Request"} {request.number}

))}
); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (
{zh ? "最近请求" : "Recent requests"}
{Array.from({ length: 8 }, (_, index) => ({ id: `request-${index + 1}`, number: index + 1, })).map((request) => (

{zh ? "请求" : "Request"} {request.number}

))}
); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx
Recent requests
{Array.from({ length: 8 }, (_, index) => ({ id: `request-${index + 1}`, number: index + 1, })).map((request) => (

Request {request.number}

))}
; ``` **Avoid** ```tsx
Recent requests
{Array.from({ length: 8 }, (_, index) => ({ id: `request-${index + 1}`, number: index + 1, })).map((request) => (

Request {request.number}

))}
; ``` ## Maintain content size during collapse animations Collapsible content must maintain its content size while closing to avoid its content shifting during animations. Toggle each panel and watch the paragraph. Animate the outer wrapper from 256px to 0 while keeping the recommended inner content at `w-64`; the avoid example shrinks the text container and changes its line breaks. Reduced motion removes the transition. ### Example: design-guidelines-collapse-content-size ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { useReducedMotion } from "@workspace/ui/hooks/use-reduced-motion"; import { motion } from "motion/react"; import { useState } from "react"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [open, setOpen] = useState(true); const reduce = useReducedMotion(); return (
{zh ? "面板关闭时,这段文字应保持相同的换行,不要在动画过程中重新排版" : "Text should keep the same line breaks while this panel closes."}
); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [open, setOpen] = useState(true); const reduce = useReducedMotion(); return (
{zh ? "面板关闭时,这段文字应保持相同的换行,不要在动画过程中重新排版" : "Text should keep the same line breaks while this panel closes."}
); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Button } from "@workspace/ui/components/button"; import { useReducedMotion } from "@workspace/ui/hooks/use-reduced-motion"; import { motion } from "motion/react"; import { useState } from "react"; export function RecommendedExample() { const [open, setOpen] = useState(true); const reduce = useReducedMotion(); return (
Text should keep the same line breaks while this panel closes.
); } ``` **Avoid** ```tsx import { Button } from "@workspace/ui/components/button"; import { useReducedMotion } from "@workspace/ui/hooks/use-reduced-motion"; import { motion } from "motion/react"; import { useState } from "react"; export function AvoidExample() { const [open, setOpen] = useState(true); const reduce = useReducedMotion(); return (
Text should keep the same line breaks while this panel closes.
); } ``` ## Never stack elevated cards Never stack elevated cards on top of one another. Use a plain container, spacing, or separators to group content inside a card. The recommended heading sits outside the elevated surface. ### Example: design-guidelines-layer-card-nesting ```tsx import { Card } from "@workspace/ui/components/card"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "最近请求" : "Recent requests"}

{zh ? "请求数据" : "Request data"}
); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; return (

{zh ? "最近请求" : "Recent requests"}

{zh ? "请求数据" : "Request data"}
); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Card } from "@workspace/ui/components/card";

Recent requests

Request data
; ``` **Avoid** ```tsx import { Card } from "@workspace/ui/components/card";

Recent requests

Request data
; ``` ## Never conditionally render dialogs Conditionally rendering dialogs disables their open/close animation. Use the `open` prop to determine if a dialog should be visible or not. Open and close both dialogs. The recommended `Dialog` remains in the React tree while Base UI manages the popup’s presence, exit animation, focus, and dismissal. In the avoid example, setting `open` to false immediately removes the entire dialog tree. Always include a title and description. ### Example: design-guidelines-dialog-rendering ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@workspace/ui/components/dialog"; import { useState } from "react"; import { DesignComparison } from "../design-guidelines-comparison"; import type { ExampleProps } from "../types"; function RecommendedSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [open, setOpen] = useState(false); return ( }> {zh ? "打开弹窗" : "Open dialog"} {zh ? "编辑项目" : "Edit project"} {zh ? "更新此项目的设置" : "Update this project’s settings."} }> {zh ? "关闭" : "Close"} ); } function AvoidSample({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [open, setOpen] = useState(false); return ( <> {open && ( {zh ? "编辑项目" : "Edit project"} {zh ? "更新此项目的设置" : "Update this project’s settings."} }> {zh ? "关闭" : "Close"} )} ); } export default function Example({ locale }: ExampleProps) { return ( } avoid={} /> ); } ``` **Recommended** ```tsx import { Button } from "@workspace/ui/components/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@workspace/ui/components/dialog"; import { useState } from "react"; export function RecommendedExample() { const [open, setOpen] = useState(false); return ( }> Open dialog Edit project Update this project’s settings. }>Close ); } ``` **Avoid** ```tsx import { Button } from "@workspace/ui/components/button"; import { Dialog, DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@workspace/ui/components/dialog"; import { useState } from "react"; export function AvoidExample() { const [open, setOpen] = useState(false); return ( <> {open && ( Edit project Update this project’s settings. }> Close )} ); } ``` --- # Installation Install editable SUI source with the shadcn CLI. Page: https://sui.draco.dev/docs/installation Install SUI source through its shadcn registry. Components, styles, and dependencies integrate with your application configuration. ## Install through the registry ### Prepare the consuming application Use React 19, Tailwind CSS 4, and a shadcn project configured for Base UI. In an existing application that has not initialized shadcn, run: ```bash bunx --bun shadcn@latest init --base base ``` Merge this entry into the application's existing `components.json`. Preserve its CSS path and aliases: ```json { "registries": { "@sui": "https://raw.githubusercontent.com/draco-china/sui/main/registry/r/{name}.json" } } ``` The catalog URL is `https://raw.githubusercontent.com/draco-china/sui/main/registry/r/registry.json`. Individual manifests use the same directory. ### Search, inspect, and install Run the following commands from the consuming application directory: ```bash bunx --bun shadcn@latest search @sui -q button bunx --bun shadcn@latest view @sui/button bunx --bun shadcn@latest add @sui/button @sui/editor ``` Install the business blocks by their registry names: ```bash bunx --bun shadcn@latest add @sui/data-table @sui/delete-resource @sui/tanstack-form ``` To install the complete collection, including all components and blocks: ```bash bunx --bun shadcn@latest add @sui/sui ``` Each manifest includes the item's local component dependencies, hooks, helpers, types, package dependencies, and SUI CSS and theme tokens. The CLI installs source into the directories configured by your aliases and merges styles into the configured Tailwind CSS file. Standalone entries `@sui/sui-style` and `@sui/sui-theme` provide the shared styles and theme helpers. Review the generated files and CSS when adding SUI to an application that already has components with the same names or custom theme values. Preview an update before applying it: ```bash bunx --bun shadcn@latest add @sui/button --dry-run bunx --bun shadcn@latest add @sui/button --diff ``` Editor uses locally bundled Monaco workers and requires a Vite-compatible `?worker` loader. Keep the installed `worker.d.ts` in your TypeScript include paths. This also applies when installing the complete collection. Blocks default to English. Pass labels from your application's locale configuration to customize their visible text; see the [Data Table](/docs/blocks/data-table), [Delete Resource](/docs/blocks/delete-resource), and [TanStack Form](/docs/blocks/tanstack-form) references. ### Import installed source With the default shadcn aliases, import from your own application: ```tsx import { Button } from "@/components/ui/button"; export function SaveButton() { return ; } ``` Use the actual aliases in your `components.json` if they differ. Examples throughout this reference use workspace imports such as `@workspace/ui/components/button`. For a registry installation, replace `@workspace/ui/components/*` with your `ui` alias, `@workspace/ui/blocks/*` with your `components` alias followed by `/blocks/*`, and hook or helper imports with your `hooks` or `lib` alias. For example, the default aliases use `@/components/ui/button`, `@/components/blocks/data-table`, and `@/hooks/use-mobile`. The CLI adapts imports inside the installed files automatically. ## Connect the shadcn MCP server The [MCP guide](/docs/mcp) covers client configuration, project working directories, example requests, and troubleshooting. MCP shares this application's `@sui` registry configuration. For API documentation and complete example text, use [LLMs](/docs/llms-txt). See [Theming](/docs/theming) for CSS tokens and [Compatibility](/docs/compatibility) for runtime requirements. --- # Introduction Start with a component. Build an interface that feels like yours. Page: https://sui.draco.dev/docs SUI is a collection of composable React components built with Base UI and Tailwind CSS 4. It brings form controls, navigation, feedback, and content viewers into one visual language. The source lives in `packages/ui`, where you can read it, change it, and compose it for your application. ## Start here 1. **Connect your application.** Follow [Installation](/docs/installation) to add the workspace dependency, load the shared styles, and configure Tailwind to scan the component source. 2. **Try a component.** Browse the [component directory](/docs/components). Each page pairs an interactive preview with the complete example source. 3. **Make it your own.** Use [Theming](/docs/theming) for colors and CSS tokens, and [Design guidelines](/docs/design-guidelines) for typography, spacing, surfaces, and motion. SUI currently uses the private `@workspace/ui` package inside this Bun workspace. There is no published SUI registry or standalone install command. Start with the installation guide before copying an example. ## Your first component Once your application is connected, import components from their individual modules: ```tsx import { Button } from "@workspace/ui/components/button"; export function SaveButton() { return ; } ``` The previews and your application use the same shared package. The code tab shows the full example file, including imports and composition; English and Chinese pages share the same examples. ## Find the right pieces | What you are building | Start with | | --- | --- | | Forms and settings | [Field](/docs/components/field), [Input](/docs/components/input), [Select](/docs/components/select), [Switch](/docs/components/switch) | | Navigation and feedback | [Tabs](/docs/components/tabs), [Dialog](/docs/components/dialog), [Toast](/docs/components/toast) | | Content and conversations | [Message](/docs/components/message), [Markdown Viewer](/docs/components/markdown-viewer), [Code Viewer](/docs/components/code-viewer) | | Optional glass surfaces | [Glass](/docs/components/glass) | ## Compose with intent Components expose named parts. A dialog combines a trigger, content, title, description, and actions. A field combines a label, control, description, and error. Keep related parts together to preserve their behavior and give each interaction a clear purpose. Base UI primitives provide keyboard interactions, focus management, and ARIA semantics. Your application still supplies meaningful labels, useful descriptions, and understandable feedback. Test custom compositions with a keyboard and assistive technology. ## Work with the source - `packages/ui` contains components, shared hooks, CSS tokens, and accent themes. - `apps/docs` contains the bilingual reference site and runnable examples. - [CSS utilities](/docs/utils) cover shared effects such as Scroll Fade and Shimmer. For coding assistants, [llms.txt](/llms.txt) provides the documentation index and [llms-full.txt](/llms-full.txt) includes the full reference and example source. Individual pages also offer Markdown through the page actions. --- # LLMs Read SUI documentation and complete examples as plain text. Page: https://sui.draco.dev/docs/llms-txt Use the LLM endpoints to give an assistant the current SUI APIs and examples. These endpoints read documentation. Use [MCP](/docs/mcp) or the [CLI](/docs/cli) when you want to install component source. ## Documentation endpoints | Content | English | 简体中文 | | --- | --- | --- | | Index of pages | [`/llms.txt`](/llms.txt) | [`/zh-CN/llms.txt`](/zh-CN/llms.txt) | | Complete reference | [`/llms-full.txt`](/llms-full.txt) | [`/zh-CN/llms-full.txt`](/zh-CN/llms-full.txt) | | One component | [Button Markdown](/api/llms-markdown?locale=en-US&slug=components/button) | [Button Markdown](/api/llms-markdown?locale=zh-CN&slug=components/button) | Use these paths on the current documentation origin. GitHub raw hosts the [registry](/docs/registry) JSON, while the documentation site supplies these text endpoints. The index groups guides, components, Blocks, and utilities and links to their Markdown. Start with the index and read the relevant pages. The complete reference includes every page and can be much larger than one assistant request needs. ## Read one page `/api/llms-markdown` accepts `locale` and `slug`: | Parameter | Values | Default | | --- | --- | --- | | `locale` | `en-US`, `zh-CN` | `en-US` | | `slug` | Document path without `/docs/`, such as `components/button`, `blocks/data-table`, or `theming` | Introduction when omitted | Unknown locales and documents return `404`. An explicit `/en-US/llms.txt` or `/en-US/llms-full.txt` redirects to the corresponding English path without the prefix. [Read Sensitive Input Markdown](/api/llms-markdown?locale=en-US&slug=components/sensitive-input) The result includes the title, description, page URL, API text, and complete source for the page's shared examples. Preview and Code read the same example files, so the text reference stays aligned with the runnable documentation. ## Copy from a document The page header provides **Copy Markdown** and **Open**. Copy Markdown returns the original MDX document; Open can supply the expanded LLM Markdown to an assistant. For programmatic use, `/api/markdown` returns the original document and `/api/llms-markdown` expands shared examples into code fences. ## Example request ```text Read SUI's llms.txt, then the Field and Sensitive Input pages. Compose a labeled secret input with an external validation message. Keep validation and translated feedback in my application. Use the import aliases from my components.json when installing source. ``` Examples use workspace imports such as `@workspace/ui/components/button`. After source installation, use your application's configured aliases; the [installation guide](/docs/installation#import-installed-source) explains the mapping. Reading documentation does not install files or configure your MCP client. --- # MCP Connect AI assistants to the SUI registry. Page: https://sui.draco.dev/docs/mcp Use the official [shadcn MCP server](https://ui.shadcn.com/docs/mcp) to browse the SUI registry from your AI assistant. The server runs locally over stdio and reads the consuming application's `components.json`. SUI supplies registry JSON files; the registry URL is an HTTP data source, not an MCP endpoint. For API guidance and complete usage examples, provide the assistant with [LLMs documentation](/docs/llms-txt). MCP queries and inspects installable source. Its `get_add_command_for_items` tool returns a CLI command; the assistant then runs the command in your application to install the files. ## Configure the application Prepare the consuming application using [Installation](/docs/installation). Merge this registry entry into its existing `components.json`, preserving the application's aliases and CSS configuration: ```json { "registries": { "@sui": "https://raw.githubusercontent.com/draco-china/sui/main/registry/r/{name}.json" } } ``` The catalog is `https://raw.githubusercontent.com/draco-china/sui/main/registry/r/registry.json`. CLI and MCP share this configuration. Open the consuming application in your MCP client and start the server from the directory containing its `components.json`. In a monorepo, use the application directory rather than a root directory without that file. The same configuration works with the [CLI](/docs/cli). ## Configure the client These examples use Bun to launch the official server: ```bash bunx --bun shadcn@latest mcp ``` The MCP client starts this process and communicates over stdin/stdout. Merge the relevant configuration into your project's existing client file, then restart or enable the server. Bun must be available in the client process's environment. ### Claude Code Use the project's `.mcp.json`: ```json { "mcpServers": { "shadcn": { "command": "bunx", "args": ["--bun", "shadcn@latest", "mcp"] } } } ``` Restart Claude Code and use `/mcp` to check the connection. ### Cursor Use the project's `.cursor/mcp.json`: ```json { "mcpServers": { "shadcn": { "command": "bunx", "args": ["--bun", "shadcn@latest", "mcp"] } } } ``` Enable the shadcn server in Cursor's MCP settings and check that its tools are listed. ### VS Code For GitHub Copilot, use the project's `.vscode/mcp.json`. VS Code uses the `servers` key: ```json { "servers": { "shadcn": { "command": "bunx", "args": ["--bun", "shadcn@latest", "mcp"] } } } ``` Open the file and start the server through VS Code's MCP controls. The client file locations and keys follow the [official shadcn configuration guide](https://ui.shadcn.com/docs/mcp#configuration). ### Codex Merge this into the consuming application's `.codex/config.toml`. Replace the placeholder with the application's absolute directory: ```toml [mcp_servers.shadcn] command = "bunx" args = ["--bun", "shadcn@latest", "mcp"] cwd = "/absolute/path/to/app" ``` Codex loads project configuration for trusted projects. Open or restart the project after editing and verify that the server's tools are available. See the official OpenAI documentation for [project configuration](https://learn.chatgpt.com/docs/config-file/config-basic) and [MCP settings](https://learn.chatgpt.com/docs/extend/mcp?surface=cli). ## Browse, inspect, and install Ask the assistant to use the `@sui` namespace explicitly: > Search @sui for table components, inspect @sui/data-table and its dependencies, then install it in this application and adapt the documentation example to my aliases. The server exposes these tools for the workflow: | Tool | Purpose | | --- | --- | | `get_project_registries` | Confirm that the application has configured `@sui`. | | `list_items_in_registries` | List entries in `@sui`. | | `search_items_in_registries` | Search names and descriptions in `@sui`. | | `view_items_in_registries` | Inspect an item's manifest and file contents. | | `get_add_command_for_items` | Return the CLI command for the selected items. | For example, search with `registries: ["@sui"]` and `query: "table"`, then inspect `items: ["@sui/data-table"]`. After obtaining the add command, the assistant runs it from the consuming application directory. With Bun, the equivalent command is: ```bash bunx --bun shadcn@latest add @sui/data-table ``` Review the installed files, styles, and dependencies. See [CLI](/docs/cli) for previewing changes and [Compatibility](/docs/compatibility) for runtime requirements. SUI's catalog currently contains installable components, blocks, styles, and helpers, without separate demo or example entries. Use [LLMs documentation](/docs/llms-txt) for complete examples and API guidance rather than expecting MCP's example search to return `@sui/*-demo` items. ## Troubleshooting | Symptom | Check | | --- | --- | | GitHub registry returns 404 | Check the configured URL, access to the repository, and whether the requested item exists. | | Unknown registry or missing `@sui` | Check the namespace in the consuming application's `components.json`, then call `get_project_registries`. | | Server reads a different project | Check its working directory. It must point to the consuming application containing `components.json`; restart after changing it. | | `bunx` is not found | Ensure the client process can find Bun in `PATH`, or use the absolute path to the `bunx` executable in its server configuration. | | Tools appear but installation does not happen | `get_add_command_for_items` returns a command. The assistant still needs to execute it in the application. | To isolate registry problems, run these commands from the same application directory as MCP: ```bash bunx --bun shadcn@latest search @sui -q button bunx --bun shadcn@latest view @sui/button ``` See [Registry](/docs/registry) for configuration and item contents, and [CLI](/docs/cli) for installation and updates. --- # Registry Understand SUI manifests, shared styles, and GitHub distribution. Page: https://sui.draco.dev/docs/registry The SUI registry distributes editable component source through the [CLI](/docs/cli) and [MCP](/docs/mcp). Configure the `@sui` namespace as described in [installation](/docs/installation). ## GitHub files ```json { "registries": { "@sui": "https://raw.githubusercontent.com/draco-china/sui/main/registry/r/{name}.json" } } ``` `registry/r/registry.json` is the searchable catalog. `registry/r/button.json`, for example, contains the installation payload for Button. Names do not have a locale prefix; installed components use English defaults and accept application-owned text. | Item | Content | | --- | --- | | Component name, such as `button` | Component, transitive local files, dependencies, CSS, and theme tokens | | `data-table`, `delete-resource`, `tanstack-form` | A Block and its required source and styles | | `sui-style` | Shared theme and component CSS | | `sui-theme` | Theme helpers and shared styles | | `sui` | All components and Blocks | ## Files and dependencies Manifests use shadcn's registry schema. `dependencies` and `devDependencies` declare package versions. `files` contains source and targets resolved against application aliases; `css` and `cssVars` merge into the configured stylesheet. Each item is self-contained, including local hooks, helpers, declarations, and other components it imports. This avoids depending on a consumer's namespace or accidentally resolving a same-named component from another registry. The catalog describes items without duplicating their source; the individual payloads carry full content. The CLI rewrites imports to your aliases. Workspace imports in documentation are explained in the [import mapping](/docs/installation#import-installed-source). Installed theme values and source are editable; review their differences when adding SUI to an existing design system. ## Update installed source Preview changes with the [CLI](/docs/cli#review-an-update) before updating components and shared dependencies. Installation copies source; files do not automatically follow library changes. Keep your application's package-manager lockfile and review styles alongside local edits. LLM documentation lives on the documentation site's [text endpoints](/docs/llms-txt). Registry JSON supplies installation data; an MCP client starts the shadcn stdio server separately. --- # Theming Semantic colors, light and dark surfaces, and reusable palettes. Page: https://sui.draco.dev/docs/theming SUI colors describe the role of an element. A card uses `card` and `card-foreground`; a primary action uses `primary` and `primary-foreground`. The same component adapts when its palette or appearance changes. Shared CSS color variables use OKLCH, including the seven presets and custom palette output. These values preserve the existing colors. Custom input and preset seeds remain HEX, and contrast is measured using sRGB relative luminance. The shared package provides general semantic variables; this documentation app uses `accent-foreground` for links and `ring` for focus directly. ## Semantic usage Choose a token by purpose, then keep the surface and its matching foreground together. Use the component's built-in variant when it already expresses that purpose. **Recommended** ```tsx import { Button } from "@workspace/ui/components/button";

Your changes are ready.

; ``` **Avoid for product UI** ```tsx

Your changes are ready.

; ``` Fixed colors can be appropriate for illustrations or external brand assets. Product surfaces, controls, and text should follow semantic tokens so they stay consistent across themes. SUI does not add a lint rule that forbids primitive colors. ## Surface hierarchy Start with the page canvas and layer content using the surface that matches its role. The default theme uses an Apple-inspired gray canvas, white surfaces, and blue actions; dark mode uses deep gray surfaces and brighter blue accents. Inter remains the font. | Role | Surface | Matching text | Typical use | | --- | --- | --- | --- | | Page | `bg-background` | `text-foreground` | Application canvas | | Card | `bg-card` | `text-card-foreground` | Grouped content | | Floating | `bg-popover` | `text-popover-foreground` | Menus and popovers | | Secondary | `bg-secondary` | `text-secondary-foreground` | Secondary controls | | Quiet | `bg-muted` | `text-muted-foreground` | Supporting content | | Selected | `bg-accent` | `text-accent-foreground` | Selection and hover | Surfaces express purpose rather than a fixed brightness ladder. `card` and `popover` may share a value while preserving different roles. ## Primary and status Use `primary` for the main action and `primary-foreground` for its text. Use `accent` for selected or hovered items. Links use `accent-foreground`, which can differ from the primary seed to maintain text contrast on neutral surfaces. Selection colors also apply to pressed Toggle and ToggleGroup items, Command highlights, current navigation links, selected options and menu items, selected table rows, choice cards, and calendar ranges. Filled selections, including Tabs, current pagination links, checkboxes, and radio buttons, use `primary` with `primary-foreground`. Focus indicators use `ring`. Use `destructive` for destructive actions and invalid input. Its color remains independent of the selected accent. SUI does not define separate success, warning, or information color tokens; express those states with clear text and icons, or define application-specific semantic tokens. ```tsx import { Button } from "@workspace/ui/components/button"; ; ``` ## Text Pair foreground tokens with their intended surfaces. `foreground` is the default body color; `muted-foreground` supports descriptions and secondary labels. A lighter shade alone should not communicate disabled, invalid, or selected state: retain accessible labels and the control's state attributes. ## Borders and focus `border` separates surfaces. Existing Input uses `bg-input/50` for its fill. `ring` marks keyboard focus. Keep the outline visible when customizing interactive elements. ```tsx Explore colors ; ``` ## Charts Use `chart-1` through `chart-5` for data series. Every preset supplies five coordinated colors. These colors distinguish series; add legends, labels, or patterns instead of relying only on hue. Chart colors are not substitutes for text or status tokens. ## Token reference Browse the actual default tokens by role. Each row shows its utility, its light and dark value, and buttons to copy the variable, utility, or value. The catalog reads `globals.css`; it does not maintain a separate color table. ### Example: theming-semantic-colors ```tsx import { Button } from "@workspace/ui/components/button"; import { InlineCopyText } from "@workspace/ui/components/inline-copy-text"; import { useState } from "react"; import { previewTokens, tokenValue } from "../support/theming-tokens"; import type { ExampleProps } from "../types"; const groups = [ { en: "Surfaces", zh: "表面", tokens: [ ["background", "bg-background", "Page canvas", "页面画布"], ["card", "bg-card", "Card surface", "卡片表面"], ["popover", "bg-popover", "Floating surface", "浮层表面"], ["secondary", "bg-secondary", "Secondary control", "次要控件"], ["muted", "bg-muted", "Quiet surface", "低强调表面"], ["accent", "bg-accent", "Selection and hover", "选中与悬停"], ], }, { en: "Text", zh: "正文", tokens: [ ["foreground", "text-foreground", "Page text", "页面正文"], ["card-foreground", "text-card-foreground", "Text on card", "卡片正文"], [ "popover-foreground", "text-popover-foreground", "Text on floating surface", "浮层正文", ], [ "secondary-foreground", "text-secondary-foreground", "Text on secondary control", "次要控件正文", ], [ "muted-foreground", "text-muted-foreground", "Supporting text", "辅助文字", ], [ "accent-foreground", "text-accent-foreground", "Text on selection", "选中态正文", ], ], }, { en: "Primary", zh: "主色", tokens: [ ["primary", "bg-primary", "Primary action", "主要操作"], [ "primary-foreground", "text-primary-foreground", "Text on primary action", "主要操作正文", ], ], }, { en: "Borders and focus", zh: "边框与焦点", tokens: [ ["border", "border-border", "Surface boundary", "表面边界"], ["input", "bg-input/50", "Input fill", "输入控件填充"], ["ring", "ring-ring", "Keyboard focus", "键盘焦点"], ], }, { en: "Status", zh: "状态", tokens: [ [ "destructive", "text-destructive", "Destructive action or invalid input", "危险操作或无效输入", ], ], }, { en: "Charts", zh: "图表", tokens: [1, 2, 3, 4, 5].map((index) => [ `chart-${index}`, `fill-chart-${index}`, `Data series ${index}`, `数据系列 ${index}`, ]), }, { en: "Sidebar", zh: "侧栏", tokens: [ ["sidebar", "bg-sidebar", "Sidebar surface", "侧栏表面"], [ "sidebar-foreground", "text-sidebar-foreground", "Sidebar text", "侧栏正文", ], [ "sidebar-primary", "bg-sidebar-primary", "Sidebar primary action", "侧栏主要操作", ], [ "sidebar-primary-foreground", "text-sidebar-primary-foreground", "Text on sidebar action", "侧栏主要操作正文", ], [ "sidebar-accent", "bg-sidebar-accent", "Sidebar selected item", "侧栏选中项", ], [ "sidebar-accent-foreground", "text-sidebar-accent-foreground", "Selected sidebar text", "侧栏选中项正文", ], [ "sidebar-border", "border-sidebar-border", "Sidebar boundary", "侧栏边界", ], [ "sidebar-ring", "ring-sidebar-ring", "Sidebar keyboard focus", "侧栏键盘焦点", ], ], }, ]; export default function Example({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const stateLabels = zh ? { dark: "深色", light: "浅色", } : { dark: "Dark", light: "Light", }; const [selected, setSelected] = useState(0); const group = groups[selected] ?? groups[0]; const labels = { copy: zh ? "复制" : "Copy", copied: zh ? "已复制" : "Copied", failed: zh ? "复制失败,请手动复制" : "Copy failed. Copy the text manually.", }; return (
{zh ? "颜色用途" : "Color roles"} {groups.map((item, index) => ( ))}
    {group?.tokens.map(([token, utility, en, cn]) => (
  • {zh ? cn : en}

    {`--${token}`}
    {utility ?? ""}
    {[false, true].map((dark) => { const value = tokenValue( previewTokens("default", dark), `--${token}`, ); return (
    ); })}
  • ))}
); } ``` ## Appearance Set `dark` on a parent to use dark tokens. Without that class, the application uses light tokens. Mode and palette are separate choices: `dark` controls appearance, while `data-color` selects a palette. ```html ``` The following two panels use the same SUI components with isolated default light and dark variables. Edit the field, toggle the switch, or save changes to try the controls. ### Example: theming-mode-comparison ```tsx import { cn } from "cn"; import { ThemePreview } from "../support/theming-preview"; import { previewStyle } from "../support/theming-tokens"; import type { ExampleProps } from "../types"; export default function Example({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const stateLabels = zh ? { dark: "深色", light: "浅色", } : { dark: "Dark", light: "Light", }; return (
{[false, true].map((dark) => (

{dark ? stateLabels.dark : stateLabels.light}

))}
); } ``` For the whole documentation site, open the appearance panel and choose **Light**, **Dark**, or **System**. System is the default and follows the device preference. ## Preset palettes Choose **Default** or one of seven presets: Bamboo, Mauve, Mist, Sand, Pine, Rose, and Lime. Default uses the base tokens in `globals.css`. Each preset has a complete five-color palette, with the colors mapped in order to `chart-1` through `chart-5`. Its primary seed controls `primary` independently; it does not have to be the first chart color. The cards below show the full five-color palette, copyable color values, and the same interactive component preview for every theme. Use the shared light/dark control to compare them under the same appearance. The default theme has its own complete card. Surfaces, body text, and destructive colors keep their roles when the accent changes. Primary actions, selected states, focus rings, sidebar accents, and charts follow the palette. Accessible foreground and focus colors can differ from the seed to maintain contrast. These previews stay local and do not change the site theme or browser storage. ### Example: theming-palette-playground ```tsx import { Button } from "@workspace/ui/components/button"; import { InlineCopyText } from "@workspace/ui/components/inline-copy-text"; import { themePresets } from "@workspace/ui/lib/theme/theme"; import { cn } from "cn"; import { useState } from "react"; import { ThemePreview } from "../support/theming-preview"; import { previewStyle, previewTokens, tokenValue, } from "../support/theming-tokens"; import type { ExampleProps } from "../types"; const palettes = [ { id: "default" as const, en: "Default", zh: "默认" }, ...themePresets, ]; export default function Example({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const [dark, setDark] = useState(false); const mode = zh ? { dark: "深色预览", light: "浅色预览" } : { dark: "Dark preview", light: "Light preview" }; return (

{zh ? "每套配色包含五个协调色" : "Each palette contains five coordinated colors"}

{palettes.map((preset) => { const tokens = previewTokens(preset.id, dark); const colors = "palette" in preset ? preset.palette : [1, 2, 3, 4, 5].map((index) => tokenValue(tokens, `--chart-${index}`), ); return (

{zh ? preset.zh : preset.en}

{"color" in preset && ( {preset.color} )}
{colors.map((color, index) => (
))}
); })}

{zh ? "明暗切换仅作用于这些预览,不改变整站主题或浏览器存储" : "Appearance switches affect these previews without changing the site theme or browser storage."}

); } ``` Lime uses a bright `#D5F267` primary with black text. Accessible links and focus rings use deeper olive shades in light mode; dark mode keeps the lime accents bright against neutral dark surfaces. Choose **Lime** in the theme panel or set `data-color="lime"`. ## Custom HEX The preview and the site theme panel accept three- or six-digit HEX values, with or without `#`. Shorthand expands to six digits. Invalid input displays an error and preserves the current valid theme. The primary seed stays unchanged. The shared calculation chooses black or white primary text and adjusts links, selected text, and focus colors against the light and dark surfaces. It checks ordinary text at 4.5:1 and focus colors at 3:1. Use the matching foreground token rather than assuming white text works on every primary color. ### Example: theming-custom-color ```tsx import { Button } from "@workspace/ui/components/button"; import { ColorPicker } from "@workspace/ui/components/color-picker"; import { Input } from "@workspace/ui/components/input"; import { Label } from "@workspace/ui/components/label"; import { normalizeHex } from "@workspace/ui/lib/theme/theme"; import { cn } from "cn"; import { useId, useState } from "react"; import { colorPickerLabels } from "../../lib/color-picker-labels"; import { ThemePreview } from "../support/theming-preview"; import { previewStyle } from "../support/theming-tokens"; import type { ExampleProps } from "../types"; export default function Example({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const id = useId(); const [dark, setDark] = useState(false); const [input, setInput] = useState("#0066CC"); const [custom, setCustom] = useState("#0066CC"); const [invalid, setInvalid] = useState(false); const mode = zh ? { dark: "深色预览", light: "浅色预览" } : { dark: "Dark preview", light: "Light preview" }; return (
{ event.preventDefault(); const next = normalizeHex(input); setInvalid(next === null); if (next) { setCustom(next); setInput(next); } }} >
setInput(event.target.value)} aria-invalid={invalid} aria-describedby={invalid ? `${id}-error` : undefined} />
{ setCustom(next); setInput(next); setInvalid(false); }} labels={colorPickerLabels(locale)} /> {invalid && ( )}
); } ``` ## Shared theme files Import the UI stylesheet to include the default tokens, component styles, and all seven presets: ```css @import "@workspace/ui/globals.css"; ``` The seven files live in `packages/ui/src/styles/themes`: `bamboo.css`, `mauve.css`, `mist.css`, `sand.css`, `pine.css`, `rose.css`, and `lime.css`. A setup that already loads SUI base tokens can import an individual preset: ```css @import "@workspace/ui/themes/pine.css"; ``` Select it on the root or a container with `data-color="pine"`. A dark ancestor enables its dark palette. To reset the root, use `data-color="default"` or remove the attribute and clear custom inline tokens. The default comes directly from `globals.css`; it has no separate theme file. A nested container without its own overrides inherits its parent's colors. ## Custom application themes Use the shared helpers for custom input. Validate first, clear previous inline accent tokens, and calculate overrides only for a custom seed: ```tsx import { createThemeTokens, getThemeId, normalizeHex, themeTokenNames, } from "@workspace/ui/lib/theme/theme"; function applyTheme(input: string | null, dark: boolean) { const seed = input === null ? null : normalizeHex(input); if (input !== null && seed === null) return; const root = document.documentElement; root.classList.toggle("dark", dark); root.dataset.color = getThemeId(seed); for (const name of themeTokenNames) root.style.removeProperty(name); if (root.dataset.color === "custom") { for (const [name, value] of Object.entries(createThemeTokens(seed, dark))) { root.style.setProperty(name, value); } } } ``` Pass `null` to restore Default. Recalculate custom overrides when appearance changes. The shared package supplies colors and calculation; your application owns mode selection, persistence, and initial restoration. Use `text-accent-foreground` for accessible links and `outline-ring` for focus. The documentation stylesheet maps Fumadocs's `--color-fd-*` variables to the shared semantic tokens; these framework aliases are not part of the shared theme files or `createThemeTokens` output. ## Creating a theme For a reusable preset, add its stable ID, bilingual name, seed, and five chart colors to `themePresets` in `packages/ui/src/lib/theme/theme.ts`. The generator combines that definition with the current base semantic tokens in `globals.css`. ```sh bun run --cwd packages/ui themes:generate ``` Import the resulting individual file in `globals.css`, then use its ID with `data-color`. Regenerate after changing a preset or the base tokens. Review both modes, matching foregrounds, keyboard focus, and chart legends before adopting a palette. ## Persistence The header provides separate Appearance and Accent color controls. Appearance selects Light, Dark, or System; Accent color selects a preset palette or custom color. Changing the mode preserves the accent. The documentation site saves its selected mode and accent in browser storage, restores them before the first paint, and responds to device changes while System is active. When storage is unavailable, it uses System and Default. The interactive examples on this page do not write those preferences. ## Right-to-left interfaces Wrap the relevant subtree in `DirectionProvider` and prefer logical spacing utilities: ```tsx import { DirectionProvider } from "@workspace/ui/components/direction"; ; ``` --- # Components Browse all 67 SUI components, listed alphabetically. Page: https://sui.draco.dev/docs/components Every preview runs the components in this workspace. Choose a component to find its import, composition, examples, and API. - [Accordion](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Faccordion): A vertically stacked set of interactive headings that each reveal a section of content. - [Alert](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Falert): Displays a callout for user attention. - [Alert Dialog](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Falert-dialog): A modal dialog that interrupts the user with important content and expects a response. - [Aspect Ratio](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Faspect-ratio): Displays content within a desired ratio. - [Attachment](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fattachment): Displays a file or image attachment with media, metadata, upload state, and actions. - [Autocomplete](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fautocomplete): Suggests matching options while keeping the input free to accept custom text. - [Avatar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Favatar): An image element with a fallback for representing the user. - [Badge](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fbadge): Displays a badge or a component that looks like a badge. - [Breadcrumb](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fbreadcrumb): Displays the path to the current resource using a hierarchy of links. - [Bubble](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fbubble): Displays conversational content in a message bubble. Supports variants, alignment, grouping, reactions, and collapsible content. - [Button](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fbutton): Displays a button or a component that looks like a button. - [Button Group](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fbutton-group): A container that groups related buttons together with consistent styling. - [Calendar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcalendar): A calendar component that allows users to select a date or a range of dates. - [Card](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcard): Displays a card with header, content, and footer. - [Carousel](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcarousel): A carousel with motion and swipe built using Embla. - [Chart](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fchart): Beautiful charts. Built using Recharts. Copy and paste into your apps. - [Checkbox](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcheckbox): A control that allows the user to toggle between checked and not checked. - [Clipboard Text](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fclipboard-text): A selectable text field with a separate copy action and async feedback. - [Code Viewer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcode-viewer): Syntax-highlighted code with folding, line highlights, copying, and streaming feedback. - [Collapsible](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcollapsible): An interactive component which expands/collapses a panel. - [ColorPicker](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcolor-picker): Choose colors using a saturation area, hue and opacity sliders, editable values and preset swatches. - [Combobox](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcombobox): Autocomplete input with a list of suggestions. - [Command](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcommand): Command menu for search and quick actions. - [Context Menu](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fcontext-menu): Displays a menu of actions triggered by a right click. - [Dialog](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fdialog): A window overlaid on either the primary window or another dialog window, rendering the content underneath inert. - [Diff Viewer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fdiff-viewer): A line-based code comparison with split and unified views. - [Direction](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fdirection): A provider component that sets the text direction for your application. - [Drawer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fdrawer): A drawer component for React. - [Dropdown Menu](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fdropdown-menu): Displays a menu to the user — such as a set of actions or functions — triggered by a button. - [Editor](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Feditor): A client-loaded code editor with preview, toolbar actions, and fullscreen modes. - [Empty](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fempty): Use the Empty component to display an empty state. - [Field](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ffield): Combine labels, controls, and help text to compose accessible form fields and grouped inputs. - [Glass](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fglass): SVG highlights and CSS frosted surfaces, progressively enhanced with WebGPU refraction. - [Hover Card](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fhover-card): For sighted users to preview content available behind a link. - [HTML Viewer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fhtml-viewer): An isolated iframe preview for HTML content. - [Image Viewer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fimage-viewer): A dialog image viewer with navigation, zoom, rotation, and panning. - [Inline Copy Text](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Finline-copy-text): Inline code that stays readable and copies its value when activated. - [Input](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Finput): A text input component for forms and user data entry with built-in styling and accessibility features. - [Input Group](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Finput-group): Add addons, buttons, and helper content to inputs. - [Input OTP](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Finput-otp): Individual code inputs with character filtering and asynchronous verification feedback - [Item](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fitem): A versatile component for displaying content with media, title, description, and actions. - [Kbd](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fkbd): Used to display textual user input from keyboard. - [Label](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Flabel): Renders an accessible label associated with controls. - [Loader](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Floader): Eighteen loading animations with accessible labels, adjustable speed, and reduced motion support. - [Locale Toggle](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Flocale-toggle): A controlled language button or selector without routing or persistence assumptions. - [LongText](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Flong-text): Truncated text with a full-text disclosure only when the content overflows. - [Markdown Viewer](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fmarkdown-viewer): Markdown rendering with tables, task lists, alerts, highlighted code, and sanitized HTML. - [Marker](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fmarker): Displays an inline status, system note, bordered row, or labeled separator in a conversation. - [Menubar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fmenubar): A visually persistent menu common in desktop applications that provides quick access to a consistent set of commands. - [Message](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fmessage): Displays a message in a conversation, with optional avatar, header, footer, and alignment. - [Message Scroller](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fmessage-scroller): A chat scroll container that anchors turns, opens saved transcripts, follows streamed responses, loads history without jumping, and jumps to any message. - [Native Select](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fnative-select): A styled native HTML select element with consistent design system integration. - [Navigation Menu](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fnavigation-menu): A collection of links for navigating websites. - [NavigationProgress](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fnavigation-progress): A controlled navigation loading bar with delayed start and completion feedback. - [Pagination](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fpagination): Compact data pagination with range information, page navigation, and page-size selection. - [Popover](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fpopover): Displays rich content in a portal, triggered by a button. - [Progress](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fprogress): Displays an indicator showing the completion progress of a task, typically displayed as a progress bar. - [QRCode](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fqr-code): A theme-aware QR code with loading feedback and optional reveal animation. - [Questionnaire](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fquestionnaire): A multi-step questionnaire with single-choice, multiple-choice, freeform, and skippable questions. - [Radio Group](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fradio-group): A set of checkable buttons—known as radio buttons—where no more than one of the buttons can be checked at a time. - [Resizable](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fresizable): Accessible resizable panel groups and layouts with keyboard support. - [Scroll Area](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fscroll-area): Augments native scroll functionality for custom, cross-browser styling. - [Select](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fselect): Displays a list of options for the user to pick from—triggered by a button. - [Sensitive Input](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fsensitive-input): A sensitive-value input with a fixed mask, click-to-reveal interaction, and copy feedback. - [Separator](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fseparator): Visually or semantically separates content. - [Sheet](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fsheet): Extends the Dialog component to display content that complements the main content of the screen. - [Sidebar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fsidebar): A composable, themeable and customizable sidebar component. - [Skeleton](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fskeleton): Use to show a placeholder while content is loading. - [Slider](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fslider): An input where the user selects a value from within a given range. - [Switch](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Fswitch): A control that allows the user to toggle between checked and not checked. - [TabBar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftab-bar): Controlled application navigation with press-and-slide selection and optional glass material. - [Table](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftable): A responsive table component. - [Tabs](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftabs): A set of layered sections of content—known as tab panels—that are displayed one at a time. - [Tag Input](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftag-input): A multi-value input with custom tags, suggestions, and keyboard-accessible chips. - [Textarea](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftextarea): Displays a form textarea or a component that looks like a textarea. - [Theme Toggle](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftheme-toggle): A controlled light/dark switch with optional view-transition effects. - [Toast](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftoast): A succinct message that is displayed temporarily. - [Toggle](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftoggle): A two-state button that can be either on or off. - [Toggle Group](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftoggle-group): A set of two-state buttons that can be toggled on or off. - [Toolbar](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftoolbar): Groups related commands and controls with arrow-key focus navigation. - [Tooltip](https://sui.draco.dev/api/llms-markdown?locale=en-US&slug=components%2Ftooltip): A popup that displays information related to an element when the element receives keyboard focus or the mouse hovers over it. --- # Accordion A vertically stacked set of interactive headings that each reveal a section of content. Page: https://sui.draco.dev/docs/components/accordion ### Example: accordion-demo ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; export default function AccordionDemo() { return ( What are your shipping options? We offer standard (5-7 days), express (2-3 days), and overnight shipping. Free shipping on international orders. What is your return policy? Returns accepted within 30 days. Items must be unused and in original packaging. Refunds processed within 5-7 business days. How can I contact customer support? Reach us via email, live chat, or phone. We respond within 24 hours during business days. ); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/accordion ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx showLineNumbers import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion" ``` ```tsx showLineNumbers Is it accessible? Yes. It adheres to the WAI-ARIA design pattern. ``` ## Composition Use the following composition to build an `Accordion`: ```text Accordion ├── AccordionItem │ ├── AccordionTrigger │ └── AccordionContent └── AccordionItem ├── AccordionTrigger └── AccordionContent ``` ## Basic A basic accordion that shows one item at a time. The first item is open by default. ### Example: accordion-basic ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; const items = [ { value: "item-1", trigger: "How do I reset my password?", content: "Click on 'Forgot Password' on the login page, enter your email address, and we'll send you a link to reset your password. The link will expire in 24 hours.", }, { value: "item-2", trigger: "Can I change my subscription plan?", content: "Yes, you can upgrade or downgrade your plan at any time from your account settings. Changes will be reflected in your next billing cycle.", }, { value: "item-3", trigger: "What payment methods do you accept?", content: "We accept all major credit cards, PayPal, and bank transfers. All payments are processed securely through our payment partners.", }, ]; export function AccordionBasic() { return ( {items.map((item) => ( {item.trigger} {item.content} ))} ); } export default AccordionBasic; ``` ## Multiple Use the `multiple` prop to allow multiple items to be open at the same time. ### Example: accordion-multiple ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; const items = [ { value: "notifications", trigger: "Notification Settings", content: "Manage how you receive notifications. You can enable email alerts for updates or push notifications for mobile devices.", }, { value: "privacy", trigger: "Privacy & Security", content: "Control your privacy settings and security preferences. Enable two-factor authentication, manage connected devices, review active sessions, and configure data sharing preferences. You can also download your data or delete your account.", }, { value: "billing", trigger: "Billing & Subscription", content: "View your current plan, payment history, and upcoming invoices. Update your payment method, change your subscription tier, or cancel your subscription.", }, ]; export function AccordionMultiple() { return ( {items.map((item) => ( {item.trigger} {item.content} ))} ); } export default AccordionMultiple; ``` ## Disabled Use the `disabled` prop on `AccordionItem` to disable individual items. ### Example: accordion-disabled ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; export default function AccordionDisabled() { return ( Can I access my account history? Yes, you can view your complete account history including all transactions, plan changes, and support tickets in the Account History section of your dashboard. Premium feature information This section contains information about premium features. Upgrade your plan to access this content. How do I update my email address? You can update your email address in your account settings. You'll receive a verification email at your new address to confirm the change. ); } ``` ## Borders Add `border` to the `Accordion` and `border-b last:border-b-0` to the `AccordionItem` to add borders to the items. ### Example: accordion-borders ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; const items = [ { value: "billing", trigger: "How does billing work?", content: "We offer monthly and annual subscription plans. Billing is charged at the beginning of each cycle, and you can cancel anytime. All plans include automatic backups, 24/7 support, and unlimited team members.", }, { value: "security", trigger: "Is my data secure?", content: "Yes. We use end-to-end encryption, SOC 2 Type II compliance, and regular third-party security audits. All data is encrypted at rest and in transit using industry-standard protocols.", }, { value: "integration", trigger: "What integrations do you support?", content: "We integrate with 500+ popular tools including Slack, Zapier, Salesforce, HubSpot, and more. You can also build custom integrations using our REST API and webhooks.", }, ]; export default function AccordionBorders() { return ( {items.map((item) => ( {item.trigger} {item.content} ))} ); } ``` ## Card Wrap the `Accordion` in a `Card` component. ### Example: accordion-card ```tsx import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; import { Card, CardContent, CardDescription, CardHeader, CardTitle, } from "@workspace/ui/components/card"; const items = [ { value: "plans", trigger: "What subscription plans do you offer?", content: "We offer three subscription tiers: Starter ($9/month), Professional ($29/month), and Enterprise ($99/month). Each plan includes increasing storage limits, API access, priority support, and team collaboration features.", }, { value: "billing", trigger: "How does billing work?", content: "Billing occurs automatically at the start of each billing cycle. We accept all major credit cards, PayPal, and ACH transfers for enterprise customers. You'll receive an invoice via email after each payment.", }, { value: "cancel", trigger: "How do I cancel my subscription?", content: "You can cancel your subscription anytime from your account settings. There are no cancellation fees or penalties. Your access will continue until the end of your current billing period.", }, ]; export default function AccordionCard() { return ( Subscription & Billing Common questions about your account, plans, payments and cancellations. {items.map((item) => ( {item.trigger} {item.content} ))} ); } ``` ## RTL To enable RTL support in shadcn/ui, see the [RTL configuration guide](https://ui.shadcn.com/docs/rtl). ### Example: accordion-rtl ```tsx "use client"; import { Accordion, AccordionContent, AccordionItem, AccordionTrigger, } from "@workspace/ui/components/accordion"; import { type Translations, useTranslation } from "./support"; const translations: Translations = { en: { dir: "ltr", values: { question1: "How do I reset my password?", answer1: "Click on 'Forgot Password' on the login page, enter your email address, and we'll send you a link to reset your password.", question2: "Can I change my subscription plan?", answer2: "Yes, you can upgrade or downgrade your plan at any time from your account settings. Changes will be reflected in your next billing cycle.", question3: "What payment methods do you accept?", answer3: "We accept all major credit cards, PayPal, and bank transfers. All payments are processed securely through our payment partners.", }, }, ar: { dir: "rtl", values: { question1: "كيف يمكنني إعادة تعيين كلمة المرور؟", answer1: "انقر على 'نسيت كلمة المرور' في صفحة تسجيل الدخول، أدخل عنوان بريدك الإلكتروني، وسنرسل لك رابطًا لإعادة تعيين كلمة المرور. سينتهي صلاحية الرابط خلال 24 ساعة.", question2: "هل يمكنني تغيير خطة الاشتراك الخاصة بي؟", answer2: "نعم، يمكنك ترقية أو تخفيض خطتك في أي وقت من إعدادات حسابك. ستظهر التغييرات في دورة الفوترة التالية.", question3: "ما هي طرق الدفع التي تقبلونها؟", answer3: "نقبل جميع بطاقات الائتمان الرئيسية و PayPal والتحويلات المصرفية. تتم معالجة جميع المدفوعات بأمان من خلال شركاء الدفع لدينا.", }, }, he: { dir: "rtl", values: { question1: "איך אני מאפס את הסיסמה שלי?", answer1: "לחץ על 'שכחתי סיסמה' בעמוד ההתחברות, הזן את כתובת האימייל שלך, ונשלח לך קישור לאיפוס הסיסמה. הקישור יפוג תוך 24 שעות.", question2: "האם אני יכול לשנות את תוכנית המנוי שלי?", answer2: "כן, אתה יכול לשדרג או להוריד את התוכנית שלך בכל עת מההגדרות של החשבון שלך. השינויים יבואו לידי ביטוי במחזור החיוב הבא.", question3: "אילו אמצעי תשלום אתם מקבלים?", answer3: "אנו מקבלים כרטיסי אשראי, PayPal והעברות בנקאיות.", }, }, }; const items = [ { value: "item-1", questionKey: "question1" as const, answerKey: "answer1" as const, }, { value: "item-2", questionKey: "question2" as const, answerKey: "answer2" as const, }, { value: "item-3", questionKey: "question3" as const, answerKey: "answer3" as const, }, ] as const; export function AccordionRtl() { const { t } = useTranslation(translations, "ar"); return ( {items.map((item) => ( {t[item.questionKey]} {t[item.answerKey]} ))} ); } export default AccordionRtl; ``` ## API Reference See the [Base UI](https://base-ui.com/react/components/accordion#api-reference) documentation for more information. - [Documentation](https://base-ui.com/react/components/accordion) - [API reference](https://base-ui.com/react/components/accordion#api-reference) --- # Alert Displays a callout for user attention. Page: https://sui.draco.dev/docs/components/alert ### Example: alert-demo ```tsx import { Alert, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { CheckCircle2Icon, InfoIcon } from "lucide-react"; export default function AlertDemo() { return (
Payment successful Your payment of $29.99 has been processed. A receipt has been sent to your email address. New feature available We've added dark mode support. You can enable it in your account settings.
); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/alert ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx showLineNumbers import { Alert, AlertAction, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert" ``` ```tsx showLineNumbers Heads up! You can add components and dependencies to your app using the cli. ``` ## Composition Use the following composition to build an `Alert`: ```text Alert ├── Icon ├── AlertTitle ├── AlertDescription └── AlertAction ``` ## Basic A basic alert with an icon, title and description. ### Example: alert-basic ```tsx import { Alert, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { CheckCircle2Icon } from "lucide-react"; export default function AlertBasic() { return ( Account updated successfully Your profile information has been saved. Changes will be reflected immediately. ); } ``` ## Destructive Use `variant="destructive"` to create a destructive alert. ### Example: alert-destructive ```tsx import { Alert, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { AlertCircleIcon } from "lucide-react"; export default function AlertDestructive() { return ( Payment failed Your payment could not be processed. Please check your payment method and try again. ); } ``` ## Action Use `AlertAction` to add a button or other action element to the alert. ### Example: alert-action ```tsx import { Alert, AlertAction, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { Button } from "@workspace/ui/components/button"; export default function AlertActionExample() { return ( Dark mode is now available Enable it under your profile settings to get started. ); } ``` ## Custom Colors You can customize the alert colors by adding custom classes such as `bg-amber-50 dark:bg-amber-950` to the `Alert` component. ### Example: alert-colors ```tsx import { Alert, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { AlertTriangleIcon } from "lucide-react"; export default function AlertColors() { return ( Your subscription will expire in 3 days. Renew now to avoid service interruption or upgrade to a paid plan to continue using the service. ); } ``` ## RTL To enable RTL support in shadcn/ui, see the [RTL configuration guide](https://ui.shadcn.com/docs/rtl). ### Example: alert-rtl ```tsx "use client"; import { Alert, AlertDescription, AlertTitle, } from "@workspace/ui/components/alert"; import { CheckCircle2Icon, InfoIcon } from "lucide-react"; import { type Translations, useTranslation } from "./support"; const translations: Translations = { en: { dir: "ltr", values: { paymentTitle: "Payment successful", paymentDescription: "Your payment of $29.99 has been processed. A receipt has been sent to your email address.", featureTitle: "New feature available", featureDescription: "We've added dark mode support. You can enable it in your account settings.", }, }, ar: { dir: "rtl", values: { paymentTitle: "تم الدفع بنجاح", paymentDescription: "تمت معالجة دفعتك البالغة 29.99 دولارًا. تم إرسال إيصال إلى عنوان بريدك الإلكتروني.", featureTitle: "ميزة جديدة متاحة", featureDescription: "لقد أضفنا دعم الوضع الداكن. يمكنك تفعيله في إعدادات حسابك.", }, }, he: { dir: "rtl", values: { paymentTitle: "התשלום בוצע בהצלחה", paymentDescription: "התשלום שלך בסך 29.99 דולר עובד. קבלה נשלחה לכתובת האימייל שלך.", featureTitle: "תכונה חדשה זמינה", featureDescription: "הוספנו תמיכה במצב כהה. אתה יכול להפעיל אותו בהגדרות החשבון שלך.", }, }, }; const alerts = [ { icon: CheckCircle2Icon, titleKey: "paymentTitle" as const, descriptionKey: "paymentDescription" as const, }, { icon: InfoIcon, titleKey: "featureTitle" as const, descriptionKey: "featureDescription" as const, }, ] as const; export function AlertRtl() { const { dir, t } = useTranslation(translations, "ar"); return (
{alerts.map((alert) => { const Icon = alert.icon; return ( {t[alert.titleKey]} {t[alert.descriptionKey]} ); })}
); } export default AlertRtl; ``` ## API Reference ### Alert The `Alert` component displays a callout for user attention. | Prop | Type | Default | | --------- | ---------------------------- | ----------- | | `variant` | `"default" \| "destructive"` | `"default"` | ### AlertTitle The `AlertTitle` component displays the title of the alert. | Prop | Type | Default | | ----------- | -------- | ------- | | `className` | `string` | - | ### AlertDescription The `AlertDescription` component displays the description or content of the alert. | Prop | Type | Default | | ----------- | -------- | ------- | | `className` | `string` | - | ### AlertAction The `AlertAction` component displays an action element (like a button) positioned absolutely in the top-right corner of the alert. | Prop | Type | Default | | ----------- | -------- | ------- | | `className` | `string` | - | --- # Alert Dialog A modal dialog that interrupts the user with important content and expects a response. Page: https://sui.draco.dev/docs/components/alert-dialog ### Example: alert-dialog-demo ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; export default function AlertDialogDemo() { return ( }> Show Dialog Are you absolutely sure? This action cannot be undone. This will permanently delete your account from our servers. Cancel Continue ); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/alert-dialog ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx showLineNumbers import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog" ``` ```tsx showLineNumbers }> Show Dialog Are you absolutely sure? This action cannot be undone. This will permanently delete your account from our servers. Cancel Continue ``` ## Composition Use the following composition to build an `AlertDialog`: ```text AlertDialog ├── AlertDialogTrigger └── AlertDialogContent ├── AlertDialogHeader │ ├── AlertDialogMedia │ ├── AlertDialogTitle │ └── AlertDialogDescription └── AlertDialogFooter ├── AlertDialogCancel └── AlertDialogAction ``` ## Basic A basic alert dialog with a title, description, and cancel and continue buttons. ### Example: alert-dialog-basic ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; export function AlertDialogBasic() { return ( Show Dialog} /> Are you absolutely sure? This action cannot be undone. This will permanently delete your account and remove your data from our servers. Cancel Continue ); } export default AlertDialogBasic; ``` ## Small Use the `size="sm"` prop to make the alert dialog smaller. ### Example: alert-dialog-small ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; export function AlertDialogSmall() { return ( Show Dialog} /> Allow accessory to connect? Do you want to allow the USB accessory to connect to this device? Don't allow Allow ); } export default AlertDialogSmall; ``` ## Media Use the `AlertDialogMedia` component to add a media element such as an icon or image to the alert dialog. ### Example: alert-dialog-media ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; import { CircleFadingPlusIcon } from "lucide-react"; export function AlertDialogWithMedia() { return ( Share Project} /> Share this project? Anyone with the link will be able to view and edit this project. Cancel Share ); } export default AlertDialogWithMedia; ``` ## Small with Media Use the `size="sm"` prop to make the alert dialog smaller and the `AlertDialogMedia` component to add a media element such as an icon or image to the alert dialog. ### Example: alert-dialog-small-media ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; import { BluetoothIcon } from "lucide-react"; export function AlertDialogSmallWithMedia() { return ( Show Dialog} /> Allow accessory to connect? Do you want to allow the USB accessory to connect to this device? Don't allow Allow ); } export default AlertDialogSmallWithMedia; ``` ## Destructive Use the `AlertDialogAction` component to add a destructive action button to the alert dialog. ### Example: alert-dialog-destructive ```tsx import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; import { Trash2Icon } from "lucide-react"; export function AlertDialogDestructive() { return ( Delete Chat} /> Delete chat? This will permanently delete this chat conversation. View{" "} Settings delete any memories saved during this chat. Cancel Delete ); } export default AlertDialogDestructive; ``` ## RTL To enable RTL support in shadcn/ui, see the [RTL configuration guide](https://ui.shadcn.com/docs/rtl). ### Example: alert-dialog-rtl ```tsx "use client"; import { AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogMedia, AlertDialogTitle, AlertDialogTrigger, } from "@workspace/ui/components/alert-dialog"; import { Button } from "@workspace/ui/components/button"; import { BluetoothIcon } from "lucide-react"; import { type Translations, useTranslation } from "./support"; const translations: Translations = { en: { dir: "ltr", values: { showDialog: "Show Dialog", showDialogSm: "Show Dialog (sm)", title: "Are you absolutely sure?", description: "This action cannot be undone. This will permanently delete your account from our servers.", cancel: "Cancel", continue: "Continue", smallTitle: "Allow accessory to connect?", smallDescription: "Do you want to allow the USB accessory to connect to this device?", dontAllow: "Don't allow", allow: "Allow", }, }, ar: { dir: "rtl", values: { showDialog: "إظهار الحوار", showDialogSm: "إظهار الحوار (صغير)", title: "هل أنت متأكد تمامًا؟", description: "لا يمكن التراجع عن هذا الإجراء. سيؤدي هذا إلى حذف حسابك نهائيًا من خوادمنا.", cancel: "إلغاء", continue: "متابعة", smallTitle: "السماح للملحق بالاتصال؟", smallDescription: "هل تريد السماح لملحق USB بالاتصال بهذا الجهاز؟", dontAllow: "عدم السماح", allow: "السماح", }, }, he: { dir: "rtl", values: { showDialog: "הצג דיאלוג", showDialogSm: "הצג דיאלוג (קטן)", title: "האם אתה בטוח לחלוטין?", description: "פעולה זו לא ניתנת לביטול. זה ימחק לצמיתות את החשבון שלך מהשרתים שלנו.", cancel: "ביטול", continue: "המשך", smallTitle: "לאפשר להתקן להתחבר?", smallDescription: "האם אתה רוצה לאפשר להתקן USB להתחבר למכשיר זה?", dontAllow: "אל תאפשר", allow: "אפשר", }, }, }; export function AlertDialogRtl() { const { dir, t, language } = useTranslation(translations, "ar"); return (
}> {t.showDialog} {t.title} {t.description} {t.cancel} {t.continue} }> {t.showDialogSm} {t.smallTitle} {t.smallDescription} {t.dontAllow} {t.allow}
); } export default AlertDialogRtl; ``` ## API Reference ### size Use the `size` prop on the `AlertDialogContent` component to control the size of the alert dialog. It accepts the following values: | Prop | Type | Default | | ------ | ------------------- | ----------- | | `size` | `"default" \| "sm"` | `"default"` | For more information about the other components and their props, see the [Base UI documentation](https://base-ui.com/react/components/alert-dialog#api-reference). - [Documentation](https://base-ui.com/react/components/alert-dialog) - [API reference](https://base-ui.com/react/components/alert-dialog#api-reference) --- # Aspect Ratio Displays content within a desired ratio. Page: https://sui.draco.dev/docs/components/aspect-ratio ### Example: aspect-ratio-demo ```tsx import { AspectRatio } from "@workspace/ui/components/aspect-ratio"; export default function AspectRatioDemo() { return ( Landscape ); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/aspect-ratio ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx showLineNumbers import { AspectRatio } from "@workspace/ui/components/aspect-ratio" ``` ```tsx showLineNumbers Image ``` ## Square A square aspect ratio component using the `ratio={1 / 1}` prop. This is useful for displaying images in a square format. ### Example: aspect-ratio-square ```tsx import { AspectRatio } from "@workspace/ui/components/aspect-ratio"; export function AspectRatioSquare() { return ( Landscape ); } export default AspectRatioSquare; ``` ## Portrait A portrait aspect ratio component using the `ratio={9 / 16}` prop. This is useful for displaying images in a portrait format. ### Example: aspect-ratio-portrait ```tsx import { AspectRatio } from "@workspace/ui/components/aspect-ratio"; export function AspectRatioPortrait() { return ( Landscape ); } export default AspectRatioPortrait; ``` ## RTL To enable RTL support in shadcn/ui, see the [RTL configuration guide](https://ui.shadcn.com/docs/rtl). ### Example: aspect-ratio-rtl ```tsx "use client"; import { AspectRatio } from "@workspace/ui/components/aspect-ratio"; import { type Translations, useTranslation } from "./support"; const translations: Translations = { en: { dir: "ltr", values: { caption: "Beautiful landscape", }, }, ar: { dir: "rtl", values: { caption: "منظر طبيعي جميل", }, }, he: { dir: "rtl", values: { caption: "נוף יפה", }, }, }; export function AspectRatioRtl() { const { dir, t } = useTranslation(translations, "ar"); return (
Landscape
{t.caption}
); } export default AspectRatioRtl; ``` ## API Reference ### AspectRatio The `AspectRatio` component displays content within a desired ratio. | Prop | Type | Default | Required | | ----------- | -------- | ------- | -------- | | `ratio` | `number` | - | Yes | | `className` | `string` | - | No | For more information, see the [Base UI documentation](https://base-ui.com/react/components/aspect-ratio#api-reference). --- # Attachment Displays a file or image attachment with media, metadata, upload state, and actions. Page: https://sui.draco.dev/docs/components/attachment ### Example: attachment-demo ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, } from "@workspace/ui/components/attachment"; import { Loader } from "@workspace/ui/components/loader"; import { FileCodeIcon, XIcon } from "lucide-react"; const images = [ { name: "workspace.png", meta: "PNG · 820 KB", src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80", alt: "Workspace", }, { name: "desk-reference.jpg", meta: "JPG · 1.1 MB", src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80", alt: "Desk", }, { name: "office-reference.jpg", meta: "JPG · 940 KB", src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80", alt: "Office", }, ]; export function AttachmentDemo() { return (
{images.map((image) => ( {image.alt} {image.name} {image.meta} ))} sales-dashboard.pdf Uploading · 64% message-renderer.tsx TypeScript · 12 KB
); } export default AttachmentDemo; ``` The `Attachment` component displays a file or image attachment, its media, name, and metadata, with optional actions and upload state. Use it for files and images in chat composers, message threads, and upload lists. ## Installation ```bash bunx --bun shadcn@latest add @sui/attachment ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, } from "@workspace/ui/components/attachment" ``` ```tsx sales-dashboard.pdf PDF · 2.4 MB ``` ## Composition Use the following composition to build an attachment: ```text Attachment ├── AttachmentMedia ├── AttachmentContent │ ├── AttachmentTitle │ └── AttachmentDescription ├── AttachmentActions │ └── AttachmentAction └── AttachmentTrigger ``` Use `AttachmentGroup` to lay out multiple attachments in a scrollable row: ```text AttachmentGroup ├── Attachment └── Attachment ``` ## Features - Icon and image media through `AttachmentMedia` - Upload states: `idle`, `uploading`, `processing`, `error`, and `done` with built-in styling and a shimmer while in progress - Three sizes and horizontal or vertical orientation - A full-card `AttachmentTrigger` that opens a link or dialog while the actions stay independently clickable - Scrollable, snapping `AttachmentGroup` with an edge fade - Customizable styling through the `className` prop on every part ## Image Set `variant="image"` on `AttachmentMedia` and render an `` inside it. Use `orientation="vertical"` to stack the media above the content. ### Example: attachment-image ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, AttachmentTrigger, } from "@workspace/ui/components/attachment"; import { XIcon } from "lucide-react"; const images = [ { name: "workspace.png", meta: "PNG · 820 KB", src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80", alt: "Workspace", }, { name: "desk-reference.jpg", meta: "JPG · 1.1 MB", src: "https://images.unsplash.com/photo-1497215728101-856f4ea42174?w=900&auto=format&fit=crop&q=80", alt: "Desk", }, { name: "office-reference.jpg", meta: "JPG · 940 KB", src: "https://images.unsplash.com/photo-1497366811353-6870744d04b2?w=900&auto=format&fit=crop&q=80", alt: "Office", }, ]; export function AttachmentImage() { return (
{images.map((image) => ( {image.alt} {image.name} {image.meta} } /> ))}
); } export default AttachmentImage; ``` ## States Set `state` to reflect the upload lifecycle. `uploading` and `processing` shimmer the title, and `error` switches to a destructive treatment. ### Example: attachment-states ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, } from "@workspace/ui/components/attachment"; import { Loader } from "@workspace/ui/components/loader"; import { CheckIcon, ClockIcon, FileTextIcon, FileWarningIcon, RefreshCwIcon, XIcon, } from "lucide-react"; export function AttachmentStates() { return (
selected-file.pdf Ready to upload design-system.zip Uploading · 64% market-research.pdf Processing document financial-model.xlsx Upload failed. Try again. uploaded-report.pdf Uploaded · 1.8 MB
); } export default AttachmentStates; ``` ## Sizes Use `size` to switch between `default`, `sm`, and `xs`. ### Example: attachment-sizes ```tsx import { Attachment, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, } from "@workspace/ui/components/attachment"; import { FileTextIcon } from "lucide-react"; export function AttachmentSizes() { return (
Default attachment PDF · 2.4 MB Small attachment PDF · 2.4 MB Extra small attachment
); } export default AttachmentSizes; ``` ## Group Wrap attachments in `AttachmentGroup` to lay them out in a horizontally scrollable, snapping row with an edge fade. ### Example: attachment-group ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentGroup, AttachmentMedia, AttachmentTitle, } from "@workspace/ui/components/attachment"; import { FileCodeIcon, FileTextIcon, type LucideIcon, TableIcon, XIcon, } from "lucide-react"; import type { ReactNode } from "react"; type Item = { name: string; meta: string; icon?: LucideIcon; src?: string; }; const items: Item[] = [ { name: "briefing-notes.pdf", meta: "PDF · 1.4 MB", icon: FileTextIcon }, { name: "workspace.png", meta: "PNG · 820 KB", src: "https://images.unsplash.com/photo-1497366754035-f200968a6e72?w=900&auto=format&fit=crop&q=80", }, { name: "customers.csv", meta: "CSV · 18 KB", icon: TableIcon }, { name: "renderer.tsx", meta: "TSX · 12 KB", icon: FileCodeIcon }, ]; export function AttachmentGroupDemo() { return (
{items.map((item) => { const Icon = item.icon; let media: ReactNode = null; if (item.src) { media = ( {item.name} ); } else if (Icon) { media = ( ); } return ( {media} {item.name} {item.meta} ); })}
); } export default AttachmentGroupDemo; ``` ## Trigger Add an `AttachmentTrigger` to make the whole card open a link or dialog. It fills the card behind the actions, so the actions stay clickable. ### Example: attachment-trigger ```tsx import { Attachment, AttachmentAction, AttachmentActions, AttachmentContent, AttachmentDescription, AttachmentMedia, AttachmentTitle, AttachmentTrigger, } from "@workspace/ui/components/attachment"; import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger, } from "@workspace/ui/components/dialog"; import { CopyIcon, FileSearchIcon, XIcon } from "lucide-react"; export function AttachmentTriggerDemo() { return (
research-summary.pdf Open preview dialog } /> research-summary.pdf The attachment trigger fills the card and opens the dialog, while the actions stay independently clickable above it.
); } export default AttachmentTriggerDemo; ``` ```tsx showLineNumbers {/* media, content, actions */} } /> {/* ... */} ``` ## Accessibility `AttachmentAction` renders a `Button`, and `AttachmentTrigger` renders a real ` ); } export default BubbleReactionsDemo; ``` ## Show More / Collapsible Long bubble content can be composed with [`Collapsible`](/docs/components/collapsible) to allow for a show more or show less interaction. Use the `CollapsibleTrigger` component to trigger the collapsible content. ### Example: bubble-collapsible ```tsx "use client"; import { Bubble, BubbleContent } from "@workspace/ui/components/bubble"; import { Button } from "@workspace/ui/components/button"; import { Collapsible, CollapsibleTrigger, } from "@workspace/ui/components/collapsible"; import { ChevronDownIcon } from "lucide-react"; import * as React from "react"; const text = `The accessibility review found two focus states that were visually too subtle in dark mode. I checked the dialog, menu, and drawer paths because each one renders focusable controls inside a layered surface. The dialog and drawer are fine. The menu needs the hover and focus tokens split so keyboard focus stays visible when the pointer is not involved. I also recommend keeping the change in the style file instead of the primitive so the other themes can choose their own focus treatment later.`; const previewLength = 180; export function BubbleCollapsible() { const [open, setOpen] = React.useState(false); const isLong = text.length > previewLength; const preview = `${text.slice(0, previewLength)}...`; return (
How can I help you today?
{open || !isLong ? text : preview}
{isLong ? ( } > {open ? "Show less" : "Show more"} ) : null}
); } export default BubbleCollapsible; ``` ## Tooltip Wrap a bubble in a [`Tooltip`](/docs/components/tooltip) to reveal metadata on hover, such as when a message was read. ### Example: bubble-tooltip ```tsx import { Bubble, BubbleContent, BubbleReactions, } from "@workspace/ui/components/bubble"; import { Button } from "@workspace/ui/components/button"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@workspace/ui/components/tooltip"; import { CheckIcon } from "lucide-react"; export function BubbleTooltipDemo() { return (
Did you remove the stale route? Yes, removed it from the registry. }> Read on Jan 5, 2026 at 4:32 PM
); } export default BubbleTooltipDemo; ``` ## Popover Pair a bubble with a [`Popover`](/docs/components/popover) to surface more information on demand, such as the full error message for a failed action. ### Example: bubble-popover ```tsx import { Bubble, BubbleContent, BubbleReactions, } from "@workspace/ui/components/bubble"; import { Button } from "@workspace/ui/components/button"; import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "@workspace/ui/components/popover"; import { InfoIcon } from "lucide-react"; export function BubblePopoverDemo() { return (
Run the build script. Failed to run the command. } > Command failed with exit code 1 ENOENT: no such file or directory, open pnpm-lock.yaml
); } export default BubblePopoverDemo; ``` ## Accessibility `Bubble` renders the presentational message surface. Keep conversation-level semantics on the surrounding container and follow the guidelines below. ### Labeling Reactions Reactions render as a row of emoji. A screen reader reads each glyph with no context, and counters like `+8` are announced as "plus eight". Group the row as a single image with a descriptive `aria-label` so it announces once. `role="img"` also hides the individual emoji from assistive tech, so no `aria-hidden` is needed. ```tsx showLineNumbers 👍 🔥 +8 ``` When reactions are interactive, render buttons instead and give icon-only buttons an `aria-label`. ```tsx showLineNumbers ``` ### Interactive Bubbles When a bubble is clickable, render it as a real ` ); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/button ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx import { Button } from "@workspace/ui/components/button" ``` ```tsx ``` ## Cursor Tailwind v4 [switched](https://tailwindcss.com/docs/upgrade-guide#buttons-use-the-default-cursor) from `cursor: pointer` to `cursor: default` for the button component. If you want to keep the `cursor: pointer` behavior, add the following code to your CSS file: You can also enable this during project setup with `npx shadcn@latest init --pointer`. ```css showLineNumbers title="globals.css" @layer base { button:not(:disabled), [role="button"]:not(:disabled) { cursor: pointer; } } ``` ## Size Use the `size` prop to change the size of the button. ### Example: button-size ```tsx import { Button } from "@workspace/ui/components/button"; import { ArrowUpRightIcon } from "lucide-react"; export default function ButtonSize() { return (
); } ``` ## Default ### Example: button-default ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonDefault() { return ; } ``` ## Outline ### Example: button-outline ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonOutline() { return ; } ``` ## Secondary ### Example: button-secondary ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonSecondary() { return ; } ``` ## Ghost ### Example: button-ghost ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonGhost() { return ; } ``` ## Destructive ### Example: button-destructive ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonDestructive() { return ; } ``` ## Link ### Example: button-link ```tsx import { Button } from "@workspace/ui/components/button"; export default function ButtonLink() { return ; } ``` ## Icon ### Example: button-icon ```tsx import { Button } from "@workspace/ui/components/button"; import { CircleFadingArrowUpIcon } from "lucide-react"; export default function ButtonIcon() { return ( ); } ``` ## With Icon Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the icon for the correct spacing. ### Example: button-with-icon ```tsx import { Button } from "@workspace/ui/components/button"; import { GitBranchIcon, GitForkIcon } from "lucide-react"; export default function ButtonWithIcon() { return (
); } ``` ## Rounded Use the `rounded-full` class to make the button rounded. ### Example: button-rounded ```tsx import { Button } from "@workspace/ui/components/button"; import { ArrowUpIcon } from "lucide-react"; export default function ButtonRounded() { return (
); } ``` ## Loader Render a `` component inside the button to show a loading state. Remember to add the `data-icon="inline-start"` or `data-icon="inline-end"` attribute to the spinner for the correct spacing. ### Example: button-loading ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { Loader } from "@workspace/ui/components/loader"; import { useEffect, useRef, useState } from "react"; import type { ExampleProps } from "../types"; export default function ButtonLoading({ locale }: ExampleProps) { const zh = locale === "zh-CN"; const stateLabels = zh ? { generating: "正在生成", generate: "生成", downloading: "正在下载", download: "下载", done: "演示完成,可以再次点击", idle: "点击按钮模拟加载状态", } : { generating: "Generating", generate: "Generate", downloading: "Downloading", download: "Download", done: "Demo complete. Try it again.", idle: "Click a button to simulate loading.", }; const [status, setStatus] = useState<"idle" | "pending" | "done">("idle"); const timer = useRef | undefined>(undefined); const pending = status === "pending"; useEffect(() => () => clearTimeout(timer.current), []); function startDemo() { clearTimeout(timer.current); setStatus("pending"); // Replace this delay with your application's async operation. timer.current = setTimeout(() => setStatus("done"), 1200); } return (

{status === "done" ? stateLabels.done : stateLabels.idle}

); } ``` ## Button Group To create a button group, use the `ButtonGroup` component. See the [Button Group](/docs/components/button-group) documentation for more details. ### Example: button-group-demo ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@workspace/ui/components/dropdown-menu"; import { ArchiveIcon, ArrowLeftIcon, CalendarPlusIcon, ClockIcon, ListFilterIcon, MailCheckIcon, MoreHorizontalIcon, TagIcon, Trash2Icon, } from "lucide-react"; import * as React from "react"; export default function ButtonGroupDemo() { const [label, setLabel] = React.useState("personal"); return ( } > Mark as Read Archive Snooze Add to Calendar Add to List Label As... Personal Work Other Trash ); } ``` ## As Link You can use the `buttonVariants` helper to make a link look like a button. **Do not use `
); } export default ButtonRtl; ``` ## API Reference ### Button The `Button` component is a wrapper around the `button` element that adds a variety of styles and functionality. | Prop | Type | Default | | --------- | ------------------------------------------------------------------------------------ | ----------- | | `variant` | `"default" \| "outline" \| "ghost" \| "destructive" \| "secondary" \| "link"` | `"default"` | | `size` | `"default" \| "xs" \| "sm" \| "lg" \| "icon" \| "icon-xs" \| "icon-sm" \| "icon-lg"` | `"default"` | --- # Button Group A container that groups related buttons together with consistent styling. Page: https://sui.draco.dev/docs/components/button-group ### Example: button-group-demo ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, } from "@workspace/ui/components/dropdown-menu"; import { ArchiveIcon, ArrowLeftIcon, CalendarPlusIcon, ClockIcon, ListFilterIcon, MailCheckIcon, MoreHorizontalIcon, TagIcon, Trash2Icon, } from "lucide-react"; import * as React from "react"; export default function ButtonGroupDemo() { const [label, setLabel] = React.useState("personal"); return ( } > Mark as Read Archive Snooze Add to Calendar Add to List Label As... Personal Work Other Trash ); } ``` ## Installation ```bash bunx --bun shadcn@latest add @sui/button-group ``` Install with the shadcn CLI or use the shared `@workspace/ui` package. Follow the [installation guide](/docs/installation) to configure the registry, load styles, and choose import aliases. ## Usage ```tsx import { ButtonGroup, ButtonGroupSeparator, ButtonGroupText, } from "@workspace/ui/components/button-group" ``` ```tsx ``` ## Composition Use the following composition to build a `ButtonGroup`: ```text ButtonGroup ├── Button or Input ├── ButtonGroupSeparator └── ButtonGroupText ``` ## Accessibility - The `ButtonGroup` component has the `role` attribute set to `group`. - Use Tab to navigate between the buttons in the group. - Use `aria-label` or `aria-labelledby` to label the button group. ```tsx showLineNumbers ``` ## ButtonGroup vs ToggleGroup - Use the `ButtonGroup` component when you want to group buttons that perform an action. - Use the `ToggleGroup` component when you want to group buttons that toggle a state. ## Orientation Set the `orientation` prop to change the button group layout. ### Example: button-group-orientation ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { MinusIcon, PlusIcon } from "lucide-react"; export default function ButtonGroupOrientation() { return ( ); } ``` ## Size Control the size of buttons using the `size` prop on individual buttons. ### Example: button-group-size ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { PlusIcon } from "lucide-react"; export default function ButtonGroupSize() { return (
); } ``` ## Nested Nest `` components to create button groups with spacing. ### Example: button-group-nested ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { InputGroup, InputGroupAddon, InputGroupInput, } from "@workspace/ui/components/input-group"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@workspace/ui/components/tooltip"; import { AudioLinesIcon, PlusIcon } from "lucide-react"; export function ButtonGroupNested() { return ( }> Voice Mode ); } export default ButtonGroupNested; ``` ## Separator The `ButtonGroupSeparator` component visually divides buttons within a group. Buttons with variant `outline` do not need a separator since they have a border. For other variants, a separator is recommended to improve the visual hierarchy. ### Example: button-group-separator ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup, ButtonGroupSeparator, } from "@workspace/ui/components/button-group"; export default function ButtonGroupSeparatorDemo() { return ( ); } ``` ## Split Create a split button group by adding two buttons separated by a `ButtonGroupSeparator`. ### Example: button-group-split ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup, ButtonGroupSeparator, } from "@workspace/ui/components/button-group"; import { PlusIcon } from "lucide-react"; export default function ButtonGroupSplit() { return ( ); } ``` ## Input Wrap an `Input` component with buttons. ### Example: button-group-input ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { Input } from "@workspace/ui/components/input"; import { SearchIcon } from "lucide-react"; export default function ButtonGroupInput() { return ( ); } ``` ## Input Group Wrap an `InputGroup` component to create complex input layouts. ### Example: button-group-input-group ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { InputGroup, InputGroupAddon, InputGroupButton, InputGroupInput, } from "@workspace/ui/components/input-group"; import { Tooltip, TooltipContent, TooltipTrigger, } from "@workspace/ui/components/tooltip"; import { AudioLinesIcon, PlusIcon } from "lucide-react"; import * as React from "react"; export default function ButtonGroupInputGroup() { const [voiceEnabled, setVoiceEnabled] = React.useState(false); return ( setVoiceEnabled(!voiceEnabled)} size="icon-xs" data-active={voiceEnabled} className="data-[active=true]:bg-orange-100 data-[active=true]:text-orange-700 dark:data-[active=true]:bg-orange-800 dark:data-[active=true]:text-orange-100" aria-pressed={voiceEnabled} /> } > Voice Mode ); } ``` ## Dropdown Menu Create a split button group with a `DropdownMenu` component. ### Example: button-group-dropdown ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { DropdownMenu, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from "@workspace/ui/components/dropdown-menu"; import { AlertTriangleIcon, CheckIcon, ChevronDownIcon, CopyIcon, ShareIcon, TrashIcon, UserRoundXIcon, VolumeOffIcon, } from "lucide-react"; export default function ButtonGroupDropdown() { return ( } > Mute Conversation Mark as Read Report Conversation Block User Share Conversation Copy Conversation Delete Conversation ); } ``` ## Select Pair with a `Select` component. ### Example: button-group-select ```tsx "use client"; import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { Input } from "@workspace/ui/components/input"; import { Select, SelectContent, SelectGroup, SelectItem, SelectTrigger, } from "@workspace/ui/components/select"; import { ArrowRightIcon } from "lucide-react"; import * as React from "react"; const CURRENCIES = [ { label: "US Dollar", value: "$" }, { label: "Euro", value: "€" }, { label: "British Pound", value: "£" }, ]; export default function ButtonGroupSelect() { const [currency, setCurrency] = React.useState("$"); return ( ); } ``` ## Popover Use with a `Popover` component. ### Example: button-group-popover ```tsx import { Button } from "@workspace/ui/components/button"; import { ButtonGroup } from "@workspace/ui/components/button-group"; import { Field, FieldDescription, FieldLabel, } from "@workspace/ui/components/field"; import { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger, } from "@workspace/ui/components/popover"; import { Textarea } from "@workspace/ui/components/textarea"; import { BotIcon, ChevronDownIcon } from "lucide-react"; import { useId as usePreviewId } from "react"; export default function ButtonGroupPopover() { const previewId = usePreviewId(); return ( } > Start a new task with Copilot Describe your task in natural language. Task Description