Advanced

Advanced Theming

Take your Feather UI themes to the next level with advanced customization techniques.

Component-Level Theming

While global theming with CSS variables provides a consistent look, sometimes you need to customize specific components differently. Feather UI supports component-level customization through variant props and CSS composition.

Custom Button Variants

Extend the button component with custom variants

components/ui/custom-button.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
// components/ui/custom-button.tsx
import { cva } from "class-variance-authority"
import { Button, buttonVariants } from "@/components/ui/button"

// Extend the existing button variants
const customButtonVariants = cva(
  "inline-flex items-center justify-center whitespace-nowrap rounded-md text-sm font-medium ring-offset-background transition-colors focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-2 disabled:pointer-events-none disabled:opacity-50",
  {
    variants: {
      variant: {
        // Inherit all existing variants
        ...buttonVariants.variants.variant,
        // Add new variants
        gradient: 
          "bg-gradient-to-r from-blue-600 to-indigo-600 text-primary-foreground hover:from-blue-700 hover:to-indigo-700",
        success: 
          "bg-green-600 text-primary-foreground hover:bg-green-700",
        warning: 
          "bg-amber-500 text-primary-foreground hover:bg-amber-600",
        glass: 
          "bg-white/10 backdrop-blur-md border border-white/20 hover:bg-white/20 text-white",
      },
    },
    defaultVariants: {
      variant: "default",
      size: "default",
    },
  }
)

// Export an enhanced button with the new variants
export function CustomButton({ variant, ...props }) {
  // Pass all props to the original Button but override the className
  return <Button {...props} className={customButtonVariants({ variant })} />
}

// Usage:
// <CustomButton variant="gradient">Gradient Button</CustomButton>
// <CustomButton variant="success">Success Button</CustomButton>

Component States

Customize interactive states of components

Custom Component States
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// styles/custom-card.css
.custom-card {
  transition: all 0.2s ease;
  border-left: 4px solid transparent;
}

.custom-card:hover {
  transform: translateY(-2px);
  border-left: 4px solid var(--primary);
  box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
}

// In your React component
import "styles/custom-card.css";

function CustomCard({ children }) {
  return (
    <Card className="custom-card">
      {children}
    </Card>
  );
}

Scoped Themes

Apply different theme values to specific sections

Scoped Theming Example
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// Create a scoped theme section with different values
<div className="themed-section" style={{
  "--primary": "340 82% 52%",
  "--primary-foreground": "0 0% 98%",
  "--secondary": "340 4.8% 95.9%",
  "--secondary-foreground": "340 5.9% 10%",
}}>
  {/* Components in here will use the scoped theme values */}
  <Card>
    <CardHeader>
      <CardTitle>Scoped Theme Section</CardTitle>
    </CardHeader>
    <CardContent>
      <Button>Themed Button</Button>
    </CardContent>
  </Card>
</div>

Multi-Theme Support

Support multiple themes beyond just light and dark mode. This allows users to switch between different color schemes like "blue", "purple", "green", etc., all while maintaining dark/light mode compatibility.

Theme Provider Setup

Extended theme provider with multiple theme support

lib/theme-provider.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
// lib/theme-provider.tsx
"use client"

import { createContext, useContext, useEffect, useState } from "react"

type Theme = "blue" | "green" | "purple" | "rose" | "amber"
type Mode = "light" | "dark" | "system"

type ThemeProviderProps = {
  children: React.ReactNode
  defaultTheme?: Theme
  defaultMode?: Mode
}

type ThemeContextType = {
  theme: Theme
  mode: Mode
  setTheme: (theme: Theme) => void
  setMode: (mode: Mode) => void
}

const ThemeContext = createContext<ThemeContextType | undefined>(undefined)

