Strongtie Design System
Getting StartedComponents

Command Palette

Search for a command to run...

Getting Started
  • Introduction
  • Setup Guide
  • Package Installation
  • Code Quality Setup
  • Migration Guide
  • Resources
Registry
  • Getting Started
  • Combobox
  • Datepicker
  • MultiSelect
  • Tab Nav
  • Tree
Guides
  • Framework Recommendations
Foundations
  • States
  • Variables
Components
  • Accordion
  • Alert
  • Alert Dialog
  • Avatar
  • Badge
  • Breadcrumb
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Collapsible
  • Command
  • Combobox
  • Context Menu
  • Date Picker
  • Dialog
  • Drawer
  • Dropdown Menu
  • Empty
  • Field
  • Hover Card
  • Input
  • Input Group
  • Item
  • Kbd
  • Label
  • Menubar
  • Multi Select
  • Navigation Menu
  • Pagination
  • Popover
  • Progress
  • Radio Group
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Skeleton
  • Slider
  • Switch
  • Table
  • Tabs
  • Tab Nav
  • Textarea
  • Toaster
  • Toggle
  • Toggle Group
  • Tooltip
  • Tree
2026 Simpson Strong-Tie
  1. Docs
  2. Design Standards

Design Standards

PreviousNext

Design consistency standards for React applications. These standards apply to all applications and should be adopted incrementally.

Design consistency standards for React applications. These standards apply to all applications and should be adopted incrementally.

See Also: Code Examples - Comprehensive code examples for all patterns described in this document.

ESLint Setup: For linting configuration, see the @strongtie/eslint Setup Guide.

Core Stack

LayerTechnologyVersionNotes
BuildVite7+Fast dev/build
FrameworkReact19+With React Compiler
RoutingReact Router7+Standard routing
UIshadcn/ui + Tailwind CSS4+Design system
Data FetchingTanStack Query5+Server state
State (Simple)React Context-Session/UI state
State (Complex)Zustand4+Cross-component/computed
Forms (Simple)Native useState-1-3 fields, no validation
Forms (Complex)React Hook Form + Zod7+ / 3+4+ fields or validation
TestingVitest-Jest-compatible API
Linting@strongtie/eslint1.0+Ultracite + custom rules
FormattingPrettier-Via Ultracite
CSS LintingStylelint-Via Ultracite

React Compiler

React Compiler eliminates the need for manual memoization:

  • Do not use: useMemo(), useCallback(), React.memo()
  • Do use: Standard hooks, React.lazy() for code splitting

File Organization

 
/src/
  main.tsx              # App entry, providers
  router.tsx            # Route configuration
  auth-config.ts        # Auth configuration
  globals.css           # Global Tailwind styles
  /pages/               # Route handlers (thin wrappers)
  /layouts/             # Layout components
 
/components/            # Feature & shared components
  /ui/                  # shadcn/ui primitives (DO NOT EDIT)
 
/hooks/                 # Custom React hooks
  query-keys.ts         # Centralized query key factory
 
/lib/                   # Utilities, clients
  /api/                 # API service modules
  error-messages.ts     # Error code → user message mapping
 
/schemas/               # Zod validation schemas
 
/stores/                # Zustand stores
 
/data/                  # Types, constants
 

Naming Conventions

TypeConventionExample
Fileskebab-case.tsxproject-card.tsx
ComponentsPascalCase (named export)export function ProjectCard()
Hooksuse- prefixuse-projects.ts → useProjects()
TypesPascalCasetype ProjectStatus
Schemas-schema.ts suffixproject-schema.ts
Stores-store.ts suffixproject-store.ts
API Modules-api.ts suffixprojects-api.ts
ConstantsSCREAMING_SNAKE_CASEAUTH_DISABLED

Import Rules

  • Always use @/ path aliases
  • Never use relative imports (../../)
  • Group: React → Third-party → Local
  • Use import type {} for type-only imports

State Management

Decision Matrix

