Skip to main content

Source: ocean/docs/patterns/ui-utilities.md | ✏️ Edit on GitHub

UI Utilities and Patterns

Ocean provides powerful UI utilities and patterns for building responsive, theme-aware interfaces with excellent user experience.

Typography System

Using Typography Classes

Ocean uses a consistent typography system based on the dashboard patterns:

import { typography } from '@/lib/typography'

// Direct class usage
<h1 className={typography.h1}>Dashboard</h1>
<p className={typography.muted}>Manage your account</p>

// With additional classes
<h2 className={cn(typography.h2, "text-primary")}>Section Title</h2>

// Component usage
import { TypographyH1, TypographyP } from '@/components/ui/typography'

<TypographyH1>Dashboard</TypographyH1>
<TypographyP>Welcome to your dashboard</TypographyP>

Page Headers

Consistent page header pattern:

import { PageHeader } from '@/components/ui/page-header'

// Simple usage
<PageHeader
heading="Settings"
description="Manage your account settings"
/>

// With actions
<div className="flex items-center justify-between">
<PageHeader heading="Team Members" />
<Button>Invite Member</Button>
</div>

// Custom pattern
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
<div>
<h1 className="text-3xl font-bold tracking-tight">Dashboard</h1>
<p className="text-muted-foreground">Overview of your account</p>
</div>
{/* Page content */}
</div>

Responsive Design

useIsMobile Hook

Detect mobile devices for responsive behavior:

import { useIsMobile } from '@/hooks/use-is-mobile'

function ResponsiveComponent() {
const isMobile = useIsMobile()

if (isMobile) {
return <MobileLayout />
}

return <DesktopLayout />
}

// With conditional features
function Navigation() {
const isMobile = useIsMobile()

return (
<nav>
{!isMobile && <SearchBar />}
<NavItems collapsed={isMobile} />
{isMobile && <MobileMenu />}
</nav>
)
}

Container Queries (Tailwind v4)

Component-level responsive design:

// Container that enables queries
<div className="@container">
<div className="grid grid-cols-1 @sm:grid-cols-2 @lg:grid-cols-3 gap-4">
{items.map(item => (
<Card key={item.id}>
<CardHeader className="@md:flex-row @md:items-center">
<CardTitle className="@lg:text-2xl">{item.title}</CardTitle>
</CardHeader>
</Card>
))}
</div>
</div>

// Named containers
<div className="@container/sidebar">
<nav className="space-y-1 @sm/sidebar:space-y-2">
{/* Responds to sidebar container size */}
</nav>
</div>

Theme Utilities

Theme-Aware Assets

Dynamic logos based on current theme:

import { getThemedLogo, getThemedLogomark } from '@/config/assets'
import { useTheme } from '@/hooks/use-theme'

function Logo() {
const { theme } = useTheme()
const [logo, setLogo] = useState('')

useEffect(() => {
const systemPref = window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'

setLogo(getThemedLogo(theme, systemPref))
}, [theme])

return <img src={logo} alt="Logo" className="h-8" />
}

// Logomark for collapsed states
const logomark = getThemedLogomark(theme, systemPref)

Theme Favicon

Automatic favicon switching:

import { ThemeFavicon } from '@/components/theme-favicon'

// In your root layout
function RootLayout() {
return (
<>
<ThemeFavicon />
<App />
</>
)
}

Loading States

Skeleton Components

Pre-built loading skeletons for consistency:

import { DashboardLoadingSkeleton } from '@/components/dashboard-loading-skeleton'
import { AuthLoadingSkeleton } from '@/components/auth-loading-skeleton'
import { Skeleton } from '@/components/ui/skeleton'

// Full page skeletons
if (isLoading) return <DashboardLoadingSkeleton />

// Custom skeleton patterns
function CardSkeleton() {
return (
<Card>
<CardHeader>
<Skeleton className="h-6 w-48" />
<Skeleton className="h-4 w-full max-w-sm" />
</CardHeader>
<CardContent>
<div className="space-y-2">
<Skeleton className="h-4 w-full" />
<Skeleton className="h-4 w-3/4" />
</div>
</CardContent>
</Card>
)
}

// List skeleton
function ListSkeleton({ count = 5 }) {
return (
<div className="space-y-3">
{Array.from({ length: count }).map((_, i) => (
<Skeleton key={i} className="h-16 w-full" />
))}
</div>
)
}

Toast Notifications

Using Sonner

Ocean uses Sonner for toast notifications:

import { toast } from 'sonner'

// Simple notifications
toast.success('Profile updated')
toast.error('Something went wrong')
toast.info('New features available')

// With custom content
toast.custom((t) => (
<div className="flex items-center gap-3">
<CheckCircle className="h-5 w-5 text-success" />
<div>
<p className="font-medium">Upload complete</p>
<p className="text-sm text-muted-foreground">
File processed successfully
</p>
</div>
</div>
))

