Review UI code for alignment with the emcn design system — components, tokens, patterns, and conventions
Arguments:
User arguments: $ARGUMENTS
This codebase uses emcn, a custom component library built on Radix UI primitives with CVA (class-variance-authority) variants and CSS variable design tokens. All UI must use emcn components and tokens — never raw HTML elements or hardcoded colors.
apps/sim/components/emcn/components/index.ts to know what's availableapps/sim/app/_styles/globals.css for the full set of CSS variable tokens@/components/emcn@/components/emcn/icons or lucide-reactcn from @/lib/core/utils/cn for conditional class merging@/components/ui// Good
import { Button, Modal, Badge } from '@/components/emcn'
// Bad
import { Button } from '@/components/emcn/components/button/button'
Never use raw color values. Always use CSS variable tokens via Tailwind arbitrary values: text-[var(--text-primary)], not text-gray-500 or #333. The CSS variable pattern is canonical (1,700+ uses) — do not use Tailwind semantic classes like text-muted-foreground.
| Token | Use |
|---|---|
text-[var(--text-primary)] | Main content text |
text-[var(--text-secondary)] | Secondary/supporting text |
text-[var(--text-tertiary)] | Tertiary text |
text-[var(--text-muted)] | Disabled, placeholder text |
text-[var(--text-icon)] | Icon tinting |
text-[var(--text-inverse)] | Text on dark backgrounds |
text-[var(--text-error)] | Error/warning messages |
| Token | Use |
|---|---|
bg-[var(--bg)] | Page background |
bg-[var(--surface-2)] through bg-[var(--surface-7)] | Increasing elevation |
bg-[var(--surface-hover)] | Hover state backgrounds |
bg-[var(--surface-active)] | Active/selected backgrounds |
| Token | Use |
|---|---|
border-[var(--border)] | Default borders |
border-[var(--border-1)] | Stronger borders (inputs, cards) |
border-[var(--border-muted)] | Subtle dividers |
| Token | Use |
|---|---|
--success | Success states |
--error | Error states |
--caution | Warning states |
| Token | Use |
|---|---|
--brand-secondary | Brand color |
--brand-accent | Accent/CTA color |
Use shadow tokens, never raw box-shadow values:
shadow-subtle, shadow-medium, shadow-overlayshadow-kbd, shadow-cardUse z-index tokens for layering:
z-[var(--z-dropdown)] (100), z-[var(--z-modal)] (200), z-[var(--z-popover)] (300), z-[var(--z-tooltip)] (400), z-[var(--z-toast)] (500)Available variants: default, primary, destructive, ghost, outline, active, secondary, tertiary, subtle, ghost-secondary, 3d
| Action type | Variant | Frequency |
|---|---|---|
| Toolbar, icon-only, utility actions | ghost | Most common (28%) |
| Primary action (create, save, submit) | primary | Very common (24%) |
| Cancel, close, secondary action | default | Common |
| Delete, remove, destructive action | destructive | Targeted use only |
| Active/selected state | active | Targeted use only |
| Toggle, mode switch | outline | Moderate |
Sizes: sm (compact, 32% of buttons) or md (default, used when no size specified). Never create custom button styles — use an existing variant.
Buttons without an explicit variant prop get default styling. This is acceptable for cancel/secondary actions.
Use Modal + subcomponents. Never build custom dialog overlays.
<Modal open={open} onOpenChange={setOpen}>
<ModalContent size="sm">
<ModalHeader>Title</ModalHeader>
<ModalBody>Content</ModalBody>
<ModalFooter>
<Button variant="default" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="primary" onClick={handleSubmit}>Save</Button>
</ModalFooter>
</ModalContent>
</Modal>
Modal sizes by frequency: sm (440px, most common — confirmations and simple dialogs), md (500px, forms), lg (600px, content-heavy), xl (800px, rare), full (1200px, rare).
Footer buttons: Cancel on left (variant="default"), primary action on right. This pattern is followed 100% across the codebase.
Always use Modal with size="sm". The established pattern:
<Modal open={open} onOpenChange={setOpen}>
<ModalContent size="sm">
<ModalHeader>Delete {itemType}</ModalHeader>
<ModalBody>
<p>Description of consequences</p>
<p className="text-[var(--text-error)]">Warning about irreversibility</p>
</ModalBody>
<ModalFooter>
<Button variant="default" onClick={() => setOpen(false)}>Cancel</Button>
<Button variant="destructive" onClick={handleDelete} disabled={isDeleting}>
Delete
</Button>
</ModalFooter>
</ModalContent>
</Modal>
Rules:
text-[var(--text-error)] for warning text when the action is irreversiblevariant="destructive" for the action button (100% compliance)variant="default" for cancel (100% compliance)Use the imperative toast API from @/components/emcn. Never build custom notification UI.
import { toast } from '@/components/emcn'
toast.success('Item saved')
toast.error('Something went wrong')
toast.success('Deleted', { action: { label: 'Undo', onClick: handleUndo } })
Variants: default, success, error. Auto-dismiss after 5s. Supports optional action buttons with callbacks.
Use semantic color variants for status:
| Status | Variant | Usage |
|---|---|---|
| Error, failed, disconnected | red | Most common (15 uses) |
| Metadata, roles, auth types, scopes | gray-secondary | Very common (12 uses) |
| Type annotations (TS types, field types) | type | Very common (12 uses) |
| Success, active, enabled, running | green | Common (7 uses) |
| Neutral, default, unknown | gray | Common (6 uses) |
| Outline, parameters, public | outline | Moderate (6 uses) |
| Warning, processing | amber | Moderate (5 uses) |
| Paused, warning | orange | Occasional |
| Info, queued | blue | Occasional |
| Data types (arrays) | purple | Occasional |
| Generic with border | default | Occasional |
Use dot prop for status indicators (19 instances in codebase). icon prop is available but rarely used.
Use Tooltip from emcn with namespace pattern:
<Tooltip.Root>
<Tooltip.Trigger asChild>
<Button variant="ghost">{icon}</Button>
</Tooltip.Trigger>
<Tooltip.Content>Helpful text</Tooltip.Content>
</Tooltip.Root>
Use tooltips for icon-only buttons and truncated text. Don't tooltip self-explanatory elements.
Use for filters, option menus, and nested navigation:
<Popover open={open} onOpenChange={setOpen} size="sm">
<PopoverTrigger asChild>
<Button variant="ghost">Trigger</Button>
</PopoverTrigger>
<PopoverContent side="bottom" align="end" minWidth={160}>
<PopoverSection>Section Title</PopoverSection>
<PopoverItem active={isActive} onClick={handleClick}>
Item Label
</PopoverItem>
<PopoverDivider />
</PopoverContent>
</Popover>
Use for context menus and action menus:
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button variant="ghost">
<MoreHorizontal className="h-[14px] w-[14px]" />
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end">
<DropdownMenuItem onClick={handleEdit}>Edit</DropdownMenuItem>
<DropdownMenuSeparator />
<DropdownMenuItem onClick={handleDelete} className="text-[var(--text-error)]">
Delete
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
Destructive items go last, after a separator, in error color.
Use FormField wrapper for labeled inputs:
<FormField label="Name" htmlFor="name" error={errors.name} optional>
<Input id="name" value={name} onChange={e => setName(e.target.value)} />
</FormField>
Rules:
Input from emcn, never raw <input> (exception: hidden file inputs)Textarea from emcn, never raw <textarea>FormField for label + input + error layoutoptional propCombobox for searchable selectsTagInput for multi-value inputsUse Skeleton for content placeholders:
<Skeleton className="h-5 w-[200px] rounded-md" />
Rules:
rounded-md to match component radiusStandard sizing — h-[14px] w-[14px] is the dominant pattern (400+ uses):
<Icon className="h-[14px] w-[14px] text-[var(--text-icon)]" />
Size scale by frequency:
h-[14px] w-[14px] — default for inline icons (most common)h-[16px] w-[16px] — slightly larger inline iconsh-3 w-3 (12px) — compact/tight spacesh-4 w-4 (16px) — Tailwind equivalent, also commonh-3.5 w-3.5 (14px) — Tailwind equivalent of 14pxh-5 w-5 (20px) — larger icons, section headersUse text-[var(--text-icon)] for icon color (113+ uses in codebase).
cn() for conditional classes: cn('base', condition && 'conditional') — never template literal concatenation like `base ${condition ? 'active' : ''}`style={{ width: dynamicVar }} or CSS variable references). Never use inline styles for colors or static values.text-gray-500, bg-red-100, #fff, or rgb(). Always text-[var(--text-*)], bg-[var(--surface-*)], etc.text-[var(--text-muted)] not text-muted-foreground. The CSS variable pattern is canonical.hover-hover: pseudo-class for hover-capable devicestransition-colors for color changes, transition-colors duration-100 for fast hoverrounded-lg (large cards), rounded-md (medium), rounded-sm (small), rounded-xs (tiny)text-small (13px), text-caption (12px), text-xs (11px), text-micro (10px)font-medium for emphasis, avoid font-bold unless for headingsgap-2, gap-3, px-4 py-2.5<button> instead of Button component (exception: inside Radix primitives)<input> instead of Input component (exception: hidden file inputs, read-only checkboxes in markdown)text-gray-*, bg-red-*, text-blue-*)bg-[#fff], text-[#333])text-muted-foreground) instead of CSS variables (text-[var(--text-muted)])Modaltoastcn()z-50, z-[9999]) instead of z-index tokensh-[14px] w-[14px])