Skip to main content
The UI Components skill provides best practices for building user interfaces with shadcn/ui, Tailwind CSS, and chart components. Learn the decision flow for creating components and maintaining a consistent design system.

Overview

This skill covers:
  • Decision flow - When to reuse vs create components
  • Component placement - Where different types of components belong
  • shadcn/ui integration - Installing and composing primitives
  • Tailwind conventions - Mobile-first responsive design
  • Chart references - Building data visualizations

Decision Flow

Before building any UI component, follow these steps in order. Stop as soon as you find a match.
1

Check Existing Components

Search web/components/ui/ and web/components/blocks/ for an existing component that fits.
# Search for button-related components
ls web/components/ui/*button*
ls web/components/blocks/*button*
If found, import via @/components/* and use it.
2

Check shadcn/ui Library

If no existing component matches, look up the equivalent at shadcn/ui.
# Install from web/ directory
npx shadcn@latest add button
npx shadcn@latest add dialog
If available, install and use it.
3

Create New Primitive

Only if steps 1 and 2 fail, create a new primitive under web/components/ui/.Keep it generic with no domain logic.

Component Placement

web/components/ui/ (Primitives)

UI primitives only - no domain logic, data fetching, or feature-specific state.
// ✅ Good: Generic button primitive
export function Button({ children, variant, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        "px-4 py-2 rounded-md",
        variant === "primary" && "bg-blue-600 text-white",
        variant === "secondary" && "bg-gray-200 text-gray-900"
      )}
      {...props}
    >
      {children}
    </button>
  )
}

// ❌ Bad: Domain-specific logic in primitive
export function Button({ children, ...props }: ButtonProps) {
  const { user } = useAuth() // ❌ No data fetching
  const handleSubmit = () => { // ❌ No domain logic
    submitForm(user.id)
  }
  return <button onClick={handleSubmit}>{children}</button>
}

web/components/blocks/ (Compositions)

Reusable compositions built from primitives. May contain layout and interaction patterns, but avoid feature-specific state.
// ✅ Good: Reusable modal composition
export function Modal({ isOpen, onClose, children }: ModalProps) {
  return (
    <Dialog open={isOpen} onOpenChange={onClose}>
      <DialogContent>
        {children}
      </DialogContent>
    </Dialog>
  )
}

// ❌ Bad: Feature-specific state
export function Modal() {
  const [users, setUsers] = useState([]) // ❌ Feature state
  useEffect(() => {
    fetchUsers().then(setUsers) // ❌ Data fetching
  }, [])
  return <Dialog>...</Dialog>
}

web/features/* (Feature Components)

Feature/domain UI that composes blocks + primitives and owns feature logic.
// ✅ Good: Feature component with domain logic
export function UserProfileModal({ userId }: Props) {
  const [user, setUser] = useState<User | null>(null)
  
  useEffect(() => {
    fetchUser(userId).then(setUser)
  }, [userId])
  
  return (
    <Modal isOpen={!!user} onClose={() => setUser(null)}>
      <Card>
        <CardHeader>
          <CardTitle>{user?.name}</CardTitle>
        </CardHeader>
        <CardContent>
          <p>{user?.bio}</p>
        </CardContent>
      </Card>
    </Modal>
  )
}

shadcn/ui Integration

Installing Components

# From web/ directory
cd web
npx shadcn@latest add button

Using Components

import { Button } from "@/components/ui/button"
import { Card, CardContent, CardHeader, CardTitle } from "@/components/ui/card"
import { Dialog, DialogContent, DialogHeader } from "@/components/ui/dialog"

export function UserCard({ user }: Props) {
  return (
    <Card>
      <CardHeader>
        <CardTitle>{user.name}</CardTitle>
      </CardHeader>
      <CardContent>
        <p>{user.email}</p>
        <Button variant="outline" size="sm">
          View Profile
        </Button>
      </CardContent>
    </Card>
  )
}

Customizing Components

Shadcn components are copied into your project, so you can customize them:
// web/components/ui/button.tsx
export function Button({ className, variant, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        buttonVariants({ variant }),
        className // Your custom classes
      )}
      {...props}
    />
  )
}

Tailwind Conventions

Mobile-First Responsive Design

<div className="flex-row md:flex-col">
  {/* Breaks mobile layout */}