export function ThemeProvider({
  children,
  defaultTheme = "blue",
  defaultMode = "system",
}: ThemeProviderProps) {
  const [theme, setTheme] = useState<Theme>(defaultTheme)
  const [mode, setMode] = useState<Mode>(defaultMode)
  
  useEffect(() => {
    const root = document.documentElement
    
    // Remove previous theme classes
    root.classList.remove("theme-blue", "theme-green", "theme-purple", "theme-rose", "theme-amber")
    
    // Add new theme class
    root.classList.add(`theme-${theme}`)
    
    // Handle mode
    if (mode === "system") {
      const systemTheme = window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light"
      root.classList.toggle("dark", systemTheme === "dark")
    } else {
      root.classList.toggle("dark", mode === "dark")
    }
  }, [theme, mode])
  
  useEffect(() => {
    const mediaQuery = window.matchMedia("(prefers-color-scheme: dark)")
    
    const handleChange = () => {
      if (mode === "system") {
        document.documentElement.classList.toggle("dark", mediaQuery.matches)
      }
    }
    
    mediaQuery.addEventListener("change", handleChange)
    return () => mediaQuery.removeEventListener("change", handleChange)
  }, [mode])
  
  const value = {
    theme,
    mode,
    setTheme,
    setMode,
  }
  
  return <ThemeContext.Provider value={value}>{children}</ThemeContext.Provider>
}

export const useTheme = () => {
  const context = useContext(ThemeContext)
  if (context === undefined) {
    throw new Error("useTheme must be used within a ThemeProvider")
  }
  return context
}

CSS Configuration

Multi-theme CSS variables setup

globals.css
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
/* globals.css */
@tailwind base;
@tailwind components;
@tailwind utilities;

@layer base {
  /* Base variables applicable to all themes */
  :root {
    --radius: 0.5rem;
  }
  
  /* Blue theme (default) */
  .theme-blue {
    --primary: 217.2 91.2% 59.8%;
    --primary-foreground: 0 0% 98%;
    --secondary: 214.3 31.8% 91.4%;
    --secondary-foreground: 222.2 47.4% 11.2%;
    --accent: 210 40% 96.1%;
    --accent-foreground: 222.2 47.4% 11.2%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
  }
  
  /* Green theme */
  .theme-green {
    --primary: 142.1 76.2% 36.3%;
    --primary-foreground: 0 0% 98%;
    --secondary: 143 30% 96%;
    --secondary-foreground: 140 30% 10%;
    --accent: 140 40% 94%;
    --accent-foreground: 140 40% 10%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
  }
  
  /* Purple theme */
  .theme-purple {
    --primary: 272.1 91.7% 65.1%;
    --primary-foreground: 0 0% 98%;
    --secondary: 270 30% 96%;
    --secondary-foreground: 272 30% 10%;
    --accent: 269 40% 94%;
    --accent-foreground: 269 40% 10%;
    --destructive: 0 84.2% 60.2%;
    --destructive-foreground: 0 0% 98%;
  }
  
  /* More themes... */
  
  /* Light mode for all themes */
  :root,
  .theme-blue,
  .theme-green,
  .theme-purple {
    --background: 0 0% 100%;
    --foreground: 222.2 47.4% 11.2%;
    --card: 0 0% 100%;
    --card-foreground: 222.2 47.4% 11.2%;
    --popover: 0 0% 100%;
    --popover-foreground: 222.2 47.4% 11.2%;
    --muted: 210 40% 96.1%;
    --muted-foreground: 215.4 16.3% 46.9%;
    --border: 214.3 31.8% 91.4%;
    --input: 214.3 31.8% 91.4%;
    --ring: 222.2 47.4% 11.2%;
  }
  
  /* Dark mode for all themes */
  .dark,
  .dark .theme-blue,
  .dark .theme-green,
  .dark .theme-purple {
    --background: 222.2 84% 4.9%;
    --foreground: 210 40% 98%;
    --card: 222.2 84% 4.9%;
    --card-foreground: 210 40% 98%;
    --popover: 222.2 84% 4.9%;
    --popover-foreground: 210 40% 98%;
    --muted: 217.2 32.6% 17.5%;
    --muted-foreground: 215 20.2% 65.1%;
    --border: 217.2 32.6% 17.5%;
    --input: 217.2 32.6% 17.5%;
    --ring: 212.7 26.8% 83.9%;
  }
}