// With actions
toast.error('Failed to save', {
action: {
label: 'Retry',
onClick: () => retryAction()
}
})

// Loading toast
const toastId = toast.loading('Uploading...')
// Update when done
toast.success('Upload complete', { id: toastId })

Animation Patterns

CSS Transitions

Using Tailwind's transition utilities:

// Smooth hover states
<Button className="transition-colors hover:bg-primary/90" />

// Animated accordion
<div className={cn(
"overflow-hidden transition-all duration-300",
isOpen ? "max-h-96" : "max-h-0"
)} />

// Fade in/out
<div className={cn(
"transition-opacity duration-200",
isVisible ? "opacity-100" : "opacity-0"
)} />

Loading Spinners

import { Loader2 } from 'lucide-react'

// Button with loading state
<Button disabled={isLoading}>
{isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />}
{isLoading ? 'Loading...' : 'Submit'}
</Button>

// Standalone spinner
function LoadingSpinner() {
return (
<div className="flex items-center justify-center p-4">
<Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
</div>
)
}

Layout Patterns

Dashboard Layout

Standard dashboard page structure:

function DashboardPage() {
return (
<div className="flex-1 space-y-4 p-4 md:p-8 pt-6">
{/* Page header */}
<div className="flex items-center justify-between">
<PageHeader
heading="Dashboard"
description="Overview of your account"
/>
<Button>New Item</Button>
</div>

{/* Content grid */}
<div className="grid gap-4 @container">
<div className="grid gap-4 @lg:grid-cols-3">
<Card>...</Card>
<Card>...</Card>
<Card>...</Card>
</div>

<div className="grid gap-4 @lg:grid-cols-2">
<Card>...</Card>
<Card>...</Card>
</div>
</div>
</div>
)
}

Card Layouts

Common card patterns:

// Stats card
<Card>
<CardHeader className="flex flex-row items-center justify-between pb-2">
<CardTitle className="text-sm font-medium">Total Revenue</CardTitle>
<DollarSign className="h-4 w-4 text-muted-foreground" />
</CardHeader>
<CardContent>
<div className="text-2xl font-bold">$45,231.89</div>
<p className="text-xs text-muted-foreground">
+20.1% from last month
</p>
</CardContent>
</Card>

// Action card
<Card>
<CardHeader>
<CardTitle>Quick Actions</CardTitle>
<CardDescription>Common tasks and shortcuts</CardDescription>
</CardHeader>
<CardContent className="grid gap-2">
<Button variant="outline" className="justify-start">
<Plus className="mr-2 h-4 w-4" />
New Project
</Button>
<Button variant="outline" className="justify-start">
<Users className="mr-2 h-4 w-4" />
Invite Team
</Button>
</CardContent>
</Card>

Accessibility

Focus Management

// Skip navigation link
<a
href="#main-content"
className="sr-only focus:not-sr-only focus:absolute focus:top-4 focus:left-4"
>
Skip to main content
</a>

// Focus trap for modals
import { FocusTrap } from '@/components/focus-trap'

<Dialog>
<FocusTrap>
<DialogContent>...</DialogContent>
</FocusTrap>
</Dialog>

Screen Reader Support

// Visually hidden but readable
<span className="sr-only">Loading</span>

// Live regions for dynamic content
<div aria-live="polite" aria-atomic="true">
{status && <p>{status}</p>}
</div>

// Descriptive labels
<Button aria-label="Delete item">
<Trash className="h-4 w-4" />
</Button>

Performance

Lazy Loading

import { lazy, Suspense } from 'react'

const HeavyComponent = lazy(() => import('./HeavyComponent'))

function App() {
return (
<Suspense fallback={<LoadingSkeleton />}>
<HeavyComponent />
</Suspense>
)
}

Image Optimization

// Lazy load images
<img
src={imageSrc}
loading="lazy"
alt="Description"
className="aspect-video object-cover"
/>

// Responsive images
<picture>
<source
srcSet="/image-mobile.webp"
media="(max-width: 768px)"
type="image/webp"
/>
<source
srcSet="/image-desktop.webp"
media="(min-width: 769px)"
type="image/webp"
/>
<img src="/image-fallback.jpg" alt="Description" />
</picture>

Utility Functions

Class Name Merging

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

// Merge classes with conflict resolution
<div className={cn(
"px-4 py-2", // Base classes
"bg-white dark:bg-gray-900", // Theme classes
isActive && "ring-2 ring-primary", // Conditional
className // Override from props
)} />

// With variants
const buttonVariants = {
primary: "bg-primary text-primary-foreground",
secondary: "bg-secondary text-secondary-foreground",
ghost: "hover:bg-accent hover:text-accent-foreground"
}

<Button className={cn(buttonVariants[variant], className)} />