Take your Feather UI themes to the next level with advanced customization techniques.
Advanced techniques for customizing Feather UI
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.
Extend the button component with custom variants
// 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>
Customize interactive states of components
// 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>
);
}Apply different theme values to specific sections
// 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>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.
Extended theme provider with multiple theme support
// 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
}Multi-theme CSS variables setup
/* 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%;
}
}UI component for selecting between multiple themes
"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.
For more complex applications, you might need different themes for different sections or pages. Custom theme contexts allow you to implement section-specific theming.
Create isolated theming for specific sections
// 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>
Different themes for different dashboard sections
"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.
Save user theme preferences to provide a consistent experience across sessions. Combine localStorage with server-side persistence for authenticated users.
Save theme preferences to local storage
// 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])
Save theme preferences for authenticated users
// 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.
Add smooth transitions between themes for a polished user experience. Proper transitions can make theme switching feel seamless and professional.
Apply transitions to theme changes
/* 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');
// });
// `
// }} />