AgentSkillsCN

Ui Standards

UI 标准

SKILL.md

UI Standards Skill

Purpose: This skill defines UI patterns, accessibility standards, and component conventions for the web application. Reference this when building, reviewing, or modifying UI components.


Quick Rules

Always Do

  • Use semantic color tokens (text-foreground, bg-primary) — never hardcoded colors
  • Include sr-only or aria-label on icon-only buttons
  • Use Pencil for edit actions, Trash2 for delete actions
  • Ensure 44×44px minimum touch targets on interactive elements
  • Add aria-describedby linking inputs to their error messages
  • Use Loader2 with animate-spin for loading states
  • Respect prefers-reduced-motion with motion-reduce: classes

Never Do

  • Import Edit, Edit2, Edit3, Trash, or TrashIcon from lucide-react
  • Use color alone to convey meaning (errors, required fields, status)
  • Skip heading levels (h1 → h3 without h2)
  • Create interactive elements smaller than 24×24px
  • Use tabIndex values other than 0, -1
  • Hardcode colors like text-gray-500 or bg-[#4B7F52]

Responsive Breakpoints

BreakpointMin WidthUsage
(default)0pxMobile-first base styles
sm:640pxLarge phones
md:768pxTablets
lg:1024pxLaptops/desktops
xl:1280pxLarge desktops
2xl:1536pxExtra large screens

Layout Patterns

tsx
// Page container
<div className="container mx-auto px-4 sm:px-6 lg:px-8">
  {children}
</div>

// Responsive grid
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-3">
  {items}
</div>

// Sidebar layout
<div className="flex min-h-screen">
  <aside className="hidden lg:block w-64 shrink-0 border-r">{sidebar}</aside>
  <main className="flex-1 min-w-0">{content}</main>
</div>

// Stack on mobile, row on desktop
<div className="flex flex-col gap-4 sm:flex-row">
  {children}
</div>

Component Responsiveness

ComponentMobileDesktop
NavigationBottom nav or hamburgerSidebar
TablesCard view or horizontal scrollFull table
ModalsFull-screen sheetCentered dialog
Action buttonsFull-width stackedInline
Form layoutsSingle columnTwo columns

Icons

All icons from lucide-react. Standard size: h-4 w-4.

Icon Reference

ActionIconImport
EditPencilPencil
DeleteTrash2Trash2
AddPlusPlus
ViewEyeEye
CloseXX
SettingsSettingsSettings
SearchSearchSearch
LoadingLoader2Loader2
SuccessCheckCircle2CheckCircle2
WarningAlertTriangleAlertTriangle
ErrorAlertCircleAlertCircle
MenuMoreHorizontalMoreHorizontal
DownloadDownloadDownload
UploadUploadUpload
CopyCopyCopy
RefreshRefreshCwRefreshCw
BackArrowLeftArrowLeft
ForwardArrowRightArrowRight
ExpandChevronDownChevronDown
CollapseChevronUpChevronUp
External linkExternalLinkExternalLink

Icon Sizes

ContextSizeExample
Buttons, inline texth-4 w-416px
Card headers, alertsh-5 w-520px
Empty statesh-8 w-832px
Hero/featureh-12 w-1248px

Prohibited Icons

tsx
// ❌ WRONG - These are prohibited
import { Edit, Edit2, Edit3, Trash, TrashIcon } from "lucide-react";

// ✅ CORRECT - Use these instead
import { Pencil, Trash2 } from "lucide-react";

Buttons

Variants

VariantUsageExample
defaultPrimary actionsSave, Submit, Create
secondarySecondary actionsSave Draft
outlineTertiary actionsCancel
ghostSubtle/icon buttonsIcon actions
destructiveDangerous actionsDelete
linkInline text linksLearn more

Button Patterns

tsx
// Primary action
<Button>Save Changes</Button>

// With icon
<Button>
  <Plus className="h-4 w-4" />
  Add Item
</Button>

// Icon-only (MUST have sr-only or aria-label)
<Button variant="ghost" size="icon" title="Edit">
  <Pencil className="h-4 w-4" />
  <span className="sr-only">Edit</span>
</Button>

// Loading state
<Button disabled>
  <Loader2 className="h-4 w-4 animate-spin" />
  Saving...
</Button>

// Destructive
<Button variant="destructive">
  <Trash2 className="h-4 w-4" />
  Delete
</Button>

// Responsive full-width
<Button className="w-full sm:w-auto">Submit</Button>

Forms

Field Structure

tsx
<div className="space-y-2">
  <Label htmlFor="email">
    Email{" "}
    <span className="text-destructive" aria-hidden="true">
      *
    </span>
    <span className="sr-only">(required)</span>
  </Label>
  <Input
    id="email"
    type="email"
    placeholder="you@example.com"
    aria-describedby="email-help email-error"
    aria-invalid={!!error}
    className={error ? "border-destructive" : ""}
  />
  <p id="email-help" className="text-sm text-muted-foreground">
    We'll never share your email.
  </p>
  {error && (
    <p
      id="email-error"
      className="text-sm text-destructive flex items-center gap-1"
    >
      <AlertCircle className="h-4 w-4 shrink-0" />
      {error}
    </p>
  )}
</div>

Form Layout

tsx
// Single column (default)
<form className="space-y-6 max-w-md">
  <div className="space-y-2">
    <Label>Field</Label>
    <Input />
  </div>
  <Button type="submit">Submit</Button>
</form>

// Two-column responsive
<div className="grid gap-4 sm:grid-cols-2">
  <div className="space-y-2">
    <Label>First Name</Label>
    <Input />
  </div>
  <div className="space-y-2">
    <Label>Last Name</Label>
    <Input />
  </div>
</div>

// Fieldset for grouped fields
<fieldset className="space-y-4 border rounded-lg p-4">
  <legend className="px-2 font-medium">Contact Information</legend>
  {/* fields */}
</fieldset>

Input States

StateClasses
Default(none)
FocusAutomatic via shadcn
Errorborder-destructive aria-invalid="true"
Disableddisabled opacity-50

Accessibility (WCAG 2.1 AA)

Color Contrast

ElementMinimum Ratio
Normal text (<18px)4.5:1
Large text (≥18px or ≥14px bold)3:1
UI components, icons3:1
Focus indicators3:1

Keyboard Navigation

KeyExpected Behavior
TabMove to next focusable element
Shift+TabMove to previous focusable element
EnterActivate buttons, submit forms
SpaceActivate buttons, toggle checkboxes
EscapeClose modals, dropdowns
Arrow keysNavigate within components

Skip Link (Required)

tsx
// Place as first element in layout
<a
  href="#main-content"
  className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4 focus:z-100 focus:px-4 focus:py-2 focus:bg-background focus:text-foreground focus:ring-2 focus:ring-ring focus:rounded-md"
>
  Skip to main content
</a>

// Main content target
<main id="main-content" tabIndex={-1} className="outline-none">
  {content}
</main>

ARIA Patterns

tsx
// Live region for status updates
<div aria-live="polite" aria-atomic="true">
  {statusMessage}
</div>

// Navigation landmarks
<nav aria-label="Main navigation">{/* nav items */}</nav>
<nav aria-label="Breadcrumb">{/* breadcrumbs */}</nav>

// Modal/Dialog
<Dialog>
  <DialogContent aria-labelledby="dialog-title" aria-describedby="dialog-desc">
    <DialogTitle id="dialog-title">Title</DialogTitle>
    <DialogDescription id="dialog-desc">Description</DialogDescription>
  </DialogContent>
</Dialog>

// Decorative icons (hidden from screen readers)
<Icon aria-hidden="true" className="h-4 w-4" />

Touch Targets

StandardMinimum Size
WCAG 2.1 AA24×24px
WCAG 2.2 AAA / Mobile best practice44×44px
tsx
// Ensure adequate touch target
<Button size="icon" className="h-10 w-10">
  <Pencil className="h-4 w-4" />
</Button>

Reduced Motion

tsx
// Use motion-reduce prefix
<div className="transition-transform motion-reduce:transition-none">
  {content}
</div>;

// Or check programmatically
const prefersReducedMotion = window.matchMedia(
  "(prefers-reduced-motion: reduce)"
).matches;

Feedback & States

Loading States

tsx
// Button loading
<Button disabled>
  <Loader2 className="h-4 w-4 animate-spin" />
  Saving...
</Button>

// Skeleton loading
<div className="space-y-4">
  <Skeleton className="h-8 w-1/3" />
  <Skeleton className="h-4 w-full" />
  <Skeleton className="h-4 w-2/3" />
</div>

// Full page loading
<div className="flex items-center justify-center min-h-[400px]">
  <Loader2 className="h-8 w-8 animate-spin text-muted-foreground" />
</div>

Empty States

tsx
<div className="flex flex-col items-center justify-center py-12 px-4 text-center">
  <div className="rounded-full bg-muted p-4 mb-4">
    <FileText className="h-8 w-8 text-muted-foreground" />
  </div>
  <h3 className="text-lg font-medium mb-2">No documents yet</h3>
  <p className="text-muted-foreground mb-6 max-w-sm">
    Get started by creating your first document.
  </p>
  <Button>
    <Plus className="h-4 w-4" />
    Create Document
  </Button>
</div>

Error States

tsx
// Inline field error
<p className="text-sm text-destructive flex items-center gap-1">
  <AlertCircle className="h-4 w-4 shrink-0" />
  Error message here
</p>

// Error with recovery
<div className="flex flex-col items-center justify-center py-12 text-center">
  <div className="rounded-full bg-destructive/10 p-4 mb-4">
    <AlertCircle className="h-8 w-8 text-destructive" />
  </div>
  <h3 className="text-lg font-medium mb-2">Something went wrong</h3>
  <p className="text-muted-foreground mb-6">Please try again.</p>
  <Button onClick={handleRetry}>
    <RefreshCw className="h-4 w-4" />
    Try Again
  </Button>
</div>

Alerts

TypeClassesIcon
Infobg-blue-50 text-blue-700Info
Successbg-green-50 text-green-700CheckCircle2
Warningbg-yellow-50 text-yellow-700AlertTriangle
Errorbg-destructive/10 text-destructiveAlertCircle
tsx
<Alert variant="destructive">
  <AlertCircle className="h-5 w-5" />
  <AlertTitle>Error</AlertTitle>
  <AlertDescription>Something went wrong.</AlertDescription>
</Alert>

Toasts

tsx
// Success
toast({ title: "Saved", description: "Your changes have been saved." });

// Error
toast({
  variant: "destructive",
  title: "Error",
  description: "Failed to save.",
});

// With action
toast({
  title: "Deleted",
  action: (
    <ToastAction altText="Undo" onClick={handleUndo}>
      Undo
    </ToastAction>
  ),
});

Confirmation Dialog

tsx
<AlertDialog>
  <AlertDialogTrigger asChild>
    <Button variant="destructive">Delete</Button>
  </AlertDialogTrigger>
  <AlertDialogContent>
    <AlertDialogHeader>
      <AlertDialogTitle>Delete this item?</AlertDialogTitle>
      <AlertDialogDescription>
        This action cannot be undone.
      </AlertDialogDescription>
    </AlertDialogHeader>
    <AlertDialogFooter>
      <AlertDialogCancel>Cancel</AlertDialogCancel>
      <AlertDialogAction
        onClick={handleDelete}
        className="bg-destructive text-destructive-foreground hover:bg-destructive/90"
      >
        Delete
      </AlertDialogAction>
    </AlertDialogFooter>
  </AlertDialogContent>
</AlertDialog>

Navigation

Breadcrumbs

tsx
<nav aria-label="Breadcrumb" className="mb-4">
  <ol className="flex items-center gap-2 text-sm text-muted-foreground">
    <li>
      <a href="/" className="hover:text-foreground">
        Home
      </a>
    </li>
    <li aria-hidden="true">
      <ChevronRight className="h-4 w-4" />
    </li>
    <li>
      <a href="/docs" className="hover:text-foreground">
        Documents
      </a>
    </li>
    <li aria-hidden="true">
      <ChevronRight className="h-4 w-4" />
    </li>
    <li>
      <span aria-current="page" className="text-foreground font-medium">
        Current
      </span>
    </li>
  </ol>
</nav>

Mobile Navigation

tsx
// Bottom nav (mobile)
<nav aria-label="Main" className="fixed bottom-0 inset-x-0 lg:hidden border-t bg-background z-50">
  <ul className="flex justify-around py-2">
    <li><NavLink href="/" icon={Home} label="Home" /></li>
    <li><NavLink href="/docs" icon={FileText} label="Docs" /></li>
    <li><NavLink href="/settings" icon={Settings} label="Settings" /></li>
  </ul>
</nav>

// Hamburger menu
<Sheet>
  <SheetTrigger asChild>
    <Button variant="ghost" size="icon" className="lg:hidden">
      <Menu className="h-5 w-5" />
      <span className="sr-only">Open menu</span>
    </Button>
  </SheetTrigger>
  <SheetContent side="left">{/* nav links */}</SheetContent>
</Sheet>

Pagination

tsx
<nav aria-label="Pagination" className="flex items-center justify-between">
  <p className="text-sm text-muted-foreground">Showing 1-10 of 97</p>
  <div className="flex gap-2">
    <Button variant="outline" size="sm" disabled={page === 1}>
      <ArrowLeft className="h-4 w-4" />
      <span className="sr-only sm:not-sr-only sm:ml-2">Previous</span>
    </Button>
    <Button variant="outline" size="sm" disabled={page === totalPages}>
      <span className="sr-only sm:not-sr-only sm:mr-2">Next</span>
      <ArrowRight className="h-4 w-4" />
    </Button>
  </div>
</nav>

Data Display

Responsive Table

tsx
// Card view on mobile, table on desktop
<div className="sm:hidden space-y-4">
  {items.map((item) => (
    <Card key={item.id}>
      <CardHeader className="flex flex-row items-center justify-between pb-2">
        <CardTitle className="text-base">{item.name}</CardTitle>
        <Badge>{item.status}</Badge>
      </CardHeader>
      <CardFooter className="flex gap-2">
        <Button variant="outline" size="sm" className="flex-1">View</Button>
        <Button variant="outline" size="sm" className="flex-1">Edit</Button>
      </CardFooter>
    </Card>
  ))}
</div>

<div className="hidden sm:block border rounded-lg overflow-hidden">
  <Table>
    <TableHeader>
      <TableRow>
        <TableHead>Name</TableHead>
        <TableHead>Status</TableHead>
        <TableHead className="text-right">Actions</TableHead>
      </TableRow>
    </TableHeader>
    <TableBody>
      {items.map((item) => (
        <TableRow key={item.id}>
          <TableCell className="font-medium">{item.name}</TableCell>
          <TableCell><Badge>{item.status}</Badge></TableCell>
          <TableCell className="text-right">
            <DropdownMenu>
              <DropdownMenuTrigger asChild>
                <Button variant="ghost" size="icon">
                  <MoreHorizontal className="h-4 w-4" />
                  <span className="sr-only">Actions</span>
                </Button>
              </DropdownMenuTrigger>
              <DropdownMenuContent align="end">
                <DropdownMenuItem><Eye className="h-4 w-4" />View</DropdownMenuItem>
                <DropdownMenuItem><Pencil className="h-4 w-4" />Edit</DropdownMenuItem>
                <DropdownMenuSeparator />
                <DropdownMenuItem className="text-destructive">
                  <Trash2 className="h-4 w-4" />Delete
                </DropdownMenuItem>
              </DropdownMenuContent>
            </DropdownMenu>
          </TableCell>
        </TableRow>
      ))}
    </TableBody>
  </Table>
</div>

List with Hover Actions

tsx
<ul className="divide-y rounded-lg border">
  {items.map((item) => (
    <li
      key={item.id}
      className="group flex items-center justify-between p-4 hover:bg-muted/50"
    >
      <div className="flex items-center gap-3">
        <FileText className="h-5 w-5 text-muted-foreground" />
        <span className="font-medium">{item.name}</span>
      </div>
      {/* Always visible on mobile, hover on desktop */}
      <div className="flex gap-1 lg:opacity-0 lg:group-hover:opacity-100 lg:group-focus-within:opacity-100 transition-opacity">
        <Button variant="ghost" size="icon" title="Edit">
          <Pencil className="h-4 w-4" />
          <span className="sr-only">Edit</span>
        </Button>
        <Button
          variant="ghost"
          size="icon"
          title="Delete"
          className="text-destructive"
        >
          <Trash2 className="h-4 w-4" />
          <span className="sr-only">Delete</span>
        </Button>
      </div>
    </li>
  ))}
</ul>

Spacing

Base unit: 4px. Use Tailwind spacing scale.

ClassValueUsage
gap-1, p-14pxTight spacing, icon gaps
gap-2, p-28pxRelated elements
gap-3, p-312pxButton/input padding
gap-4, p-416pxStandard spacing, card padding
gap-6, p-624pxSection gaps
gap-8, py-832pxMajor sections
gap-12, py-1248pxPage sections
tsx
// Consistent form spacing
<form className="space-y-6">
  <div className="space-y-2">{/* field */}</div>
  <div className="space-y-2">{/* field */}</div>
</form>

// Button groups
<div className="flex gap-2">
  <Button variant="outline">Cancel</Button>
  <Button>Save</Button>
</div>

// Card internal spacing
<Card className="p-4 sm:p-6">
  <CardHeader className="pb-4">{/* header */}</CardHeader>
  <CardContent className="space-y-4">{/* content */}</CardContent>
</Card>

Typography

ClassSizeUsage
text-xs12pxCaptions, metadata
text-sm14pxSecondary text, help text
text-base16pxBody text (default)
text-lg18pxLead paragraphs
text-xl20pxCard titles
text-2xl24pxSection titles
text-3xl30pxPage titles
tsx
// Heading hierarchy (never skip levels)
<h1 className="text-3xl font-bold">Page Title</h1>
<h2 className="text-2xl font-semibold">Section</h2>
<h3 className="text-xl font-medium">Subsection</h3>

// Responsive headings
<h1 className="text-2xl sm:text-3xl lg:text-4xl font-bold">Title</h1>

// Readable line length
<p className="max-w-prose">Long form content...</p>

Colors

Use Semantic Tokens Only

tsx
// ✅ CORRECT
<div className="bg-background text-foreground">
  <p className="text-muted-foreground">Secondary text</p>
  <Button className="bg-primary text-primary-foreground">Action</Button>
  <span className="text-destructive">Error</span>
</div>

// ❌ WRONG - Hardcoded colors
<div className="bg-white text-black">
  <p className="text-gray-500">Secondary text</p>
  <button className="bg-blue-500">Action</button>
</div>

Available Tokens

TokenUsage
background / foregroundPage background, primary text
muted / muted-foregroundSubtle backgrounds, secondary text
card / card-foregroundCard surfaces
primary / primary-foregroundPrimary actions
secondary / secondary-foregroundSecondary actions
destructive / destructive-foregroundErrors, delete actions
borderDefault borders
inputForm input borders
ringFocus rings

Exception: Relationship Colors

tsx
// Allowed for visual distinction
const relationshipColors = {
  spouse: "bg-rose-100 text-rose-700 border-rose-200",
  child: "bg-sky-100 text-sky-700 border-sky-200",
  parent: "bg-amber-100 text-amber-700 border-amber-200",
  sibling: "bg-emerald-100 text-emerald-700 border-emerald-200",
};

Animation

Durations

DurationClassUsage
75msduration-75Micro-interactions
150msduration-150Buttons, toggles
200msduration-200Standard (default)
300msduration-300Modals, drawers

Patterns

tsx
// Hover transition
<button className="transition-colors hover:bg-muted">Hover</button>

// Loading spinner
<Loader2 className="h-4 w-4 animate-spin" />

// Skeleton pulse
<div className="animate-pulse bg-muted rounded h-4" />

// Respect reduced motion
<div className="transition-transform motion-reduce:transition-none">
  {content}
</div>

Checklist

Before Submitting UI Code

  • All interactive elements keyboard accessible
  • Icon-only buttons have sr-only labels
  • Form inputs have associated labels
  • Error states use aria-invalid and aria-describedby
  • Color contrast meets 4.5:1 (text) / 3:1 (UI)
  • Touch targets are at least 44×44px on mobile
  • No hardcoded colors (use semantic tokens)
  • Using Pencil for edit, Trash2 for delete
  • Loading states use Loader2 with animate-spin
  • Responsive: works on mobile (320px) through desktop
  • Skip link present in layouts
  • Heading hierarchy is logical (no skipped levels)