# Tutorial 4: Styling & Theming **Video Length**: ~18 minutes | **Difficulty**: Beginner | **Series**: Getting Started ## Overview Learn how to customize the appearance of shadcn-ui components to match your brand. This tutorial covers CSS variables, theme variants, dark mode, and creating custom component styles. ## What You'll Learn - Understanding the CSS variable system - Customizing colors and spacing - Implementing dark mode - Creating custom themes - Using component variants - Responsive design patterns ## Prerequisites - Completed [Tutorial 3: Basic Form Patterns](03-basic-forms.md) - Basic understanding of CSS ## Video Outline **[0:00]** Introduction to theming **[1:45]** CSS variable system overview **[4:00]** Customizing colors **[7:00]** Dark mode implementation **[9:30]** Custom theme creation **[12:00]** Component variants **[14:30]** Responsive design **[16:00]** Best practices and tips **[17:00]** Summary and next steps ## Step-by-Step Guide ### Understanding the CSS Variable System shadcn-ui uses CSS custom properties (variables) for theming. This allows for: 1. **Runtime theme switching** without page reload 2. **Type-safe theming** through CSS variables 3. **Per-component theming** using scoped variables 4. **Dark mode support** through class-based switching The base variable structure: ```css :root { /* Base colors */ --background: 0 0% 100%; /* HSL values */ --foreground: 222.2 84% 4.9%; /* Component colors */ --card: 0 0% 100%; --card-foreground: 222.2 84% 4.9%; --primary: 222.2 47.4% 11.2%; --primary-foreground: 210 40% 98%; /* Semantic colors */ --muted: 210 40% 96.1%; --muted-foreground: 215.4 16.3% 46.9%; --accent: 210 40% 96.1%; --destructive: 0 84.2% 60.2%; /* Borders and inputs */ --border: 214.3 31.8% 91.4%; --input: 214.3 31.8% 91.4%; --ring: 222.2 84% 4.9%; /* Layout */ --radius: 0.5rem; } ``` ### Creating a Custom Theme Create a `brand-theme.css` file: ```css @import "tailwindcss/base"; @import "tailwindcss/components"; @import "tailwindcss/utilities"; @layer base { :root { /* Custom brand colors (purple theme) */ --background: 0 0% 100%; --foreground: 270 15% 10%; --card: 0 0% 100%; --card-foreground: 270 15% 10%; --popover: 0 0% 100%; --popover-foreground: 270 15% 10%; /* Primary - Purple */ --primary: 270 91% 65%; --primary-foreground: 0 0% 100%; /* Secondary - Pink */ --secondary: 330 81% 70%; --secondary-foreground: 0 0% 100%; /* Muted tones */ --muted: 270 20% 96%; --muted-foreground: 270 10% 40%; /* Accent */ --accent: 270 20% 96%; --accent-foreground: 270 15% 10%; /* Destructive */ --destructive: 0 84% 60%; --destructive-foreground: 0 0% 100%; /* Borders */ --border: 270 20% 90%; --input: 270 20% 90%; --ring: 270 91% 65%; /* Radius */ --radius: 0.75rem; } .dark { --background: 270 20% 8%; --foreground: 0 0% 100%; --card: 270 20% 10%; --card-foreground: 0 0% 100%; --popover: 270 20% 10%; --popover-foreground: 0 0% 100%; --primary: 270 91% 65%; --primary-foreground: 270 15% 10%; --secondary: 330 81% 70%; --secondary-foreground: 270 15% 10%; --muted: 270 20% 20%; --muted-foreground: 270 10% 60%; --accent: 270 20% 20%; --accent-foreground: 0 0% 100%; --destructive: 0 62% 30%; --destructive-foreground: 0 0% 100%; --border: 270 20% 20%; --input: 270 20% 20%; --ring: 270 91% 65%; } } @layer base { * { @apply border-border; } body { @apply bg-background text-foreground; font-feature-settings: "rlig" 1, "calt" 1; } } ``` ### Implementing Dark Mode Add dark mode toggle to your app: ```rust use leptos::*; use leptos_shadcn_button::Button; #[component] pub fn ThemeToggle() -> impl IntoView { let (is_dark, set_is_dark) = create_signal(false); // Toggle dark mode class on body let toggle_theme = move |_| { set_is_dark.update(|dark| *dark = !*dark); if is_dark.get() { leptos_dom::document() .body() .class_list() .remove_1("dark") .unwrap(); } else { leptos_dom::document() .body() .class_list() .add_1("dark") .unwrap(); } }; // Check system preference on mount leptos::create_effect(move |_| { let prefers_dark = window() .match_media("(prefers-color-scheme: dark)") .unwrap() .unwrap() .matches(); if prefers_dark { set_is_dark.set(true); leptos_dom::document() .body() .class_list() .add_1("dark") .unwrap(); } }); view! { } } ``` ### Using Component Variants Many components support variant props: ```rust use leptos::*; use leptos_shadcn_button::Button; #[component] pub fn ButtonVariants() -> impl IntoView { view! {
// Default button // Secondary button // Destructive button // Outline button // Ghost button // Link button
// Button sizes
} } ``` ### Creating Custom Card Styles Customize cards with inline styles and variants: ```rust use leptos::*; use leptos_shadcn_card::Card; use leptos_shadcn_button::Button; #[component] pub fn CustomCard() -> impl IntoView { view! { // Gradient card

