Components

Patterns, usage rules, and best practices for UI Lab components

Component Guidelines

Component guidelines define how UI Lab components should be used, when to use them, and best practices for implementation. These guidelines ensure consistency across interfaces and help teams make better design decisions.

Component Philosophy

When to Create Components

Components should be created when:

  1. A pattern repeats across multiple interfaces
  2. The pattern has distinct, well-defined states
  3. The component would reduce code duplication
  4. The pattern is complex enough to warrant isolation

When NOT to Create Components

Don't create components for:

  1. One-off implementations
  2. Simple elements (use HTML elements instead)
  3. Experimental features
  4. Styles-only variations without logic changes

Component Structure

All components follow a consistent structure:

Template

interface ComponentProps {
  // Required props
  required: string

  // Optional props with sensible defaults
  optional?: string
  variant?: 'primary' | 'secondary'
  size?: 'small' | 'medium' | 'large'
  disabled?: boolean
  className?: string
  children?: React.ReactNode
}

export function Component({
  required,
  optional = 'default',
  variant = 'primary',
  size = 'medium',
  disabled = false,
  className = '',
  children,
}: ComponentProps) {
  return (
    <div className={`component component--${variant} component--${size} ${className}`}>
      {children}
    </div>
  )
}

Guidelines

  • Props: Keep prop interfaces simple and intuitive
  • Defaults: Provide sensible defaults for optional props
  • Variants: Use variant prop for style-based variations
  • Sizes: Provide common size options (sm, md, lg)
  • Accessibility: Include ARIA attributes where needed
  • Flexibility: Allow className for emergency customization

Button Component

Purpose

Buttons trigger actions or navigations. They're the primary way users interact with interfaces.

When to Use

  • Actions: Submit forms, create items, delete
  • Navigation: Links to other pages or sections
  • Toggles: Toggle features on/off
  • Menus: Open dropdown menus or popovers

Variants

Primary

<Button variant="primary">
  Primary Action
</Button>
  • Most prominent, use for main action
  • Background: accent-600
  • Hover: accent-700

Secondary

<Button variant="secondary">
  Secondary Action
</Button>
  • Less prominent, use for secondary actions
  • Background: background-100
  • Hover: background-200

Danger

<Button variant="danger">
  Delete
</Button>
  • For destructive actions
  • Background: danger-600
  • Hover: danger-700

Ghost

<Button variant="ghost">
  Dismiss
</Button>
  • Minimal style, use for tertiary actions
  • Background: transparent
  • Hover: background-100

Sizes

  • Small (sm): 32px height, 12px text
  • Medium (md): 40px height, 14px text (default)
  • Large (lg): 48px height, 16px text

States

  • Default: Interactive and clickable
  • Hover: Slightly darker or elevated
  • Active: Clearly pressed/selected state
  • Disabled: Reduced opacity, no interaction
  • Loading: Show spinner, disable interaction

Props

interface ButtonProps {
  variant?: 'primary' | 'secondary' | 'danger' | 'ghost'
  size?: 'sm' | 'md' | 'lg'
  disabled?: boolean
  loading?: boolean
  onClick?: () => void
  children: React.ReactNode
  className?: string
  asChild?: boolean
}

Usage Examples

// Primary action
<Button onClick={handleSubmit}>Submit Form</Button>

// Secondary action
<Button variant="secondary">Cancel</Button>

// Dangerous action with confirmation
<Button
  variant="danger"
  onClick={() => confirmDelete()}
>
  Delete Item
</Button>

// Icon button
<Button size="sm" variant="ghost">
  <X className="w-4 h-4" />
</Button>

// Loading state
<Button loading disabled>
  Saving...
</Button>

Accessibility

  • Focus states: Clear focus ring
  • Keyboard: Fully keyboard accessible
  • Screen readers: aria-label for icon-only buttons
  • Disabled: Proper disabled attribute

Common Mistakes

  • Using buttons for navigation (use links instead)
  • Multiple primary buttons on same screen
  • Confusing buttons and links visually
  • Missing disabled visual feedback

Input Component

Purpose

Inputs collect user text data. They're essential for forms and searches.

Types

Text Input

<Input type="text" placeholder="Enter name" />

Email Input

<Input type="email" placeholder="email@example.com" />

Password Input

<Input type="password" placeholder="Enter password" />

Number Input

<Input type="number" min="0" max="100" />

Search Input

<Input type="search" placeholder="Search..." />

