Skip to main content

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., DataTablePagination from Pagination)

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

  1. Check for updates: Run npx shadcn-ui@latest diff to see changes
  2. Review customizations: List all customized components
  3. Test thoroughly: Ensure customizations still work
  4. 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

  1. Button (src/components/ui/button.tsx)

    • Added loading states
    • Custom size variants
    • Integrated with form submission states
  2. Input OTP (src/components/ui/input-otp.tsx)

    • Customized for our auth flow
    • Added auto-submit functionality
    • Enhanced error states
  3. 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

  1. Verification Card (src/components/auth/verification-card.tsx)

    • Composes Card, Input OTP, and Button
    • Domain-specific auth component
  2. Organization Switcher (src/components/auth/organization-switcher.tsx)

    • Uses Select and Avatar components
    • Adds organization-specific logic

Resources