# Postede UI design system
Source-owned shadcn/ui with Radix primitives. Installed source is authoritative; these examples are composition recipes, not complete application state or persistence implementations.
## Rules
- Read AGENTS.md, docs/lab-design-system.md and the relevant /design component page before changing UI. Installed source in src/components/ui is the API authority.
- Use source-owned shadcn/Radix components from @/components/ui. Prefer named variants and semantic theme tokens; preserve Postede’s existing palette, typography, spacing and responsive behavior.
- Compose Field, FieldLabel, a control, FieldDescription and FieldError for forms. Preserve names, values, validation, refs, submit types and pending/error behavior.
- Give dialogs a title, avatars a fallback and icon buttons an accessible name. Keep SelectItem in SelectGroup, menu items in DropdownMenuGroup and TabsTrigger in TabsList.
- Use the shared toast API from @/features/ui/toast. It preserves async actions, retries and native-modal placement through Sonner.
- Use NativeDialog, NativeCheckbox, NativeSwitch and inline surface adapters only when preserving an existing host contract. Prefer declarative primitives for new independent controls; do not spread unstyled as a default.
- Keep Tiptap, drag-and-drop scheduling, charts, provider previews and Posty as product compositions. Reuse their shared controls rather than replacing their interaction engines.
- Add missing official components with the shadcn CLI, inspect generated files, and preserve local adaptations. Never reinitialize or overwrite customized primitives to add a component.
- Update the component reference, example and agent reference together when a primitive API changes. Run npm run ui:audit, appropriate checks, and real-browser desktop/mobile verification; distinguish tested behavior from inference.
## Workflow
- Inspect: Find the matching component and read its installed source. Check existing product usage before creating a new abstraction.
- Compose: Start from the documented import and example. Use named variants, semantic tokens and the correct compound structure.
- Preserve: Keep form, focus, state, cancellation and responsive contracts intact. Use compatibility adapters only where those contracts require them.
- Verify: Exercise keyboard, Escape, disabled/pending/error states and narrow layouts. Run the UI audit and relevant tests, then update the reference.
## Foundations
- Canvas: --background — Page background
- Text: --foreground — Primary text
- Surface: --card — Panels and cards
- Elevated: --popover — Menus and overlays
- Field: --field — Text controls
- Muted: --muted-foreground — Supporting text
- Primary: --primary — Primary actions
- Selected: --accent-commit — Active and selected states
- Danger: --destructive — Destructive actions and errors
- Border: --border — Quiet boundaries
## Button
Run an action or submit a form.
Page: /design/button
Source: src/components/ui/button.tsx
Import: @/components/ui/button
API: variant: default, outline, secondary, ghost, destructive, link; size: xs, sm, default, lg, icon variants; asChild; disabled
Accessibility: Use an accessible name for icon actions. Keep a visible focus indicator.
```tsx
import { Button } from "@/components/ui/button";
<>
>
```
## ButtonGroup
Join related actions without changing their individual behavior.
Page: /design/button-group
Source: src/components/ui/button-group.tsx
Import: @/components/ui/button-group
API: orientation: horizontal or vertical; standard div attributes
Accessibility: Use distinct action labels; do not use a button group as a radio group.
```tsx
import { ButtonGroup } from "@/components/ui/button-group";
import { Button } from "@/components/ui/button";
<>
>
```
## Input
Enter one line, search, or retain a browser-native input contract.
Page: /design/input
Source: src/components/ui/input.tsx
Import: @/components/ui/input
API: Native input props: type, name, value/defaultValue, required, disabled, ref; unstyled composition
Accessibility: Keep labels associated and names stable. File, hidden, color, date/time and native radio types retain browser behavior.
```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
<>
Project name
>
```
## Textarea
Enter plain multiline content.
Page: /design/textarea
Source: src/components/ui/textarea.tsx
Import: @/components/ui/textarea
API: Native textarea props, rows, value/defaultValue, required, ref
Accessibility: Do not replace the rich-text composer with Textarea. Keep an error description associated.
```tsx
import { Field, FieldLabel } from "@/components/ui/field";
import { Textarea } from "@/components/ui/textarea";
<>
Notes
>
```
## Field
Group a label, control, description and error.
Page: /design/field
Source: src/components/ui/field.tsx
Import: @/components/ui/field
API: FieldGroup, FieldSet, FieldLegend, FieldLabel, FieldDescription, FieldError; data-invalid; data-disabled
Accessibility: Set aria-invalid on the control and connect help/error IDs with aria-describedby.
```tsx
import { Field, FieldLabel, FieldError } from "@/components/ui/field";
import { Input } from "@/components/ui/input";
<>
Email{invalid ? "Enter a valid email." : null}
>
```
## Label
Name a control, including an existing composed field.
Page: /design/label
Source: src/components/ui/label.tsx
Import: @/components/ui/label
API: htmlFor, asChild, native label attributes; unstyled composition
Accessibility: A placeholder alone is not a label. Prefer FieldLabel inside Field.
```tsx
import { Label } from "@/components/ui/label";
import { Checkbox } from "@/components/ui/checkbox";
<>
>
```
## Select
Choose one option from a list.
Page: /design/select
Source: src/components/ui/select.tsx
Import: @/components/ui/select
API: value/defaultValue, onValueChange, name, required, disabled; SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectItem
Accessibility: Keep items inside a group. Use the dialog portal container for a select inside a native modal.
```tsx
import { Select, SelectTrigger, SelectValue, SelectContent, SelectGroup, SelectItem } from "@/components/ui/select";
<>
>
```
## Checkbox
Toggle an independent selection.
Page: /design/checkbox
Source: src/components/ui/checkbox.tsx
Import: @/components/ui/checkbox
API: checked/defaultChecked, onCheckedChange, disabled, name; checked may be indeterminate
Accessibility: Use NativeCheckbox composition when checked CSS selectors, native change events, form behavior or input refs must be retained.
```tsx
import { Checkbox } from "@/components/ui/checkbox";
import { Label } from "@/components/ui/label";
<>
>
```
## Switch
Enable or disable an immediate setting.
Page: /design/switch
Source: src/components/ui/switch.tsx
Import: @/components/ui/switch
API: checked/defaultChecked, onCheckedChange, disabled; size: sm or default
Accessibility: Use a stable setting label. NativeSwitch preserves existing checkbox-input event and form contracts.
```tsx
import { Switch } from "@/components/ui/switch";
import { Label } from "@/components/ui/label";
<>
>
```
## RadioGroup
Select exactly one option.
Page: /design/radio-group
Source: src/components/ui/radio-group.tsx
Import: @/components/ui/radio-group
API: value/defaultValue, onValueChange, orientation; RadioGroupItem value
Accessibility: Group with a meaningful accessible name. Existing native radio fields use Input to retain browser name grouping and events.
```tsx
import { RadioGroup, RadioGroupItem } from "@/components/ui/radio-group";
import { Label } from "@/components/ui/label";
<>
>
```
## Slider
Adjust a bounded numeric value.
Page: /design/slider
Source: src/components/ui/slider.tsx
Import: @/components/ui/slider
API: value/defaultValue arrays, onValueChange, min, max, step, disabled
Accessibility: Name every thumb and show the current value as text.
```tsx
import { Slider } from "@/components/ui/slider";
<>
>
```
## ToggleGroup
Switch between a small set of peer options.
Page: /design/toggle-group
Source: src/components/ui/toggle-group.tsx
Import: @/components/ui/toggle-group
API: type: single or multiple; value/defaultValue; onValueChange; ToggleGroupItem
Accessibility: Use an accessible group label. Use Tabs when options reveal associated panels.
```tsx
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group";
<>
ListGrid
>
```
## Toggle
Press or release a single optional mode.
Page: /design/toggle
Source: src/components/ui/toggle.tsx
Import: @/components/ui/toggle
API: pressed/defaultPressed, onPressedChange, disabled; variant and size
Accessibility: Expose the mode name independently of its pressed state.
```tsx
import { Toggle } from "@/components/ui/toggle";
<>
Bold
>
```
## Tabs
Switch related views in the same context.
Page: /design/tabs
Source: src/components/ui/tabs.tsx
Import: @/components/ui/tabs
API: value/defaultValue, onValueChange, activationMode, orientation; TabsList, TabsTrigger, TabsContent
Accessibility: Keep trigger values and panel IDs linked. Existing hosts retain their selection and keyboard handlers through asChild composition.
```tsx
import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs";
<>
PreviewDetailsPreview contentDetails content
>
```
## Accordion
Expand related sections independently or one at a time.
Page: /design/accordion
Source: src/components/ui/accordion.tsx
Import: @/components/ui/accordion
API: type: single or multiple; collapsible; value/defaultValue; AccordionItem, AccordionTrigger, AccordionContent
Accessibility: Use a real heading hierarchy and concise trigger labels.
```tsx
import { Accordion, AccordionItem, AccordionTrigger, AccordionContent } from "@/components/ui/accordion";
<>
DetailsSupporting context.
>
```
## Collapsible
Show optional detail within the current task.
Page: /design/collapsible
Source: src/components/ui/collapsible.tsx
Import: @/components/ui/collapsible
API: open/defaultOpen, onOpenChange; CollapsibleTrigger, CollapsibleContent
Accessibility: Disclosure composes native details/summary so open selectors, default opening, SSR and retained form state keep working.
```tsx
import { Collapsible, CollapsibleTrigger, CollapsibleContent } from "@/components/ui/collapsible";
import { Button } from "@/components/ui/button";
<>
Supporting context.
>
```
## Dialog
Complete a focused task in an overlay.
Page: /design/dialog
Source: src/components/ui/dialog.tsx
Import: @/components/ui/dialog
API: open/defaultOpen, onOpenChange; DialogTrigger, DialogContent, DialogTitle, DialogDescription, DialogClose
Accessibility: Always name the dialog, manage focus deliberately, and test Escape and nested overlays.
```tsx
import { Dialog, DialogTrigger, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogClose } from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
<>
>
```
## NativeDialog
Keep an existing native top-layer dialog and imperative ref.
Page: /design/native-dialog
Source: src/components/ui/native-dialog.tsx
Import: @/components/ui/native-dialog
API: Native dialog props, ref.showModal(), ref.close(), onCancel, onClose
Accessibility: Composes DialogContent asChild. Native cancellation guards and backdrop styling remain authoritative; do not substitute it for every new dialog.
```tsx
import { NativeDialog } from "@/components/ui/native-dialog";
import { Button } from "@/components/ui/button";
<>
Edit details
{/* Existing host opens with dialogRef.current?.showModal(). */}
>
```
## DialogSurface
Compose an existing host-managed product overlay.
Page: /design/dialog-surface
Source: src/components/ui/dialog-surface.tsx
Import: @/components/ui/dialog-surface
API: One child carrying its existing ref, ARIA name, classes and event handlers
Accessibility: The host owns mounting, focus trapping and dismissal. Use declarative Dialog for an independent new modal.
```tsx
import { DialogSurface } from "@/components/ui/dialog-surface";
<>
{open ? (
{/* Existing focus management, layout and dismissal stay with the host. */}
) : null}
>
```
## AlertDialog
Confirm a consequential action with an explicit decision.
Page: /design/alert-dialog
Source: src/components/ui/alert-dialog.tsx
Import: @/components/ui/alert-dialog
API: open/onOpenChange; AlertDialogTitle, Description, Cancel, Action
Accessibility: Name the consequence and count affected objects. Never perform a destructive action in a demo.
```tsx
import { AlertDialog, AlertDialogTrigger, AlertDialogContent, AlertDialogHeader, AlertDialogTitle, AlertDialogDescription, AlertDialogFooter, AlertDialogCancel, AlertDialogAction } from "@/components/ui/alert-dialog";
import { Button } from "@/components/ui/button";
<>
Delete this record?This cannot be undone.CancelDelete record
>
```
## DropdownMenu
Offer commands attached to a trigger.
Page: /design/dropdown-menu
Source: src/components/ui/dropdown-menu.tsx
Import: @/components/ui/dropdown-menu
API: DropdownMenuTrigger, Content, Group, Item, CheckboxItem, RadioGroup, RadioItem, Separator
Accessibility: Group items. Use onSelect for commands and disabled for unavailable commands. MenuSurface preserves an existing host's inline positioning and dismissal.
```tsx
import { DropdownMenu, DropdownMenuTrigger, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem } from "@/components/ui/dropdown-menu";
import { Button } from "@/components/ui/button";
<>
Edit
>
```
## Popover
Show a small interactive tool near its trigger.
Page: /design/popover
Source: src/components/ui/popover.tsx
Import: @/components/ui/popover
API: open/defaultOpen, onOpenChange; PopoverTrigger, PopoverContent, PopoverAnchor
Accessibility: Provide a clear accessible name. Use Dialog when the task requires a modal workflow.
```tsx
import { Popover, PopoverTrigger, PopoverContent } from "@/components/ui/popover";
import { Button } from "@/components/ui/button";
<>
Contextual controls
>
```
## Tooltip
Explain a concise label or keyboard shortcut.
Page: /design/tooltip
Source: src/components/ui/tooltip.tsx
Import: @/components/ui/tooltip
API: TooltipProvider delayDuration; TooltipTrigger asChild; TooltipContent side, sideOffset
Accessibility: FastTooltip retains zero-delay product hints and delayed shortcut hints. TooltipSurface supports host-positioned chart and shortcut hints. Essential help belongs in visible text.
```tsx
import { TooltipProvider, Tooltip, TooltipTrigger, TooltipContent } from "@/components/ui/tooltip";
import { Button } from "@/components/ui/button";
<>
Search your workspace
>
```
## Card
Frame one surface or one repeated object.
Page: /design/card
Source: src/components/ui/card.tsx
Import: @/components/ui/card
API: CardHeader, CardTitle, CardDescription, CardContent, CardFooter, CardAction; size; asChild
Accessibility: DashboardCard and LabPanel compose Card with existing 8px surfaces. Avoid decorative nested cards.
```tsx
import { Card, CardHeader, CardTitle, CardDescription, CardContent, CardFooter } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
<>
Upcoming postsThis weekPost list
>
```
## Table
Compare structured records.
Page: /design/table
Source: src/components/ui/table.tsx
Import: @/components/ui/table
API: TableHeader, TableBody, TableFooter, TableRow, TableHead, TableCell, TableCaption
Accessibility: Keep header semantics and readable type. The unstyled variant retains existing scroll containers, column widths and row behavior.
```tsx
import { Table, TableCaption, TableHeader, TableRow, TableHead, TableBody, TableCell } from "@/components/ui/table";
<>
Recent draftsTitleStatusLaunch updateDraft
>
```
## Avatar
Identify a person or workspace in a stable frame.
Page: /design/avatar
Source: src/components/ui/avatar.tsx
Import: @/components/ui/avatar
API: AvatarImage src/alt, AvatarFallback; size
Accessibility: Always provide a fallback. Channel logos and provider-specific previews remain product compositions.
```tsx
import { Avatar, AvatarImage, AvatarFallback } from "@/components/ui/avatar";
<>
JP
>
```
## Badge
Emphasize status or a short category.
Page: /design/badge
Source: src/components/ui/badge.tsx
Import: @/components/ui/badge
API: variant: default, secondary, destructive, outline, ghost, link; asChild
Accessibility: Use words as well as color. Ordinary status can remain plain text; avoid making every value a badge.
```tsx
import { Badge } from "@/components/ui/badge";
<>
Draft
>
```
## Separator
Separate related groups without implying a new card.
Page: /design/separator
Source: src/components/ui/separator.tsx
Import: @/components/ui/separator
API: orientation: horizontal or vertical; decorative; asChild
Accessibility: Use decorative separators for visual spacing and a semantic separator only when the boundary conveys meaning.
```tsx
import { Separator } from "@/components/ui/separator";
<>
>
```
## Alert
Explain a persistent condition beside the affected task.
Page: /design/alert
Source: src/components/ui/alert.tsx
Import: @/components/ui/alert
API: AlertTitle, AlertDescription; variant: default or destructive
Accessibility: Describe a useful recovery action. Do not replace field-level errors with a distant alert.
```tsx
import { Alert, AlertTitle, AlertDescription } from "@/components/ui/alert";
<>
Changes savedYour draft is ready to review.
>
```
## Sonner
Acknowledge completed work or report an actionable failure.
Page: /design/sonner
Source: src/components/ui/sonner.tsx
Import: @/features/ui/toast
API: toast.success/error/info/show/dismiss; title, description, duration, optional async action
Accessibility: The shared toast store feeds Sonner while retaining action pending/retry, hover/focus pause, and native-modal placement.
```tsx
import { toast } from "@/features/ui/toast";
toast.success("Changes saved");
// Keep the shared renderer mounted once at the application root.
```
## Progress
Show measurable progress.
Page: /design/progress
Source: src/components/ui/progress.tsx
Import: @/components/ui/progress
API: value, max, aria-label; ProgressSurface for existing host-rendered meters
Accessibility: Expose a valid numeric value and maximum. Keep indeterminate work separate from measurable completion.
```tsx
import { Progress } from "@/components/ui/progress";
<>
>
```
## Skeleton
Reserve the shape of content while it loads.
Page: /design/skeleton
Source: src/components/ui/skeleton.tsx
Import: @/components/ui/skeleton
API: className for dimensions; asChild for an existing loading layout
Accessibility: Preserve layout and respect reduced motion. Do not announce every decorative skeleton.
```tsx
import { Skeleton } from "@/components/ui/skeleton";
<>
>
```
## Spinner
Indicate a short pending operation.
Page: /design/spinner
Source: src/components/ui/spinner.tsx
Import: @/components/ui/spinner
API: Standard SVG props; data-icon when composed in a button
Accessibility: Pair with useful pending text. Use one meaningful live announcement for the operation.
```tsx
import { Button } from "@/components/ui/button";
import { Spinner } from "@/components/ui/spinner";
<>
>
```
## Empty
Explain an empty result and its next useful action.
Page: /design/empty
Source: src/components/ui/empty.tsx
Import: @/components/ui/empty
API: EmptyHeader, EmptyMedia, EmptyTitle, EmptyDescription, EmptyContent
Accessibility: Distinguish no data from no filter matches. Keep one obvious next action.
```tsx
import { Empty, EmptyHeader, EmptyTitle, EmptyDescription, EmptyContent } from "@/components/ui/empty";
import { Button } from "@/components/ui/button";
<>
No drafts yetCreate your first draft.
>
```