ScenarioSolution
Server data (API responses)TanStack Query
Component-local UI (modal, input)useState
Session-level shared (auth, tenant, theme)React Context
Complex computed stateZustand
Cross-component state (unrelated)Zustand
Persisted stateZustand with persist

When to Use Zustand

Create a Zustand store when:

  • State has derived/computed values
  • Multiple unrelated components need the same state
  • You need middleware (persistence, devtools, immer)
  • State updates are complex (multi-step transactions)

Anti-Patterns

Avoid these patterns: - Storing API response in useState - Duplicating TanStack Query data in local state - Using useMemo/useCallback (React Compiler handles this) - Creating Context for single-component state


Data Fetching

All server state must use TanStack Query.

Query Key Factory

Centralize all query keys in hooks/query-keys.ts:

export const queryKeys = {
  projects: {
    all: ["projects"] as const,
    lists: () => [...queryKeys.projects.all, "list"] as const,
    list: (filters: ProjectFilters) =>
      [...queryKeys.projects.lists(), filters] as const,
    details: () => [...queryKeys.projects.all, "detail"] as const,
    detail: (id: string) => [...queryKeys.projects.details(), id] as const,
  },
}

Custom Query Hooks

Never use useQuery directly in components. Create custom hooks:

export function useProjects() {
  const { selectedTenant } = useTenantContext()
  const isAuthReady = useAuthReady()
 
  return useQuery({
    queryKey: queryKeys.projects.list({ tenantId: selectedTenant?.id }),
    queryFn: projectsApi.getAll,
    enabled: isAuthReady,
  })
}

Optimistic Updates

Implement for all user-initiated mutations:

export function useCreateProject() {
  const queryClient = useQueryClient()
 
  return useMutation({
    mutationFn: projectsApi.create,
    onMutate: async (newProject) => {
      // Cancel in-flight queries
      await queryClient.cancelQueries({ queryKey: queryKeys.projects.lists() })
 
      // Snapshot previous value
      const previous = queryClient.getQueryData(queryKeys.projects.list({}))
 
      // Optimistically add to cache
      queryClient.setQueryData(queryKeys.projects.list({}), (old) => [
        ...(old ?? []),
        { ...newProject, id: `temp-${Date.now()}` },
      ])
 
      return { previous }
    },
    onError: (_err, _vars, context) => {
      // Rollback on error
      queryClient.setQueryData(queryKeys.projects.list({}), context?.previous)
    },
    onSettled: () => {
      // Refetch to ensure consistency
      queryClient.invalidateQueries({ queryKey: queryKeys.projects.lists() })
    },
  })
}

Query Client Defaults

const queryClient = new QueryClient({
  defaultOptions: {
    queries: {
      staleTime: 5 * 60 * 1000, // 5 minutes
      gcTime: 10 * 60 * 1000, // 10 minutes
      retry: 1,
      refetchOnWindowFocus: false,
    },
  },
})

Forms

Complexity Threshold

ConditionSolution
1-3 fields, no validationuseState
4+ fields OR validation rulesReact Hook Form + Zod

Zod Schema Pattern

Define schemas in /schemas/:

// schemas/project-schema.ts
import { z } from "zod"
 
export const createProjectSchema = z.object({
  name: z.string().min(1, "Name is required").max(100, "Name too long"),
  type: z.enum(["internal", "external", "research"], {
    required_error: "Please select a project type",
  }),
  description: z.string().max(500).optional(),
})
 
export type CreateProjectInput = z.infer<typeof createProjectSchema>

React Hook Form Integration

import {
  createProjectSchema,
  type CreateProjectInput,
} from "@/schemas/project-schema"
import { zodResolver } from "@hookform/resolvers/zod"
import { useForm } from "react-hook-form"
 