States

  • Default: Ready for input
  • Focus: Clear focus indicator
  • Filled: User has entered data
  • Disabled: Cannot be edited
  • Error: Invalid input, show error message
  • Success: Valid input confirmation

Sizing

  • Small (sm): 32px height
  • Medium (md): 40px height (default)
  • Large (lg): 48px height

Props

interface InputProps {
  type?: 'text' | 'email' | 'password' | 'number' | 'search'
  placeholder?: string
  value?: string
  onChange?: (e: React.ChangeEvent<HTMLInputElement>) => void
  disabled?: boolean
  error?: string
  size?: 'sm' | 'md' | 'lg'
  prefix?: React.ReactNode
  suffix?: React.ReactNode
}

Usage Examples

// Basic input
<Input
  type="text"
  placeholder="Enter your name"
  onChange={(e) => setName(e.target.value)}
/>

// With label
<>
  <label>Email Address</label>
  <Input type="email" placeholder="email@example.com" />
</>

// With error
<Input
  type="email"
  error="Invalid email address"
  value={email}
  onChange={(e) => setEmail(e.target.value)}
/>

// With prefix/suffix
<Input
  type="number"
  prefix="$"
  placeholder="0.00"
/>

Validation

  • Required fields: Show required indicator
  • Real-time validation: Validate as user types
  • Error messages: Clear, actionable messages
  • Success feedback: Confirm valid input

Accessibility

  • Labels: Always use associated labels
  • Help text: Provide context below input
  • Error messages: Linked to input via aria-describedby
  • Keyboard: Full keyboard support

Select Component

Purpose

Selects allow users to choose from predefined options. Use instead of free-text input when options are limited.

When to Use

  • Limited number of options (< 30)
  • Options are clearly defined
  • Space is limited
  • Consistency is important

When NOT to Use

  • Large number of options (use autocomplete/combobox)
  • Users need to create new values
  • Options need rich formatting
  • Tree structure (use custom dropdown)

States

  • Default: Ready for selection
  • Open: Dropdown visible, showing options
  • Selected: Option chosen, highlighted
  • Disabled: Cannot be changed
  • Error: Invalid selection

Props

interface SelectProps {
  options: Array<{ label: string; value: string }>
  value?: string
  onChange?: (value: string) => void
  placeholder?: string
  disabled?: boolean
  error?: string
  size?: 'sm' | 'md' | 'lg'
  searchable?: boolean
}

Usage Examples

// Basic select
<Select
  options={[
    { label: 'Option 1', value: 'opt1' },
    { label: 'Option 2', value: 'opt2' },
    { label: 'Option 3', value: 'opt3' },
  ]}
  onChange={(value) => setSelected(value)}
/>

// With placeholder
<Select
  options={colors}
  placeholder="Choose a color"
/>

// Searchable
<Select
  options={users}
  searchable
  placeholder="Search users..."
/>

// With error
<Select
  options={categories}
  error="Please select a category"
/>

Accessibility

  • Keyboard navigation: Arrow keys to navigate options
  • Type to search: First letter jumps to matching option
  • Screen readers: Options properly announced
  • Labels: Paired with form labels

Card Component

Purpose

Cards are containers for grouped content. They provide visual organization and hierarchy.

When to Use

  • Organizing related content
  • Creating visual grouping
  • Elevating content hierarchy
  • Creating repeating patterns (grids)

Variants

Default

<Card>
  <CardTitle>Title</CardTitle>
  <CardDescription>Description</CardDescription>
</Card>

Interactive

<Card interactive onClick={handleClick}>
  Content
</Card>

Elevated

<Card elevation="high">
  Important content
</Card>

Props

interface CardProps {
  children: React.ReactNode
  title?: string
  description?: string
  interactive?: boolean
  elevation?: 'low' | 'medium' | 'high'
  padding?: 'sm' | 'md' | 'lg'
  onClick?: () => void
}

Usage Examples

// Basic card
<Card>
  <h3>Feature Title</h3>
  <p>Feature description</p>
</Card>

// Card with header
<Card>
  <Card.Header>
    <h3>Settings</h3>
  </Card.Header>
  <Card.Body>
    {/* Settings content */}
  </Card.Body>
</Card>

// Card grid
<div className="grid gap-4">
  {items.map((item) => (
    <Card key={item.id} interactive onClick={() => select(item)}>
      <h4>{item.name}</h4>
      <p>{item.description}</p>
    </Card>
  ))}