Theme Selector

UI component for selecting between multiple themes

components/theme-selector.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
"use client"

import { useState } from "react"
import { Check, Moon, Sun, Laptop } from "lucide-react"
import { useTheme } from "@/lib/theme-provider"
import { Button } from "@/components/ui/button"
import {
  DropdownMenu,
  DropdownMenuContent,
  DropdownMenuItem,
  DropdownMenuTrigger,
  DropdownMenuSeparator,
  DropdownMenuLabel,
} from "@/components/ui/dropdown-menu"

export function ThemeSelector() {
  const { theme, mode, setTheme, setMode } = useTheme()
  
  return (
    <DropdownMenu>
      <DropdownMenuTrigger asChild>
        <Button variant="outline" size="icon">
          {mode === "light" && <Sun className="h-4 w-4" />}
          {mode === "dark" && <Moon className="h-4 w-4" />}
          {mode === "system" && <Laptop className="h-4 w-4" />}
        </Button>
      </DropdownMenuTrigger>
      <DropdownMenuContent align="end">
        <DropdownMenuLabel>Mode</DropdownMenuLabel>
        <DropdownMenuItem onClick={() => setMode("light")}>
          <Sun className="mr-2 h-4 w-4" />
          <span>Light</span>
          {mode === "light" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setMode("dark")}>
          <Moon className="mr-2 h-4 w-4" />
          <span>Dark</span>
          {mode === "dark" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setMode("system")}>
          <Laptop className="mr-2 h-4 w-4" />
          <span>System</span>
          {mode === "system" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
        
        <DropdownMenuSeparator />
        <DropdownMenuLabel>Theme</DropdownMenuLabel>
        
        <DropdownMenuItem onClick={() => setTheme("blue")}>
          <div className="mr-2 h-4 w-4 rounded-full bg-blue-500" />
          <span>Blue</span>
          {theme === "blue" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("green")}>
          <div className="mr-2 h-4 w-4 rounded-full bg-green-500" />
          <span>Green</span>
          {theme === "green" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
        <DropdownMenuItem onClick={() => setTheme("purple")}>
          <div className="mr-2 h-4 w-4 rounded-full bg-purple-500" />
          <span>Purple</span>
          {theme === "purple" && <Check className="ml-auto h-4 w-4" />}
        </DropdownMenuItem>
      </DropdownMenuContent>
    </DropdownMenu>
  )
}

This approach allows users to select both a color theme (blue, green, etc.) and a mode (light/dark), making your UI highly customizable.

Custom Theme Contexts

For more complex applications, you might need different themes for different sections or pages. Custom theme contexts allow you to implement section-specific theming.

Section-Specific Theme Provider

Create isolated theming for specific sections

lib/section-theme-provider.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
// lib/section-theme-provider.tsx
"use client"

import { createContext, useContext, useEffect, useState } from "react"

type SectionTheme = {
  bgColor: string
  textColor: string
  accentColor: string
}

type SectionThemeProviderProps = {
  children: React.ReactNode
  defaultTheme: SectionTheme
}

type SectionThemeContextType = {
  theme: SectionTheme
  setTheme: (theme: SectionTheme) => void
}

const SectionThemeContext = createContext<SectionThemeContextType | undefined>(undefined)

export function SectionThemeProvider({
  children,
  defaultTheme,
}: SectionThemeProviderProps) {
  const [theme, setTheme] = useState<SectionTheme>(defaultTheme)
  
  return (
    <SectionThemeContext.Provider value={{ theme, setTheme }}>
      <div style={{ 
        backgroundColor: theme.bgColor,
        color: theme.textColor,
        "--section-accent": theme.accentColor
      } as React.CSSProperties}>
        {children}
      </div>
    </SectionThemeContext.Provider>
  )
}

export const useSectionTheme = () => {
  const context = useContext(SectionThemeContext)
  if (context === undefined) {
    throw new Error("useSectionTheme must be used within a SectionThemeProvider")
  }
  return context
}

// Usage:
//
// <SectionThemeProvider defaultTheme={{
//   bgColor: "#f0f9ff",
//   textColor: "#0f172a",
//   accentColor: "#3b82f6"
// }}>
//   <SectionContent />
// </SectionThemeProvider>

Practical Example: Dashboard Sections

Different themes for different dashboard sections

app/dashboard/page.tsx
TSX
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
"use client"

import { SectionThemeProvider } from "@/lib/section-theme-provider"
import { DashboardWidget } from "@/components/dashboard-widget"

export function Dashboard() {
  return (
    <div className="grid grid-cols-1 md:grid-cols-2 gap-6">
      {/* Analytics Section with blue theme */}
      <SectionThemeProvider defaultTheme={{
        bgColor: "#f0f9ff", // light blue bg
        textColor: "#0f172a",
        accentColor: "#3b82f6" // blue accent
      }}>
        <DashboardWidget
          title="Analytics"
          icon={<ChartBar />}
          data={analyticsData}
        />
      </SectionThemeProvider>
      
      {/* Revenue Section with green theme */}
      <SectionThemeProvider defaultTheme={{
        bgColor: "#f0fdf4", // light green bg
        textColor: "#0f172a",
        accentColor: "#22c55e" // green accent
      }}>
        <DashboardWidget
          title="Revenue"
          icon={<DollarSign />}
          data={revenueData}
        />
      </SectionThemeProvider>
      
      {/* User Section with purple theme */}
      <SectionThemeProvider defaultTheme={{
        bgColor: "#faf5ff", // light purple bg
        textColor: "#0f172a",
        accentColor: "#a855f7" // purple accent
      }}>
        <DashboardWidget
          title="Users"
          icon={<Users />}
          data={userData}
        />
      </SectionThemeProvider>
      
      {/* Alerts Section with amber theme */}
      <SectionThemeProvider defaultTheme={{
        bgColor: "#fffbeb", // light amber bg
        textColor: "#0f172a",
        accentColor: "#f59e0b" // amber accent
      }}>
        <DashboardWidget
          title="Alerts"
          icon={<BellRing />}
          data={alertsData}
        />
      </SectionThemeProvider>
    </div>
  )
}

This approach creates visual separation between different functional areas while maintaining a cohesive overall design. Each section can have its own theme that matches its purpose.

Theme Persistence

Save user theme preferences to provide a consistent experience across sessions. Combine localStorage with server-side persistence for authenticated users.

Local Storage Theme Persistence

Save theme preferences to local storage

lib/theme-storage.ts
TYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// lib/theme-storage.ts
type ThemePreferences = {
  theme: string
  mode: string
}

// Save theme preferences to localStorage
export function saveThemePreferences(preferences: ThemePreferences): void {
  if (typeof window !== 'undefined') {
    localStorage.setItem('theme-preferences', JSON.stringify(preferences))
  }
}

// Load theme preferences from localStorage
export function loadThemePreferences(): ThemePreferences | null {
  if (typeof window !== 'undefined') {
    const saved = localStorage.getItem('theme-preferences')
    if (saved) {
      try {
        return JSON.parse(saved) as ThemePreferences
      } catch (e) {
        console.error('Failed to parse theme preferences', e)
      }
    }
  }
  return null
}

// Clear theme preferences from localStorage
export function clearThemePreferences(): void {
  if (typeof window !== 'undefined') {
    localStorage.removeItem('theme-preferences')
  }
}

// Update the theme provider to use these functions
// Inside ThemeProvider:

useEffect(() => {
  // On mount, load saved preferences
  const savedPreferences = loadThemePreferences()
  if (savedPreferences) {
    setTheme(savedPreferences.theme as Theme)
    setMode(savedPreferences.mode as Mode)
  }
}, [])

// When theme or mode changes, save to localStorage
useEffect(() => {
  saveThemePreferences({ theme, mode })
}, [theme, mode])

Server-Side Persistence

Save theme preferences for authenticated users

Server-Side Theme Persistence
TYPESCRIPT
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
// API route for saving theme preferences
// app/api/user/preferences/route.ts
import { NextResponse } from 'next/server'
import { getServerSession } from 'next-auth/next'
import { authOptions } from '@/lib/auth'
import prisma from '@/lib/prisma'

export async function POST(request: Request) {
  const session = await getServerSession(authOptions)
  
  if (!session || !session.user) {
    return NextResponse.json({ error: 'Unauthorized' }, { status: 401 })
  }
  
  try {
    const data = await request.json()
    const { theme, mode } = data
    
    // Update or create user preferences in database
    await prisma.userPreferences.upsert({
      where: {
        userId: session.user.id,
      },
      update: {
        theme,
        mode,
      },
      create: {
        userId: session.user.id,
        theme,
        mode,
      },
    })
    
    return NextResponse.json({ success: true })
  } catch (error) {
    return NextResponse.json({ error: 'Failed to save preferences' }, { status: 500 })
  }
}

// Client-side usage in theme provider
useEffect(() => {
  // Save to localStorage for all users
  saveThemePreferences({ theme, mode })
  
  // Also save to server for authenticated users
  if (session?.user) {
    fetch('/api/user/preferences', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ theme, mode }),
    }).catch(error => console.error('Failed to save preferences to server:', error))
  }
}, [theme, mode, session?.user])