function CreateProjectForm() {
  const form = useForm<CreateProjectInput>({
    resolver: zodResolver(createProjectSchema),
    defaultValues: {
      name: "",
      type: undefined,
      description: "",
    },
  })
 
  return (
    <Form {...form}>
      <form onSubmit={form.handleSubmit(onSubmit)}>
        <FormField
          control={form.control}
          name="name"
          render={({ field }) => (
            <FormItem>
              <FormLabel>Project Name</FormLabel>
              <FormControl>
                <Input {...field} />
              </FormControl>
              <FormMessage />
            </FormItem>
          )}
        />
      </form>
    </Form>
  )
}

Error Handling

Architecture

LayerResponsibility
API ResponseReturn error code + technical message
API ClientThrow ApiError with code and status
Error BoundaryCatch unhandled React errors
Query Error StateHandle expected API failures
UI ComponentMap error codes to user messages

Error Code Mapping

APIs return error codes. UI maps to user-friendly messages:

// lib/error-messages.ts
const ERROR_MESSAGES: Record<string, string> = {
  TENANT_REQUIRED: "Please select a tenant first.",
  NOT_FOUND: "The requested item could not be found.",
  UNAUTHORIZED: "Your session has expired. Please sign in again.",
  FORBIDDEN: "You do not have permission to perform this action.",
  VALIDATION_ERROR: "Please check your input and try again.",
  NETWORK_ERROR: "Unable to connect. Please check your connection.",
  DEFAULT: "Something went wrong. Please try again.",
}
 
export function getUserMessage(error: ApiError): string {
  return ERROR_MESSAGES[error.code] ?? ERROR_MESSAGES.DEFAULT
}

HTTP Status Error Messages

StatusTitleMessageAction
503Service UnavailableWe are currently having technical difficulties. Please try again soon.Refresh
500Server ErrorSomething went wrong on our end. Try again or contact support.Refresh
400Bad RequestWe couldn't process your request. Check your input and try again.Refresh
404Not FoundWe couldn't find the content you're looking for.None
401UnauthorizedYour session has expired. Please log in again.Log in
403Permission RequiredPlease contact your administratorGo Back
Connection ErrorConnection ErrorUnable to connect. Check your network connection and try again.Refresh

Error UI Components

Error Boundary: For React component crashes (unrecoverable)

Inline Error: For API failures within a component

function ErrorMessage({ title, message, onRetry }: ErrorMessageProps) {
  return (
    <div className="flex flex-col items-center py-8">
      <AlertCircle className="text-destructive h-12 w-12" />
      <h3 className="mt-4 font-semibold">{title}</h3>
      <p className="text-muted-foreground mt-2 text-sm">{message}</p>
      {onRetry && (
        <Button variant="outline" onClick={onRetry} className="mt-4">
          Try Again
        </Button>
      )}
    </div>
  )
}

Toast Usage

TypeUse For
SuccessConfirming completed actions
ErrorAPI failures, permission errors
WarningDestructive action confirmations

Loading States

Skeleton vs Spinner

Use Skeleton when:

  • Content layout is known/predictable
  • Preventing layout shift matters
  • Data typically loads quickly (< 3s)

Use Spinner when:

  • Layout is unpredictable
  • Action-triggered loading (button clicks)
  • Overlay/blocking operations

Skeleton Pattern

Mirror the actual content structure:

function ProjectCardSkeleton() {
  return (
    <Card>
      <CardHeader>
        <Skeleton className="h-6 w-3/4" />
        <Skeleton className="h-4 w-1/2" />
      </CardHeader>
      <CardContent>
        <Skeleton className="h-20 w-full" />
      </CardContent>
    </Card>
  )
}
 
function ProjectsLoadingSkeleton() {
  return (
    <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
      {Array.from({ length: 6 }).map((_, i) => (
        <ProjectCardSkeleton key={i} />
      ))}
    </div>
  )
}

Empty State Pattern

Always provide a call-to-action:

function EmptyState({
  icon: Icon,
  title,
  description,
  action,
}: EmptyStateProps) {
  return (
    <div className="flex flex-col items-center justify-center py-12">
      <Icon className="text-muted-foreground h-12 w-12" />
      <h3 className="mt-4 text-lg font-semibold">{title}</h3>
      <p className="text-muted-foreground mt-2 text-center text-sm">
        {description}
      </p>
      {action && <div className="mt-4">{action}</div>}
    </div>
  )
}

Component State Handling

Every async component must handle all states:

function ProjectsList() {
  const { data, isLoading, error, refetch } = useProjects()
 
  if (isLoading) {
    return <ProjectsLoadingSkeleton />
  }
 
  if (error) {
    return (
      <ErrorMessage
        title="Failed to load projects"
        message={getUserMessage(error)}
        onRetry={refetch}
      />
    )
  }
 
  if (!data?.length) {
    return (
      <EmptyState
        icon={FolderOpen}
        title="No projects yet"
        description="Create your first project to get started."
        action={<Button>Create Project</Button>}
      />
    )
  }
 
  return <ProjectGrid projects={data} />
}

Accessibility

WCAG 2.1 AA Compliance

All applications must meet WCAG 2.1 Level AA.

Color Contrast

  • Normal text: 4.5:1 minimum
  • Large text (18px+ or 14px+ bold): 3:1 minimum
  • UI components: 3:1 minimum

Keyboard Navigation

  • All interactive elements must be focusable
  • Focus order must be logical
  • Focus indicators must be visible
  • No keyboard traps

Icon-Only Buttons

Icon-only buttons MUST have accessible names:

// Option 1: aria-label (preferred)
<Button variant="ghost" size="icon" aria-label="Delete project">
  <Trash2 className="h-4 w-4" />
</Button>
 
// Option 2: sr-only span
<Button variant="ghost" size="icon">
  <Trash2 className="h-4 w-4" />
  <span className="sr-only">Delete project</span>
</Button>

Label Guidelines:

  • Describe the action, not the icon ("Delete project" not "Trash icon")
  • Be specific to context ("Delete 'Project Alpha'" when possible)
  • Tooltips are supplementary, not a replacement for aria-label

Form Accessibility

  • All inputs must have associated <label> elements
  • Required fields must be indicated (visually and programmatically)
  • Error messages must be associated with fields (aria-describedby)
  • Use aria-invalid="true" for fields with errors

Images

  • Decorative images: alt=""
  • Informative images: Meaningful alt text
  • Complex images: Extended description via aria-describedby

Testing

  • Lighthouse accessibility audit (target: 100)
  • Keyboard-only navigation test
  • Screen reader testing (VoiceOver, NVDA)
  • axe-core automated testing

Performance

Bundle Size Budgets

MetricLimit
Initial JS (gzipped)< 200KB
Initial CSS (gzipped)< 50KB
Lazy-loaded chunks< 100KB each

Loading Performance

MetricTarget
First Contentful Paint< 1.5s
Largest Contentful Paint< 2.5s
Time to Interactive< 3.5s
Cumulative Layout Shift< 0.1

Bundle Chunking Strategy

Production builds use manual chunk splitting (see vite.config.ts):

ChunkContentsStrategy
vendor-reactReact, React DOM, React Router, SchedulerAlways loaded
vendor-chartsRecharts, D3, VictoryLazy-load
vendor-editorPlate.js, Slate, Emoji MartLazy-load
vendor-formsReact Hook Form, ZodLoad on form pages
vendor-queryTanStack QueryAlways loaded
vendor-radixRadix UI primitivesAlways loaded
vendor-utilsdate-fns, clsx, tailwind-merge, CVA, LucideAlways loaded
vendor-authoidc-client-ts, react-oidc-contextAlways loaded

Code Splitting

  • Routes: Automatic with React Router
  • Heavy dependencies: React.lazy() (charts, editors, maps)
  • Large components: Dynamic imports
const RichTextEditor = React.lazy(() => import("@/components/rich-text-editor"))
 
function CommentForm() {
  return (
    <Suspense fallback={<Skeleton className="h-32" />}>
      <RichTextEditor />
    </Suspense>
  )
}

