The negocie.ai design system
1.0This catalog is the exploration surface for the negocie.ai shadcn/ui based design system, seeded with the brand's Azul Royal Tech and Grafite Profundo tokens. Nothing is hard-coded: every visual attribute flows from a design token. Click any value below to edit it and see the whole catalog react live.
Token-first
Colors, radii, spacing and typography are CSS variables. Change a token and every component updates.
Complete catalog
Each component shows every variant, size, orientation, state and composition — not just a default.
Accessible
Built on Base UI primitives with correct roles, labels, focus management and keyboard support.
Composable
Modular building blocks meant to be combined. Composition examples are included throughout.
Design Tokens
Tokens are the single source of truth. They are defined as CSS custom properties in globals.css, mapped to Tailwind theme variables, and consumed by every component. Every token below is directly editable from the sidebar.
How tokens flow
A raw value is assigned to a semantic token, exposed to Tailwind via @theme inline, then referenced by components through utility classes such as bg-primary or text-muted-foreground.
/* 1. Define the value (globals.css) */
:root {
--primary: oklch(0.205 0 0);
--primary-foreground: oklch(0.985 0 0);
--radius: 0.625rem;
}
/* 2. Map it to a Tailwind theme token */
@theme inline {
--color-primary: var(--primary);
--color-primary-foreground: var(--primary-foreground);
--radius-lg: var(--radius);
}
/* 3. Consume it in a component */
/* <button class="bg-primary text-primary-foreground rounded-lg" /> */Color
A restrained, semantic palette. Neutrals carry most of the interface; feedback colors (success, warning, error, information) communicate status and should never be used decoratively. Utility tokens cover borders, form inputs and focus rings. Every swatch is editable from the sidebar — its paired foreground is recalculated automatically to preserve contrast.
Typography
The type scale is a modular scale derived from a base size and a ratio — both editable from the sidebar, reshaping every text-* utility at once.
Type scale
Families
Aa Bb Cc 123
Interface and body text. Weights 400–600.
Aa Bb Cc 123
Code, tokens and tabular values.
Spacing
Layout rhythm uses a single base spacing unit (4px by default, editable from the sidebar). Prefer gap utilities over margins for predictable, direction-aware spacing.
Radii
All corner rounding derives from a single --radius token, editable from the sidebar. The scale (sm → 2xl) is calculated from it, so adjusting one value reshapes the whole system.
Shadows
Elevation is subtle and used sparingly — mostly for overlays (popovers, dropdowns, dialogs) to separate them from the page. Both the shadow color and its strength are editable from the sidebar.
Icons
Icons come from lucide-react, the icon library configured for this project. Use a consistent stroke and let components size icons automatically; only set an explicit size for standalone, decorative usage.
Sizing
Inside components (buttons, inputs, menus) icons inherit the correct size. Standalone, use size-4 (16px), size-5 (20px) or size-6 (24px).
import { TypeIcon } from 'lucide-react'
// Standalone icon
<TypeIcon className="size-5" aria-hidden />
// Inside a button — no size class needed, use data-icon
<Button>
<PlusIcon data-icon="inline-start" />
New item
</Button>Toggle & Toggle Group
ToggleA two-state button (on/off) and a group of related toggles for single or multiple selection. Ideal for formatting controls and view options.
Toggle — variants & sizes
Standalone toggles. The pressed state uses the muted surface.
Uses aria-pressed. Icon-only toggles need an aria-label.
States
Off, on and disabled.
Toggle Group — single
Mutually exclusive selection, like a text alignment control.
Toggle Group — joined & multiple
spacing={0} joins items; multiple allows more than one active value.
Recommended
- Use a standalone Toggle for a single independent on/off setting (e.g. mute, favorite).
- Use Toggle Group when several toggles represent one decision — single selection for exclusive choices, multiple for independent flags.
Avoid
- Don't use Toggle where a Checkbox or Switch is the expected control — reserve it for icon-driven, button-like affordances.
- Avoid unlabeled icon-only toggles without an aria-label; the pressed state alone is not enough for screen reader users.
Badge
BadgeA compact label for status, counts and metadata. Non-interactive by default, but can render as a link. Use variants to communicate meaning.
Variants
Match the badge variant to the surrounding intent.
With icons & counts
Add a leading icon for status or use as a numeric counter.
Status dots
Semantic status
Combine a colored dot with an outline badge to convey status using feedback tokens.
Recommended
- Pair a status dot with a text label — never ship color as the only signal.
- Keep badge text to one or two words; move detail to a tooltip or the surrounding content.
Avoid
- Don't make a badge interactive on its own — wrap it in a button/link if it needs to be clickable.
- Avoid using more than one badge variant for the same meaning across the app (pick one mapping and stay consistent).
Input & Field
input · fieldText entry controls and the Field primitives that wire labels, descriptions, and validation together. Inputs share --input, --ring, and --border tokens for a consistent focus treatment.
States
Default, disabled, and invalid share one token set.
Input group
Compose icons, text, and units inside an input with InputGroupAddon.
Field: label, description, error
data-invalid on Field pairs with aria-invalid on the control for accessible validation.
We'll never share your email.
FieldLabel is associated via htmlFor; FieldError is announced to assistive tech when the field is invalid.
Field set with legend
Group related controls with FieldSet and FieldLegend for accessible structure.
Recommended
- Wrap every control in a Field with an associated FieldLabel.
- Use FieldError with data-invalid + aria-invalid for validation.
- Group related fields with FieldSet and FieldLegend.
Avoid
- Don't lay out forms with raw divs and space-y utilities.
- Don't put a raw Input inside an InputGroup — use InputGroupInput.
- Don't rely on placeholder text as the only label.
Textarea
textareaMulti-line text entry that inherits the same border, ring, and invalid tokens as Input.
Default
States
Disabled and invalid states.
Recommended
- Set rows to size the control for the expected content (comments vs. long-form notes).
- Pair with FieldDescription to hint at length limits or formatting.
- Use aria-invalid + FieldError together, never color alone, to signal validation.
Avoid
- Don't use Textarea for single-line values like names or emails — use Input.
- Don't disable resize without a good reason; users rely on it for long text.
- Don't rely on placeholder text as a substitute for a FieldLabel.
Checkbox
checkboxSelection control for independent boolean options. Checked, unchecked, and disabled states are all token-driven.
States
Interaction states
Focus-visible uses the shared --ring token; disabled removes pointer events and lowers opacity.
The clickable hit area extends beyond the visible box (after:-inset-x-3 after:-inset-y-2) to meet the 24px minimum target size.
Recommended
- Always pair a Checkbox with a Label (or aria-label) — never ship an unlabeled box.
- Use Checkbox for independent, multi-select choices; use RadioGroup for mutually exclusive ones.
- Reflect indeterminate state explicitly when a parent checkbox covers partial child selections.
Avoid
- Don't use Checkbox to trigger an immediate action — that's what Switch or Button are for.
- Don't shrink the hit area with custom sizing; the extended after: target keeps it tappable.
- Don't rely on color alone for the checked state — the check glyph must remain visible.
Radio Group
radio-groupMutually exclusive choice among a small set of options. Items always live inside RadioGroup.
Vertical
Horizontal
Stacks vertically on narrow viewports and lays out inline once there's room, keeping labels legible at every width.
Below sm the options wrap to one per row; from sm upward they sit on a single row.
Recommended
- Always set a defaultValue or controlled value so one option is selected by default when appropriate.
- Keep option labels short enough to stay legible in a horizontal layout on small screens.
- Use RadioGroup for 2-6 mutually exclusive options; switch to Select once the list grows long.
Avoid
- Don't mix RadioGroupItem with plain <input type="radio"> — state and styling won't sync.
- Don't omit a visible Label for each item; relying on adjacent text alone breaks click targets.
- Don't use a radio group for a single yes/no choice — a Switch or Checkbox is clearer.
Switch
switchToggle for instant on/off settings. The thumb and track colors come from primary and input tokens.
States
Interaction states
Focus-visible draws the same ring token as other controls; the thumb slides via a transform, not a layout shift.
Switch exposes role=switch with aria-checked; announce the setting name via an adjacent Label, not the state alone.
Recommended
- Use Switch for settings that take effect immediately, without a separate save action.
- Label the setting itself ("Push notifications"), not the state ("On"/"Off").
- Use the sm size only in dense rows (e.g. tables) — default size everywhere else.
Avoid
- Don't use Switch inside a form that requires an explicit Submit — use Checkbox instead.
- Don't pair a Switch with a label that describes the current value; it becomes stale once toggled.
- Don't use two Switches to express a mutually exclusive choice — that's a RadioGroup.
Select
selectDropdown selection built from SelectItem inside SelectGroup. The popup surface uses popover tokens.
Default
Long content
The trigger truncates its value; the popup scrolls once the option list exceeds the viewport.
The trigger keeps a fixed max-width and wraps to full width on narrow screens; the content panel repositions to stay within the viewport.
Disabled
Recommended
- Use Select when options are known and fixed; switch to Combobox once users need to search or filter.
- Group related options with SelectGroup so screen readers announce structure.
- Provide a placeholder via SelectValue so the empty state is never a blank trigger.
Avoid
- Don't use Select for fewer than ~5 options — RadioGroup keeps every choice visible at once.
- Don't put more than a couple dozen items in a single Select without search — use Combobox instead.
- Don't truncate the trigger value without an accessible full label available (e.g. via title or SelectValue text).
Slider
sliderRange selection bound to component state. The track and range use muted and primary tokens.
Controlled
Range
Two thumbs produce a min/max range.
Disabled
Recommended
- Always show the current numeric value near the track (label, tooltip, or adjacent text).
- Pick a step that matches meaningful increments (1 for percentages, 5-10 for coarser ranges).
- Use the two-thumb variant only for genuine min/max ranges, not two unrelated values.
Avoid
- Don't use Slider for precise numeric entry — pair it with or replace it by an Input[type=number].
- Don't omit min/max/step; relying on defaults can produce a mismatched scale for the data.
- Don't use Slider on touch targets smaller than the thumb's hit area — keep the track full width.
Input OTP
input-otpOne-time-password entry built on the input-otp library. Each slot reflects a single character and shares the same border and ring tokens as Input.
6-digit code
A single group of six slots for a verification code.
Renders as a single native input under the hood, so autofill, paste, and SMS one-time-code suggestions work as expected; each slot shows a fake caret (hasFakeCaret) only while active.
Grouped with separator
An invite code split 4+4 with InputOTPSeparator marking the boundary.
Disabled
Recommended
- Set maxLength to match the exact code length your backend issues.
- Group slots with InputOTPSeparator when the issued format has a visual break (e.g. XXXX-XXXX).
- Trigger validation once maxLength is reached via onComplete rather than on every keystroke.
Avoid
- Don't use InputOTP for free-form text or passwords — it's purpose-built for fixed-length codes.
- Don't disable paste; pasting a full code from SMS or a password manager is the primary use case.
- Don't add a visible label per slot — describe the whole field once with a Field/FieldLabel wrapper.
Calendar
calendarDate picker surface built on react-day-picker. Selected, range, and disabled days are token-driven and compose into DatePicker/DateRangePicker.
Single date
Controlled single-date selection.
Arrow keys move focus between days, PageUp/PageDown change month, and Home/End jump to the start/end of the week — all native to react-day-picker.
Range, two months
numberOfMonths={2} shows the current and next month side by side for range selection.
Two months sit side by side from md upward; below md they stack vertically so neither month gets clipped.
Disabled past dates
disabled={{ before: new Date() }} blocks selection of any day before today.
Disabled days are still visible for context but are unreachable via keyboard focus and announced as unavailable.
Chevron icons mirror automatically in RTL layouts via rtl:**:[.rdp-button_next>svg]:rotate-180.
Recommended
- Use the disabled matcher (e.g. { before: new Date() }) to block out-of-range dates instead of validating after the fact.
- Prefer the DatePicker/DateRangePicker composition when the calendar should live in a popover, not inline.
- Set numberOfMonths={2} for range selection so both endpoints can be picked without navigating.
Avoid
- Don't render Calendar inline in dense forms — it takes real estate; use DatePicker's popover instead.
- Don't forget locale when the app supports non-English formats; pass locale to align month/weekday names.
- Don't mutate the selected Date/DateRange in place — always create a new value in onSelect.
Combobox
comboboxPopover + Command composition for searchable single selection. Unlike Select, options can be filtered by typing, via cmdk.
Searchable list
Type to filter frameworks by label.
The trigger exposes role="combobox" with aria-expanded; the filtered list is a Command list with roving focus.
With a disabled option
Individual options can be marked disabled and stay unselectable while remaining visible.
Disabled
Recommended
- Reach for Combobox once the option list is long enough that scanning beats scrolling — search filters it instantly.
- Keep emptyText actionable (e.g. "No framework found.") instead of a generic message.
- Use disabled on individual options for choices that exist but are temporarily unavailable.
Avoid
- Don't use Combobox for short, fixed lists (under ~8 items) — a plain Select is simpler and needs no typing.
- Don't forget a clear placeholder; an empty trigger with no context reads as broken.
- Don't rely on the visual checkmark alone — CommandItem already carries the selected state for assistive tech.
Date Picker
date-picker · date-range-pickerPopover + Calendar + date-fns composition for picking a single date or a range from a compact trigger button.
Single date
Formats the selected date with date-fns (PPP) once chosen.
Date range
Shows a formatted 'from – to' label and opens a two-month calendar.
The popover content sizes to the calendar's natural width and repositions to stay inside the viewport on narrow screens.
Disabled
Both pickers inherit Calendar's native keyboard navigation once opened; the trigger itself is a Button, so it's focusable and operable with Enter/Space.
Recommended
- Use DatePicker/DateRangePicker for compact forms; drop to a bare Calendar when space allows an always-visible picker.
- Show the formatted value in the trigger itself so the current selection never requires opening the popover to confirm.
- Constrain selectable dates via the underlying Calendar's disabled matcher for booking-style flows.
Avoid
- Don't parse or format dates manually in consuming code — the components already use date-fns internally.
- Don't nest a DatePicker inside another popover/dialog without checking focus return on close.
- Don't use DateRangePicker when only a single date is ever needed — the wider trigger and two-month calendar add noise.
Form
react-hook-form + zodThis shadcn style (base-nova) has no dedicated Form wrapper component — there is no components/ui/form.tsx in the registry. Instead, forms are composed directly from the Field primitives (Field, FieldGroup, FieldLabel, FieldDescription, FieldError) wired to react-hook-form's useForm and validated declaratively with a zod schema via @hookform/resolvers/zod.
Profile form
A complete, functional form: react-hook-form manages field state and submission, zodResolver validates against the schema below, and each Field reports its own error through FieldError bound to form.formState.errors.
FieldError uses role='alert' and is associated with the control via aria-invalid/aria-describedby when present; react-hook-form re-validates on blur/change according to the configured mode.
FieldGroup stacks fields vertically by default; use Field orientation='responsive' for fields that sit side by side and collapse on mobile.
Single field with a visible error
For the raw, uncontrolled Field/FieldLabel/FieldDescription/FieldError composition pattern with an error already shown (no interaction required), see the 'Field: label, description, error' example in the Input & Field section above — the same data-invalid + aria-invalid + FieldError wiring applies here, just driven by form.formState.errors instead of a static prop.
Recommended
- Use zodResolver to validate declaratively against a single schema instead of hand-rolled validation logic.
- Bind FieldError to the errors from form.formState so messages stay in sync with react-hook-form state.
- Disable the submit control while form.formState.isSubmitting is true when submission is asynchronous.
Avoid
- Don't validate only on submit without inline feedback — surface errors as the user fills the form.
- Don't use generic error messages like "Invalid" — be specific, as in the schema above ("Username must be at least 2 characters.").
- Don't mix native HTML5 validation (required, pattern, minLength) with zod on the same field — pick one source of truth.
Avatar
AvatarRepresents a user or entity. Falls back to initials (or an icon) when no image is available, and can be grouped or badged with a status indicator.
Sizes
Three sizes for different densities — tables, lists, and profile headers.
Image & fallback
AvatarImage renders when it loads; AvatarFallback covers loading/error states and entities with no photo.
Provide meaningful alt text on AvatarImage; decorative icon fallbacks should be aria-hidden.
With status badge
AvatarBadge overlays a status dot or icon; scales with the avatar size.
A status conveyed only by color needs a text equivalent nearby (e.g. in a tooltip or sr-only span).
Avatar group
Overlapping stack for a compact list of participants, with an overflow count.
Recommended
- Always provide a fallback (initials or icon) for failed/missing images.
- Keep group counts accurate and clickable when they open a full list.
Avoid
- Do not rely on a status badge color as the only signal of state.
- Avoid mixing avatar sizes within the same group.
Card
CardA general-purpose container for grouping related content and actions. Composes header, content, footer and an optional action slot.
Basic
Header, content and footer — the most common composition.
With action
CardAction anchors a control (menu, toggle) to the top-right of the header grid.
Compact (size='sm')
Tighter padding for dense dashboards and sidebars.
Interactive / clickable
A card can act as a navigable surface. Keep focus and hover feedback visible.
If the whole card is clickable, wrap it in a single interactive element (link or button) rather than nesting interactive children.
Recommended
- Use CardHeader/CardContent/CardFooter to keep spacing consistent across the catalog.
- Reserve CardAction for a single, secondary control (menu, toggle) — not a primary CTA.
Avoid
- Do not nest another interactive card or link inside a fully clickable card.
- Avoid stuffing unrelated content into one card — split into multiple cards instead.
Table
TableDisplays tabular data with header, body, optional footer and caption. A semantic HTML table under the hood, horizontally scrollable on narrow screens.
Standard table
Header row, body rows and a footer summarizing a column.
| Invoice | Status | Method | Amount |
|---|---|---|---|
| INV001 | Paid | Credit card | $250.00 |
| INV002 | Pending | PayPal | $150.00 |
| INV003 | Failed | Bank transfer | $350.00 |
| Total | $750.00 | ||
Use <TableHead> (th) for column headers so assistive tech announces them when navigating cells.
The table container scrolls horizontally rather than truncating columns on narrow viewports.
Sortable header & row states
Clickable column headers signal sort order; hover and selected states help scanning.
| Name | |
|---|---|
| Default row | $100 |
| Selected row | $200 |
Recommended
- Right-align numeric columns for scannability.
- Pair status columns with color + text/badge, not color alone.
Avoid
- Do not use a table for single-record layouts — use a description list instead.
- Avoid more than ~6 columns on typical viewports; consider a card layout on mobile.
Accordion
AccordionVertically stacked, collapsible sections. Supports single or multiple open panels at once, and can start with an item expanded.
Single (default)
Only one panel open at a time — good for FAQs and settings groups.
Trigger is a real button with aria-expanded; ChevronDown/Up swap to reflect state.
Multiple open
The multiple prop allows several sections to be expanded simultaneously — useful for long-form reference content.
Recommended
- Use single-open mode for FAQs and settings groups where panels are mutually exclusive.
- Keep triggers as short, scannable phrases so users can find the right section at a glance.
Avoid
- Do not hide content a user needs immediately (e.g. a required form field) inside a collapsed panel.
- Avoid nesting accordions within accordions — flatten the hierarchy or use tabs instead.
Tabs
TabsOrganizes content into switchable panels sharing the same space. Two visual styles (default, line), two orientations, and full keyboard support.
Default style
Segmented, pill-style tab list — the most common pattern for dashboards and forms.
Uses the WAI-ARIA tabs pattern: arrow keys move focus, Tab enters/exits the tablist.
Line style
An underline indicator instead of a filled pill — reads as more editorial, less chrome-heavy.
Vertical orientation
Useful for settings screens where the list of sections is long relative to panel content.
RTL support
The whole catalog can flip direction from the sidebar control — tabs reorder and the active indicator follows the writing direction automatically.
Recommended
- Use tabs for peer content at the same level — not for a linear, multi-step flow.
- Keep tab labels to one or two words; use icons only alongside text, not instead of it, unless universally understood.
Avoid
- Do not put a primary call-to-action inside a tab a user might not open.
- Avoid more than 5–6 horizontal tabs; switch to vertical or a select on narrow screens.
Progress
ProgressCommunicates completion of a task with a determinate value, or an indeterminate state while duration is unknown.
Determinate values
A known percentage of completion.
Renders with role='progressbar' and aria-valuenow/min/max derived from the value prop.
With label & value
ProgressLabel and ProgressValue compose a header row above the track.
Indeterminate
Omit value while the duration is unknown; pair with a spinner or skeleton for the surrounding content.
Recommended
- Show a determinate value whenever the duration or completion ratio is knowable.
- Pair long-running progress with a text label describing what is happening (e.g. "Uploading").
Avoid
- Do not use progress bars for instantaneous actions — a spinner or disabled button state is enough.
- Avoid updating the value so rapidly that it becomes visually noisy; throttle updates instead.
Carousel
CarouselA motion-based slideshow for cycling through a set of items, built on Embla Carousel. Supports horizontal or vertical orientation, multiple visible slides, and keyboard navigation.
Basic
A single slide visible at a time, with Previous/Next controls.
The carousel root exposes role='region' and aria-roledescription='carousel'; each slide is role='group' with aria-roledescription='slide'. ArrowLeft/ArrowRight move slides when the carousel is focused.
Multiple slides per view
opts={{ align: 'start' }} combined with a fractional basis on CarouselItem shows several slides at once — useful for product grids or thumbnail rails.
Reduce the visible count on small screens (basis-1/2) and increase it on larger ones (md:basis-1/3) so items stay readable at every width.
Vertical orientation
Set orientation='vertical' and give the content a fixed height — useful for feeds or notification rails.
Recommended
- Always pair the carousel with visible Previous/Next controls — do not rely on swipe/drag alone.
- Reduce the number of simultaneously visible slides on narrow viewports so content stays legible.
Avoid
- Do not auto-advance slides that contain critical information without a pause/stop control.
- Avoid using a carousel for a short, fixed set of items that would read better as a static grid.
Data Table
DataTableA batteries-included table built on TanStack Table: sorting, per-column filtering, column visibility, row selection and pagination on top of the semantic Table primitive.
Payments
Sortable Amount column, a filter on Email, a column-visibility dropdown and pagination footer.
| Status | ||
|---|---|---|
| success | ken99@example.com | $316.00 |
| success | abe45@example.com | $242.00 |
| processing | monserrat44@example.com | $837.00 |
| success | silas22@example.com | $874.00 |
| failed | carmella@example.com | $721.00 |
| pending | brandon91@example.com | $129.00 |
| success | diana77@example.com | $458.00 |
| processing | jordan.lee@example.com | $592.00 |
0 of 8 row(s) selected.
Renders the same semantic table markup as the Table component (thead/th), so it inherits native table accessibility; the header sort control is a real button.
The toolbar (filter input + columns dropdown) wraps on narrow screens, and the table body scrolls horizontally rather than truncating columns.
Recommended
- Give at least one column a stable, human-readable filter (e.g. name or email) so the toolbar is useful.
- Reserve column-visibility toggles for tables with more columns than fit comfortably on screen.
Avoid
- Do not enable row selection without giving the checkbox column an accessible label.
- Avoid client-side sorting/filtering on very large datasets — paginate from the server instead.
Separator
SeparatorA thin visual divider between groups of content. Orientation determines whether it spans width (horizontal) or height (vertical), and it is purely decorative unless given semantic meaning.
Horizontal
Divides stacked blocks of content, such as sections of a settings list.
Design System
An open-source component catalog.
Decorative by default (role='none' via aria-orientation handling) — do not rely on it to convey structure to screen reader users; use headings instead.
Vertical in a toolbar
Groups related actions inside a horizontal flex container without a visible border.
Recommended
- Use a separator to group related content within the same visual region.
- Set orientation="vertical" only inside a flex/inline-flex container with a defined height.
- Prefer spacing (margin/gap) over separators when the grouping is already visually obvious.
Avoid
- Do not use a separator as a substitute for a semantic heading or landmark.
- Avoid stacking multiple separators in a row — simplify the layout instead.
Aspect Ratio
AspectRatioConstrains a child (typically an image or embed) to a fixed width/height ratio, so media does not shift layout while loading.
16 / 9 (widescreen)
The default for hero images, video thumbnails and link previews.
Provide a meaningful alt on the media element itself — the ratio wrapper is purely presentational.
1 / 1 (square)
Common for avatars, thumbnails and product grid tiles.
4 / 3
A slightly taller ratio, useful for classic photo grids and document previews.
Ratio is preserved at every viewport width — only the rendered pixel size changes, so media never jumps as it loads.
Recommended
- Reserve space for media with AspectRatio to prevent cumulative layout shift while it loads.
- Combine with object-cover on the inner image so it fills the box without distortion.
Avoid
- Do not nest interactive controls that need their own aspect ratio inside the same wrapper.
- Avoid arbitrary one-off ratios — stick to a small shared set (16/9, 1/1, 4/3) for visual consistency.
Scroll Area
ScrollAreaA styled, cross-browser-consistent scrollable viewport with a custom scrollbar and thumb, for both vertical and horizontal overflow.
Vertical list
Caps the height of a long list of items while keeping the surrounding layout fixed.
The viewport is keyboard-focusable and scrollable with arrow keys / Page Up-Down once focused, with a visible focus ring.
Horizontal gallery
Set the inner content to w-max and flex-row so it overflows horizontally; the scrollbar orientation follows the overflow direction.
On touch devices the gallery scrolls natively via swipe; the visible scrollbar is a desktop affordance layered on top.
Recommended
- Constrain height (vertical) or width (horizontal) with an explicit class so the scroll boundary is predictable.
- Use for self-contained regions (sidebars, previews, chat panes) rather than the whole page.
Avoid
- Do not nest a ScrollArea inside another scrollable ancestor without a clear visual boundary — nested scrolling confuses users.
- Avoid hiding critical actions below the fold of a scroll area with no visual cue that content continues.
Collapsible
CollapsibleToggles the visibility of a single content panel. Simpler than Accordion — no item list, just an open/closed boolean, ideal for 'show more' patterns.
Default
A trigger button expands and collapses the panel below it.
3 team members
CollapsibleTrigger renders as a real button with aria-expanded; the chevron rotates to reflect state without relying on the icon alone.
Closed by default — 'show more'
A list that reveals additional detail on demand, keeping the collapsed state compact for scanning.
Recommended
- Use for a single, independent show/hide panel — reach for Accordion when there are several mutually related items.
- Keep the trigger label or icon consistent with the current state ("Show details" vs "Hide details").
Avoid
- Do not hide content required to complete a primary task behind a collapsible.
- Avoid animating so slowly that repeated toggling feels sluggish.
Kbd
KbdRenders a keyboard key or shortcut combination as inline UI, typically next to a command, menu item or tooltip.
Single keys
Isolated keys for referencing a single input.
Kbd is visual only (pointer-events-none); it does not announce as a shortcut to screen readers — pair it with a visible or sr-only text label describing the action.
Combinations with KbdGroup
Group multiple keys to represent a chord, such as a save or search shortcut.
Inside a button
Surfaces the shortcut for an action right where the action lives, such as a command palette trigger.
Hide the shortcut hint below the sm breakpoint (className='hidden sm:inline-flex') since most touch devices have no physical keyboard.
Inside a tooltip
Kbd automatically adapts its background/foreground when nested in a tooltip, staying legible against the inverted surface.
Recommended
- Use Kbd to document real, working keyboard shortcuts — keep it in sync with the actual key handler.
- Group multi-key chords with KbdGroup rather than a single Kbd containing "Ctrl+K".
Avoid
- Do not use Kbd as a generic small-badge component for unrelated labels — it implies a literal key.
- Avoid showing shortcuts that only work on desktop as the sole affordance on touch layouts.
Item
ItemA generic, composable row primitive for lists of files, contacts, notifications or settings — media on the left, title/description in the middle, actions on the right.
File list
ItemGroup stacks Items separated by ItemSeparator; ItemMedia holds an icon, ItemContent the text, ItemActions the trailing controls.
2.4 MB · Updated 2 days ago
860 KB · Updated 5 days ago
4 KB · Updated just now
ItemGroup renders role='list' so assistive tech announces the number of entries; keep each Item's action reachable by keyboard (a real button, not a div with onClick).
ItemContent truncates title to one line and description to two via line-clamp, so rows stay a predictable height as the list narrows on mobile.
Visual variants
default is borderless, outline adds a visible border, muted fills the row with a subtle surface — pick one per list, not mixed within it.
Sizes
default/sm share spacing; xs tightens gaps and drops the description's line-height for very dense lists, like a command menu.
Standard row height
Compact row height
RTL support
ItemActions stays on the trailing edge in either direction — in RTL that reads as the visual left, since 'trailing' follows text direction rather than a fixed side.
من فريق الدعم · منذ 3 ساعات
Recommended
- Keep one variant per list for visual consistency; switch variant only to distinguish a different list context.
- Put the single most relevant action in ItemActions and move the rest into a menu behind MoreHorizontal.
- Use ItemMedia variant="image" for entities with a photo (contacts, products) and variant="icon" for types/categories.
Avoid
- Do not put more than one or two primary actions directly in ItemActions — it competes for attention and breaks on narrow screens.
- Avoid using Item for a single, one-off row — it is built for repeated, list-shaped content.
Resizable
ResizableA panel group with draggable handles for user-adjustable layouts — sidebars, split editors, or preview panes. Built on react-resizable-panels.
Horizontal
Two side-by-side panels with a shared draggable handle; defaultSize is a percentage of the group.
ResizableHandle is keyboard-operable: focus it and use the arrow keys to resize in fixed increments.
Vertical
orientation='vertical' stacks panels top-to-bottom and the handle drags along the y axis.
Nested groups — editor layout
A group inside a panel composes a three-pane layout: a fixed sidebar, and a vertical split between editor and preview.
Below a comfortable width, prefer collapsing this into stacked, non-resizable sections (or tabs) rather than shrinking three live panels — dragging thin handles is impractical on touch.
RTL support
In a right-to-left context the first panel renders on the visual right; the handle drag direction inverts to match, so 'grow the first panel' still means dragging toward the trailing edge.
Recommended
- Set a sensible minSize on every panel so a drag cannot collapse critical UI to zero width.
- Persist user-adjusted sizes (via onLayout) for layouts users return to often, like a code editor.
- Always render withHandle for a visible grab affordance — an invisible hairline handle is hard to discover.
Avoid
- Do not use Resizable for layouts that must remain pixel-perfect (e.g. a fixed-width brand rail).
- Avoid deeply nested panel groups beyond two levels — the interaction becomes hard to predict.
- Do not rely on resizable panels as the only way to reach content on small/touch screens — provide a stacked fallback.
Pagination
PaginationNavigates between pages of a paged list or table. Built from plain links/buttons so it can be wired to client-side or server-side routing.
Standard
Previous/Next plus numbered links; the active page uses the outline variant.
The current page link carries aria-current='page'; Previous/Next expose descriptive aria-labels.
Compact (mobile)
Only Previous/Next, with the label hidden below the sm breakpoint — the built-in responsive behaviour of PaginationPrevious/Next.
Text labels ('Previous', 'Next') hide under the sm breakpoint automatically, leaving icon-only controls on small screens.
Disabled edges
On the first or last page, disable the corresponding control rather than hiding it.
Recommended
- Show enough page numbers to orient the user, collapsing the middle with an ellipsis for long ranges.
- Disable, rather than remove, Previous/Next at the boundaries.
Avoid
- Do not use pagination for infinite or continuously loading feeds — prefer a "Load more" pattern or infinite scroll.
Dialog
DialogA modal window that interrupts the flow to request a decision or focused input. Traps focus, dims the page, and closes on escape, backdrop click, or an explicit action.
Standard
Header, description and a footer with cancel/confirm actions.
Focus is trapped inside the popup while open and returns to the trigger on close; the title is announced via DialogTitle.
Destructive confirmation
Irreversible actions should always require an explicit confirmation step.
Without close button
showCloseButton={false} forces an explicit choice via footer actions — use sparingly, for critical decisions only.
Recommended
- Reserve dialogs for focused decisions or short forms that block the current flow.
- Give every dialog a clear title and, for destructive actions, an unambiguous confirm label ("Delete account", not just "OK").
Avoid
- Do not stack dialogs on top of dialogs — use a single modal at a time.
- Avoid long forms or scrollable content in a dialog; use a full page or a Sheet instead.
Tooltip
TooltipA small floating label that appears on hover or keyboard focus to describe an element — typically an icon-only control. Never contains interactive content.
Sides
Position relative to the trigger: top, right, bottom, left.
Appears on both hover and keyboard focus; disappears on Escape. Do not put a tooltip's only content behind hover, since it must also work for keyboard and touch users — pair icon-only buttons with aria-label too.
With keyboard shortcut
Combine a short description with a Kbd-style shortcut hint.
Recommended
- Use tooltips to label icon-only controls or clarify truncated text.
- Keep content to a short phrase — a few words, not a paragraph.
Avoid
- Do not put essential information only in a tooltip; touch devices have no hover.
- Never place interactive elements (buttons, links) inside a tooltip — use a Popover instead.
Alert Dialog
AlertDialogAn interruptive modal reserved for critical decisions. Unlike Dialog, it has no close button and does not dismiss on backdrop click or escape by accident — the user must explicitly cancel or confirm.
Destructive confirmation
The default pattern for an irreversible action: title, description, cancel and a destructive confirm.
Focus moves to the popup on open and is trapped there; AlertDialogTitle and AlertDialogDescription are announced together so screen reader users hear the full consequence before choosing.
Compact size
size="sm" renders a narrower popup with the footer actions laid out as a 2-column grid.
With media icon
AlertDialogMedia adds a decorative icon to reinforce the severity of the decision.
Recommended
- Reserve AlertDialog for decisions with real consequences (delete, discard, sign out) where an accidental dismiss would be worse than an extra click.
- Label the confirm action with the verb being performed ("Delete", "Discard") rather than a generic "OK".
- Use size="sm" for binary yes/no choices to keep the footer buttons visually balanced.
Avoid
- Do not use AlertDialog for routine forms or non-destructive flows — it removes the escape hatches users expect from Dialog.
- Do not skip AlertDialogCancel; always give an obvious way out that does not perform the action.
Popover
PopoverA floating panel anchored to a trigger, opened by click rather than hover. Unlike Tooltip or HoverCard, it can hold interactive content such as forms and buttons.
Standard
Header, title, description and a single action.
Opens on click or Enter/Space on the trigger; Escape closes it and returns focus to the trigger.
With a form
Interactive controls (inputs, buttons) are safe inside a Popover — this is the key difference from Tooltip/HoverCard.
Sides
side="top" | "right" | "bottom" | "left" controls where the popup opens relative to the trigger.
Recommended
- Use Popover when the floating content needs to be clicked, typed into, or otherwise interacted with.
- Keep the panel focused on one task — a short form or a single decision, not a full page.
Avoid
- Do not use Popover just to show a label or description with no interaction — use Tooltip or HoverCard instead.
- Avoid nesting a Popover trigger inside another overlay unless the interaction really requires it; prefer a single layer of floating UI.
Hover Card
HoverCardA rich preview panel that opens on hover or keyboard focus, without navigating away. Typically used to preview a user, link or entity referenced inline.
User preview
Hovering (or focusing via keyboard) a mention reveals a preview card with avatar, name and bio.
Reported by @shadcn two hours ago.
Also opens on keyboard focus so it is reachable without a mouse, but — like Tooltip — it never works on touch. Never make it the only way to reach information a user needs.
Recommended
- Use HoverCard to give extra context about an inline reference (user, link, term) without a navigation.
- Keep the preview lightweight — avatar, name, one or two lines; it should not require scrolling.
Avoid
- Do not rely on HoverCard to expose information or actions unavailable elsewhere — touch devices cannot hover.
- Do not put form controls or destructive actions inside a HoverCard — use Popover for anything interactive.
Drawer
DrawerA panel that slides in and can be swiped away, anchored to an edge of the viewport. The mobile-first counterpart to Dialog — well suited to touch gestures.
Bottom drawer
The default swipeDirection="down" slides up from the bottom, with a swipe handle for touch users.
Focus is trapped in the drawer while open; the swipe handle is decorative (aria-hidden) — keyboard and screen reader users close it via DrawerClose or Escape.
Prefer Drawer over Dialog on small/touch screens: it anchors to an edge, supports swipe-to-dismiss, and avoids the disorientation of a centered modal on a small viewport.
Side drawer
swipeDirection="right" (or "left") turns the drawer into a horizontal panel, useful for secondary navigation on touch devices.
Recommended
- Use Drawer for actions and short forms on mobile/touch layouts where a swipe gesture feels native.
- Pair showSwipeHandle with a bottom drawer so touch users have a clear affordance to drag.
Avoid
- Do not use Drawer for large, complex forms — break the task down or use a full page instead.
- Avoid mixing swipe directions inconsistently across the same product; pick one per surface (e.g. always bottom on mobile).
Sheet
SheetA panel that slides in from an edge of the screen and stays there, without the swipe-to-dismiss gesture of Drawer. Ideal for navigation, filters and side forms on desktop.
Sides
side="top" | "right" | "bottom" | "left" — right is the default.
Focus is trapped while open and returns to the trigger on close; the close button carries a visually-hidden label for screen readers.
Filters form
A common pattern: a lateral form that narrows a list without leaving the page.
On narrow viewports a right/left Sheet still works well since it caps its width at max-w-sm and becomes effectively full-width below that breakpoint.
Recommended
- Use Sheet for filters, secondary navigation, or forms that benefit from staying anchored to an edge alongside the page content.
- Choose the side that matches the source of the trigger (e.g. a "Filters" button on the right opens a right Sheet).
Avoid
- Do not use Sheet for critical, must-answer decisions — it can be dismissed via backdrop click like Dialog; use AlertDialog for those.
- Avoid stacking a Sheet on top of another Sheet or Dialog; keep a single overlay layer active at a time.
Command
Command / CommandDialogA searchable list of commands, filtered as the user types. Use Command inline for an embedded command list, or CommandDialog to open it as a full command palette.
Inline command list
Command rendered directly on the page — search input, grouped items, shortcuts and a separator.
Arrow keys move the highlighted item, Enter selects it; CommandEmpty is announced when no results match the query.
Command palette
CommandDialog wraps Command in a Dialog controlled via open/onOpenChange — the classic ⌘K palette pattern.
Command Palette
Search for a command to run...
Bound here to ⌘K / Ctrl+K as well as the button; the dialog title and description are visually hidden but still announced to screen readers. Escape closes the palette.
Recommended
- Use CommandDialog for a global, keyboard-driven entry point (⌘K) to navigation and actions across the app.
- Group related items with CommandGroup and show shortcuts with CommandShortcut so power users can act without the mouse.
- Always provide CommandEmpty feedback so a no-match search state is not silent.
Avoid
- Do not bury actions only inside the command palette — it should accelerate existing UI, not replace visible affordances.
- Avoid overly long, unstructured lists; group and label items so filtering stays meaningful as the list grows.
Alert
AlertAn inline, persistent message calling attention to information the user should notice while scanning a page. Unlike a toast, it does not disappear on its own.
Default & destructive
The component ships two built-in variants; feedback colors extend it via className.
Rendered with role='alert' so assistive tech announces it when it enters the DOM.
Semantic feedback colors
Success, warning and info states use the corresponding tokens directly on top of the base component.
With action
AlertAction anchors a control (button, close icon) to the top-right corner.
Recommended
- Reserve destructive/warning styling for genuine problems that need attention.
- Keep the description short and actionable; link to more detail rather than pasting it in.
Avoid
- Do not use an Alert for transient confirmations — use a Toast instead.
- Avoid stacking more than 2–3 alerts on a single view.
Toast (Sonner)
SonnerA transient, non-blocking notification for confirming an action or reporting a background event. Stacks, auto-dismisses, and never traps focus.
Variants
Click to fire each toast type. All are announced politely without stealing focus from the page.
Rendered in a live region so screen readers announce new toasts without interrupting the current task.
With description & action
A title, supporting detail and an inline action button.
Loading / promise
Shows a spinner state, then resolves to success or error based on the promise outcome.
Recommended
- Use toasts for confirmations of actions the user just took ("Saved", "Copied").
- Keep messages under ~1 short sentence; move detail to an in-app log if needed.
Avoid
- Do not use a toast for anything the user must act on immediately — use a Dialog or Alert.
- Avoid firing more than one toast per user action.
Skeleton & Spinner
Skeleton / SpinnerTwo ways to signal loading. Skeletons preview the shape of content that's about to appear (better for layout stability); spinners indicate indeterminate activity with no known shape.
Skeleton — content shapes
Match the skeleton's shape and size to the content it will be replaced by, to avoid layout shift.
Skeleton — card
A composed loading state for a card-like layout.
Spinner — sizes & inline use
For indeterminate waits: page loads, button submissions, or small inline areas.
Carries role='status' and an aria-label of 'Loading' by default.
Recommended
- Prefer skeletons for content-heavy areas (feeds, tables, cards) to reduce perceived load time.
- Use a spinner for short, indeterminate waits inside a button or small region.
Avoid
- Do not leave a skeleton or spinner visible for more than a few seconds without feedback — add a timeout message.
- Avoid mixing skeleton and spinner for the same loading region.
Empty State
EmptyFills the space where content would normally be when there is nothing to show yet — first use, a cleared inbox, or a search with no results — and offers a clear next step.
No data yet
First-run state encouraging the primary action.
No search results
Confirms the search ran, explains why nothing matched, and offers a way to reset.
Cleared inbox
A positive, low-key empty state — not every empty state signals a problem.
Recommended
- Explain why the area is empty and what to do next, when there is a clear next step.
- Match the tone to the situation — neutral/positive for "nothing to do", helpful for "nothing found yet".
Avoid
- Do not leave a blank area with no explanation — it reads as a bug.
- Avoid a heavy illustration when a simple icon communicates the state just as well.