Source:
ocean/docs/guides/shadcn-customization.md| ✏️ Edit on GitHub
shadcn/ui Customization Guide
This guide explains how to effectively customize shadcn/ui components in the Ocean platform while maintaining consistency and upgradability.
Overview
Ocean uses shadcn/ui as its component library. Unlike traditional component libraries, shadcn/ui provides copy-and-paste components that live directly in your codebase, giving you full control over customization.
When to Fork vs Extend
Extend (Preferred Approach)
Use component extensions when:
- Adding new variants to existing components
- Adjusting spacing, colors, or sizes using design tokens
- Adding conditional logic or props
- Composing multiple components together
Example: Adding a new button variant
// ✅ Good: Extend using cva variants
// src/components/ui/button.tsx
const buttonVariants = cva('...', {
variants: {
variant: {
// Existing variants...
ghost: '...',
// Add your custom variant
brand: 'bg-brand text-brand-foreground hover:bg-brand/90',
},
},
})
Fork (Use Sparingly)
Fork components only when:
- The component structure needs fundamental changes
- You need to replace the underlying implementation
- The component's API needs to be completely different
- You're creating a domain-specific version (e.g.,
DataTablePaginationfromPagination)
Example: Creating a specialized component
// ✅ Good: Fork for domain-specific needs
// src/components/billing/subscription-card.tsx
import { Card } from '@/components/ui/card'
// This is a new component that uses Card internally
export function SubscriptionCard({ subscription }: Props) {
return (
<Card className="relative overflow-hidden">{/* Custom subscription-specific layout */}</Card>
)
}
Best Practices
1. Use Semantic Tokens
Always use semantic color tokens instead of raw Tailwind colors:
// ❌ Bad: Raw colors
<Button className="bg-gray-900 text-white">
// ✅ Good: Semantic tokens
<Button className="bg-primary text-primary-foreground">
2. Maintain Consistency
When customizing, follow the existing patterns in the codebase:
// Check existing component patterns
const existingVariants = {
size: {
default: 'h-10 px-4 py-2',
sm: 'h-9 rounded-md px-3',
lg: 'h-11 rounded-md px-8',
},
}
// Follow the same structure for new variants
const customVariants = {
size: {
// ...existing,
xs: 'h-8 rounded px-2 text-xs', // Consistent with pattern
},
}
3. Document Customizations
Add comments explaining why customizations were made:
/**
* Custom "loading" variant for async operations
* Shows spinner and disables interaction during API calls
*/
loading: "relative text-muted-foreground pointer-events-none",
4. Preserve Accessibility
Never remove accessibility features when customizing:
// ✅ Good: Preserve ARIA attributes
<DialogContent
className="custom-dialog"
aria-describedby={description ? "dialog-description" : undefined}
>
// ❌ Bad: Removing accessibility
<DialogContent className="custom-dialog">
Common Customization Patterns
Adding New Variants
// 1. Extend the variant definition
const alertVariants = cva('...', {
variants: {
variant: {
// Existing...
success: 'bg-success/10 text-success border-success/20',
warning: 'bg-warning/10 text-warning border-warning/20',
},
},
})
// 2. Update TypeScript types
interface AlertProps extends VariantProps<typeof alertVariants> {
// Component props...
}
Composing Components
// Create higher-level components by composition
export function MetricCard({ title, value, change, trend }: MetricCardProps) {
return (
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">{title}</CardTitle>
<TrendIcon trend={trend} />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">{value}</div>
<p className="text-xs text-muted-foreground">{change}</p>
</CardContent>
</Card>
)
}
Theme-Aware Components
// Use CSS variables for theme-aware customization
export function ThemedComponent() {
return (
<div
style={{
// These values change with theme
backgroundColor: 'hsl(var(--card))',
borderColor: 'hsl(var(--border))',
}}
>
{/* Content */}
</div>
)
}
Update Policy
Updating shadcn/ui Components
- Check for updates: Run
npx shadcn-ui@latest diffto see changes - Review customizations: List all customized components
- Test thoroughly: Ensure customizations still work
- Update incrementally: Update one component at a time
Version Control Strategy
# Before updating a component
git checkout -b update/shadcn-button
# Copy new component
npx shadcn-ui@latest add button --overwrite
# Review changes
git diff src/components/ui/button.tsx
# Reapply customizations manually
# Test thoroughly
# Commit with clear message
git commit -m "chore: update shadcn button component to latest
- Updated base component from shadcn/ui
- Preserved custom 'brand' variant
- Maintained accessibility improvements"
Component Customization Checklist
Before customizing a shadcn/ui component:
- Can this be achieved with className or style props?
- Can this be done by extending variants?
- Is this customization reusable across the app?
- Have I preserved all accessibility features?
- Is the customization documented?
- Will this survive component updates?
Examples in Ocean
Extended Components
-
Button (
src/components/ui/button.tsx)- Added loading states
- Custom size variants
- Integrated with form submission states
-
Input OTP (
src/components/ui/input-otp.tsx)- Customized for our auth flow
- Added auto-submit functionality
- Enhanced error states
-
Data Table (
src/components/data-table/index.tsx)- Built on top of Table component
- Added sorting, filtering, and pagination
- Integrated with TanStack Table
Composed Components
-
Verification Card (
src/components/auth/verification-card.tsx)- Composes Card, Input OTP, and Button
- Domain-specific auth component
-
Organization Switcher (
src/components/auth/organization-switcher.tsx)- Uses Select and Avatar components
- Adds organization-specific logic