</div>

Form Guidelines

Structure

<form onSubmit={handleSubmit}>
  <fieldset>
    <legend>Form Section</legend>

    {/* Form field group */}
    <div className="form-group">
      <label htmlFor="email">Email Address</label>
      <Input
        id="email"
        type="email"
        required
        onChange={(e) => setEmail(e.target.value)}
      />
      <span className="help-text">We'll never share your email</span>
    </div>

    {/* Error display */}
    {error && <Alert variant="danger">{error}</Alert>}

    {/* Actions */}
    <div className="form-actions">
      <Button type="submit">Submit</Button>
      <Button variant="secondary" type="reset">
        Clear
      </Button>
    </div>
  </fieldset>
</form>

Best Practices

  1. Logical grouping: Group related fields
  2. Clear labels: Every input needs a clear label
  3. Help text: Provide context when needed
  4. Validation: Validate on change and submit
  5. Error handling: Show errors clearly
  6. Spacing: Adequate space between fields
  7. Action buttons: Submit and cancel buttons
  8. Loading state: Show feedback during submission

Responsive Components

Breakpoints

  • Mobile: < 640px
  • Tablet: 640px - 1023px
  • Desktop: 1024px+

Responsive Guidelines

  • Buttons: Increase padding on mobile
  • Inputs: Full width on mobile, constrained on desktop
  • Cards: Stack on mobile, grid on desktop
  • Spacing: Increase gaps on mobile for touch
  • Text: Increase sizes on mobile for readability

Dark Mode Support

All components support dark mode automatically through CSS variables:

// No additional code needed
<Button>Works in light and dark mode</Button>

Colors automatically adjust based on prefers-color-scheme or data-theme attribute.

Performance Considerations

Optimization Tips

  1. Memoization: Use React.memo for expensive components
  2. Lazy loading: Lazy load heavy components
  3. Virtualization: Virtualize long lists
  4. Code splitting: Split large component libraries
  5. Bundle size: Keep components lean

Testing Components

Unit Tests

Test component rendering, props, and behavior:

describe('Button', () => {
  it('renders with text', () => {
    render(<Button>Click me</Button>)
    expect(screen.getByText('Click me')).toBeInTheDocument()
  })

  it('calls onClick when clicked', () => {
    const onClick = vi.fn()
    render(<Button onClick={onClick}>Click</Button>)
    fireEvent.click(screen.getByText('Click'))
    expect(onClick).toHaveBeenCalled()
  })

  it('is disabled when disabled prop is true', () => {
    render(<Button disabled>Click</Button>)
    expect(screen.getByText('Click')).toBeDisabled()
  })
})

Accessibility Tests

  • Focus management
  • ARIA attributes
  • Keyboard navigation
  • Screen reader compatibility

Visual Tests

  • All states (default, hover, active, disabled)
  • All variants
  • All sizes
  • Dark mode

Common Patterns

Loading State Pattern

<Button loading={isLoading} disabled={isLoading}>
  {isLoading ? 'Saving...' : 'Save'}
</Button>

Error State Pattern

<>
  <Input error={error ? 'Required field' : ''} />
  {error && <Alert variant="danger">{error}</Alert>}
</>

Confirmation Pattern

{showConfirm ? (
  <div className="confirmation">
    <p>Are you sure?</p>
    <Button onClick={handleConfirm}>Yes</Button>
    <Button variant="secondary" onClick={handleCancel}>
      Cancel
    </Button>
  </div>
) : (
  <Button variant="danger" onClick={() => setShowConfirm(true)}>
    Delete
  </Button>
)}

Disabled State Pattern

<Button
  disabled={!isFormValid || isSubmitting}
  title={!isFormValid ? 'Please fill all required fields' : ''}
>
  Submit
</Button>

Customization

When to Customize

Components can be customized using the className prop for emergency needs:

<Button className="custom-style">Button</Button>

When NOT to Customize

Don't customize for:

  • Color changes (create a new variant)
  • Size changes (use size prop)
  • Spacing changes (design system mismatch)
  • Behavior changes (create a new component)

Contributing New Components

When creating new components:

  1. Define purpose: What problem does it solve?
  2. Design variants: What variations are needed?
  3. Plan props: What customization is needed?
  4. Accessibility: ARIA attributes, keyboard support
  5. Documentation: Add to this guide
  6. Tests: Unit and accessibility tests
  7. Examples: Real usage examples

Further Resources