Prohibited Patterns

Avoid these patterns: - import * as for large modules - Barrel files (index.ts re-exports) in large directories - Unoptimized images (use WebP, proper sizing) - Inline SVGs > 4KB (use sprite or component)

Image Optimization

  • Use WebP format with fallbacks
  • Provide responsive sizes (srcset)
  • Lazy load below-fold images
  • Specify dimensions to prevent layout shift

AI Agent Skills

For AI-assisted development, we provide custom skills that encode these design standards:

  • strongtie-component - Component creation patterns
  • strongtie-test - Test generation patterns
  • strongtie-docs - Documentation patterns
  • strongtie-react-practices - React best practices
  • strongtie-style-consistency - Linting and formatting

See the Skills documentation for installation and usage.


Checklists

Component Checklist

  • Named export (not default)
  • Props use readonly modifier
  • Sub-components at module level
  • Loading state (Skeleton)
  • Error state (with retry)
  • Empty state (with CTA)
  • Icon buttons have aria-label
  • Form inputs have labels
  • Uses shadcn/ui components
  • Uses cn() for classes
  • Uses @/ import aliases

Query Hook Checklist

  • Query key in query-keys.ts
  • Custom hook in /hooks/
  • enabled flag for conditional fetching
  • Error handling in consumer

Mutation Hook Checklist

  • Optimistic update in onMutate
  • Rollback in onError
  • Invalidation in onSettled
  • Success toast notification
  • Error toast with user message

FAQ

Can I use custom CSS in a Tailwind application?

Yes, on a case-by-case basis. While Tailwind is designed to handle the vast majority of styling needs, the official Tailwind documentation explicitly states that custom CSS is acceptable when needed.

When custom CSS may be appropriate:

  • When Tailwind doesn't provide native utilities for a specific CSS property
  • For complex animations or keyframes not achievable with Tailwind's animation utilities
  • For print styles (@media print) with specific requirements
  • For targeting specific pseudo-elements Tailwind doesn't cover

Requirements:

  1. Use Tailwind tokens — Reference design tokens via CSS variables (var(--spacing), var(--radius), var(--primary), etc.) to maintain theme compatibility and future theme support
  2. Document the reason — Add a code comment explaining why custom CSS was necessary
  3. Evaluate alternatives first — Confirm Tailwind utilities or a plugin doesn't already solve the need

Example:

/* Print-specific layout - Tailwind's print utilities don't cover page breaks */
@media print {
  .page-break {
    break-before: page;
    margin-top: var(--spacing-8);
  }
}

Important: This is not blanket permission to write custom CSS freely. Each case should be evaluated individually to ensure Tailwind utilities or plugins cannot accomplish the same result. The goal is to maintain token adherence and avoid incurring tech debt that complicates future theme support.

Design Standards - Code ExamplesFramework Recommendations

On This Page

Core StackReact CompilerFile OrganizationNaming ConventionsImport RulesState ManagementDecision MatrixWhen to Use ZustandAnti-PatternsData FetchingQuery Key FactoryCustom Query HooksOptimistic UpdatesQuery Client DefaultsFormsComplexity ThresholdZod Schema PatternReact Hook Form IntegrationError HandlingArchitectureError Code MappingHTTP Status Error MessagesError UI ComponentsToast UsageLoading StatesSkeleton vs SpinnerSkeleton PatternEmpty State PatternComponent State HandlingAccessibilityWCAG 2.1 AA ComplianceColor ContrastKeyboard NavigationIcon-Only ButtonsForm AccessibilityImagesTestingPerformanceBundle Size BudgetsLoading PerformanceBundle Chunking StrategyCode SplittingProhibited PatternsImage OptimizationAI Agent SkillsChecklistsComponent ChecklistQuery Hook ChecklistMutation Hook ChecklistFAQCan I use custom CSS in a Tailwind application?

Contribute

  • Report an issue
  • Request a feature
  • Edit this page