// Load preferences on initial mount
useEffect(() => {
  async function loadUserPreferences() {
    if (session?.user) {
      try {
        const response = await fetch('/api/user/preferences')
        if (response.ok) {
          const data = await response.json()
          setTheme(data.theme as Theme)
          setMode(data.mode as Mode)
        }
      } catch (error) {
        // Fall back to localStorage if server request fails
        const savedPreferences = loadThemePreferences()
        if (savedPreferences) {
          setTheme(savedPreferences.theme as Theme)
          setMode(savedPreferences.mode as Mode)
        }
      }
    } else {
      // Use localStorage for non-authenticated users
      const savedPreferences = loadThemePreferences()
      if (savedPreferences) {
        setTheme(savedPreferences.theme as Theme)
        setMode(savedPreferences.mode as Mode)
      }
    }
  }
  
  loadUserPreferences()
}, [session?.user])

By saving preferences on the server, users will have a consistent theme experience across devices. This is particularly useful for SaaS products or applications with user accounts.

Theme Transitions

Add smooth transitions between themes for a polished user experience. Proper transitions can make theme switching feel seamless and professional.

CSS Transitions

Apply transitions to theme changes

Theme Transitions CSS
CSS
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/* Add this to your globals.css */
:root {
  /* Other CSS variables... */
  
  /* Define transition for theme changes */
  --theme-transition-duration: 400ms;
  --theme-transition-ease: cubic-bezier(0.16, 1, 0.3, 1);
}

/* Apply transitions to color properties */
*, *::before, *::after {
  transition-property: color, background-color, border-color, text-decoration-color, fill, stroke;
  transition-timing-function: var(--theme-transition-ease);
  transition-duration: var(--theme-transition-duration);
}

/* Don't transition certain elements (can cause issues) */
button, a, input, select, textarea, [role="button"] {
  transition-property: transform, opacity, filter, box-shadow;
}

/* Transition modifiers for specific elements */
.theme-transition-fast {
  transition-duration: 200ms;
}

.theme-transition-slow {
  transition-duration: 600ms;
}

.theme-transition-none {
  transition: none !important;
}

/* Disable transitions during page load */
.preload * {
  transition: none !important;
}

/* In your layout.js/.tsx, add a script to remove the preload class */
// <script dangerouslySetInnerHTML={{
//   __html: `
//     document.documentElement.classList.add('preload');
//     window.addEventListener('load', function() {
//       document.documentElement.classList.remove('preload');
//     });
//   `
// }} />