"Featured"

"This card uses a gradient background with custom styling."

// Glass morphism card

"Glass Effect"

"Frosted glass effect with transparency"

// Bordered card with accent

"Accent Border"

"Card with left accent border"

} } ``` ### Responsive Design Patterns Use Tailwind's responsive utilities: ```rust #[component] pub fn ResponsiveLayout() -> impl IntoView { view! { // Container that adjusts padding
// Grid that adjusts columns
{move || { (0..8).map(|i| { view! {

"Item "{i}

} }).collect_view() }}
// Stack that changes direction
"Sidebar content"
"Main content"
// Text that adjusts size

"Responsive Heading"

} } ``` ### Theme Switcher Component Create a component to switch between multiple themes: ```rust use leptos::*; use leptos_shadcn_button::Button; #[derive(Clone, Copy, PartialEq)] pub enum Theme { Light, Dark, Blue, Purple, Green, } impl Theme { pub fn name(&self) -> &'static str { match self { Theme::Light => "Light", Theme::Dark => "Dark", Theme::Blue => "Ocean", Theme::Purple => "Grape", Theme::Green => "Forest", } } pub fn class(&self) -> &'static str { match self { Theme::Light => "", Theme::Dark => "dark", Theme::Blue => "theme-blue", Theme::Purple => "theme-purple", Theme::Green => "theme-green", } } } #[component] pub fn ThemeSwitcher() -> impl IntoView { let (current_theme, set_current_theme) = create_signal(Theme::Light); let apply_theme = move |theme: Theme| { // Remove all theme classes let body = leptos_dom::document().body(); body.class_list() .remove_1("dark").ok(); body.class_list() .remove_1("theme-blue").ok(); body.class_list() .remove_1("theme-purple").ok(); body.class_list() .remove_1("theme-green").ok(); // Add new theme class if !theme.class().is_empty() { body.class_list().add_1(theme.class()).ok(); } set_current_theme.set(theme); }; view! {

"Theme"

{move || { [Theme::Light, Theme::Dark, Theme::Blue, Theme::Purple, Theme::Green] .into_iter() .map(|theme| { let is_active = current_theme.get() == theme; view! { } }) .collect_view() }}
} } ``` Supporting CSS for custom themes: ```css /* Ocean theme */ .theme-blue { --primary: 210 100% 50%; --primary-foreground: 0 0% 100%; } /* Grape theme */ .theme-purple { --primary: 270 91% 65%; --primary-foreground: 0 0% 100%; } /* Forest theme */ .theme-green { --primary: 140 60% 40%; --primary-foreground: 0 0% 100%; } ``` ### Animated Theme Transitions Add smooth transitions when switching themes: ```css * { transition-property: color, background-color, border-color; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } ``` ## Styling Best Practices ### 1. Use Utility Classes First ```rust // Good
// Avoid
``` ### 2. Create Reusable Component Classes ```rust // Define a common card variant const CARD_VARIANT: &str = "rounded-lg border bg-card text-card-foreground shadow-sm"; view! {
// Content
} ``` ### 3. Use Semantic Color Variables ```rust // Good - uses semantic variable
// Avoid - hardcoded colors
``` ### 4. Maintain Spacing Consistency ```rust // Use consistent spacing scale
// 1rem
// 1.5rem
// 2rem // Avoid arbitrary values
``` ## Complete Example: Themed Dashboard ```rust use leptos::*; use leptos_shadcn_button::Button; use leptos_shadcn_card::Card; #[component] pub fn ThemedDashboard() -> impl IntoView { let (is_dark, set_is_dark) = create_signal(false); view! {
// Header with theme toggle

"Dashboard"

// Main content
// Stats cards
"Total Users"
"2,543"
"+12.5%"
"Revenue"
"$45,231"
"+8.2%"
"Active Sessions"
"1,234"
"-3.1%"
// Content section

"Recent Activity"

{(0..5).map(|i| { view! {
"Activity "{i}
"Description of activity"
"2 hours ago"
} }).collect_view()}
} } ``` ## Exercise 1. Create a custom theme with your brand colors 2. Implement a theme switcher with at least 3 themes 3. Add smooth transitions for theme changes 4. Create a responsive card layout 5. Add hover effects to your components ## What's Next? - [Component Series: Form Components](../components/01-form-components.md) - Deep dive into form components - [Theming Reference](../../components/theming.md) - Complete theming guide - [Dark Mode Guide](../../architecture/dark-mode.md) - Advanced dark mode patterns --- **Previous**: [Basic Form Patterns](03-basic-forms.md) | **Next**: [Form Components](../components/01-form-components.md)