</div>

Utility Classes Over Inline Styles

<div style={{ padding: '16px', backgroundColor: '#3b82f6' }}>
  Content
</div>

Use cn() for className Composition

import { cn } from "@/lib/utils"

function Button({ className, variant, ...props }: ButtonProps) {
  return (
    <button
      className={cn(
        // Base styles
        "px-4 py-2 rounded-md font-medium transition-colors",
        // Variant styles
        variant === "primary" && "bg-blue-600 text-white hover:bg-blue-700",
        variant === "secondary" && "bg-gray-200 text-gray-900 hover:bg-gray-300",
        // Consumer styles
        className
      )}
      {...props}
    />
  )
}

Responsive Breakpoints

<div className="
  w-full           {/* Mobile: full width */}
  sm:w-1/2         {/* Small: half width */}
  md:w-1/3         {/* Medium: third width */}
  lg:w-1/4         {/* Large: quarter width */}
  xl:w-1/5         {/* Extra large: fifth width */}
">
  Content
</div>
Breakpoints:
  • sm: 640px
  • md: 768px
  • lg: 1024px
  • xl: 1280px
  • 2xl: 1536px

Charts with Recharts

See .github/skills/ui-components/references/charts.md for comprehensive chart documentation.

Installing Chart Components

cd web
npx shadcn@latest add chart

Basic Chart Example

import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, Legend } from 'recharts'
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card'

const data = [
  { month: 'Jan', revenue: 4000, expenses: 2400 },
  { month: 'Feb', revenue: 3000, expenses: 1398 },
  { month: 'Mar', revenue: 2000, expenses: 9800 },
]

export function RevenueChart() {
  return (
    <Card>
      <CardHeader>
        <CardTitle>Revenue vs Expenses</CardTitle>
      </CardHeader>
      <CardContent>
        <LineChart width={600} height={300} data={data}>
          <CartesianGrid strokeDasharray="3 3" />
          <XAxis dataKey="month" />
          <YAxis />
          <Tooltip />
          <Legend />
          <Line type="monotone" dataKey="revenue" stroke="#3b82f6" />
          <Line type="monotone" dataKey="expenses" stroke="#ef4444" />
        </LineChart>
      </CardContent>
    </Card>
  )
}

Anti-Patterns to Avoid

Avoid these common mistakes:
  • Feature logic in primitives - Keep web/components/ui/* generic
  • Skipping shadcn/ui - Check the library before creating new primitives
  • Deep relative imports - Use @/components/* absolute imports
  • Desktop-first styling - Always write mobile classes first
  • Inline styles - Use Tailwind utilities instead

Quality Checklist

Before completing any UI component task:
  • Component is in the correct folder (ui/ vs blocks/ vs features/)
  • web/components/ui/* contains no domain logic or data fetching
  • Classes are mobile-first
  • Spacing and typography match existing patterns
  • cn() is used for className composition
  • No inline styles (unless unavoidable)
  • Checked shadcn/ui library before creating new primitive
  • Imported primitives from @/components/ui/*
  • Customizations maintain design system consistency

Skill Structure

.github/skills/ui-components/
├── SKILL.md                    # This overview
└── references/
    ├── charts.md              # Chart documentation
    ├── charts-area.md         # Area chart examples
    ├── charts-bar.md          # Bar chart examples
    ├── charts-line.md         # Line chart examples
    └── charts-pie.md          # Pie chart examples

References

Use the shadcn/ui CLI to explore available components: npx shadcn@latest add (without arguments) shows an interactive list.

Build docs developers (and LLMs) love