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.
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.
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.
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
Install Single Component
Install Multiple Components
List Available 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
❌ Desktop-First (Avoid)
✅ Mobile-First (Correct)
< div className = "flex-row md:flex-col" >
{ /* Breaks mobile layout */ }
</ div >
Utility Classes Over Inline Styles
❌ Inline Styles
✅ Tailwind Classes
< 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:
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.