Files
Ubuntu feaf08246f drover: task-1767765235390919619
Task: Add video tutorials
2026-01-10 08:37:58 +00:00

17 KiB

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

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:

: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:

@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:

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! {
        <Button
            variant="ghost"
            size="icon"
            on_click=toggle_theme
            aria_label="Toggle theme"
        >
            {move || {
                if is_dark.get() {
                    "🌙" // Moon icon
                } else {
                    "☀️" // Sun icon
                }
            }}
        </Button>
    }
}

Using Component Variants

Many components support variant props:

use leptos::*;
use leptos_shadcn_button::Button;

#[component]
pub fn ButtonVariants() -> impl IntoView {
    view! {
        <div class="flex flex-wrap gap-4">
            // Default button
            <Button variant="default">
                "Default"
            </Button>

            // Secondary button
            <Button variant="secondary">
                "Secondary"
            </Button>

            // Destructive button
            <Button variant="destructive">
                "Delete"
            </Button>

            // Outline button
            <Button variant="outline">
                "Outline"
            </Button>

            // Ghost button
            <Button variant="ghost">
                "Ghost"
            </Button>

            // Link button
            <Button variant="link">
                "Learn More"
            </Button>
        </div>

        // Button sizes
        <div class="flex flex-wrap gap-4 items-center">
            <Button size="sm">"Small"</Button>
            <Button size="default">"Default"</Button>
            <Button size="lg">"Large"</Button>
            <Button size="icon">"🔍"</Button>
        </div>
    }
}

Creating Custom Card Styles

Customize cards with inline styles and variants:

use leptos::*;
use leptos_shadcn_card::Card;
use leptos_shadcn_button::Button;

#[component]
pub fn CustomCard() -> impl IntoView {
    view! {
        // Gradient card
        <Card class="relative overflow-hidden bg-gradient-to-br from-purple-500 to-pink-500 text-white border-0">
            <div class="relative z-10 p-6">
                <h3 class="text-2xl font-bold mb-2">"Featured"</h3>
                <p class="opacity-90 mb-4">
                    "This card uses a gradient background with custom styling."
                </p>
                <Button variant="secondary" size="sm">
                    "Learn More"
                </Button>
            </div>
        </Card>

        // Glass morphism card
        <Card class="backdrop-blur-sm bg-white/10 border-white/20 shadow-xl">
            <div class="p-6">
                <h3 class="text-lg font-semibold mb-2">"Glass Effect"</h3>
                <p class="text-sm text-muted-foreground">
                    "Frosted glass effect with transparency"
                </p>
            </div>
        </Card>

        // Bordered card with accent
        <Card class="border-l-4 border-l-purple-500 shadow-md hover:shadow-lg transition-shadow">
            <div class="p-6">
                <h3 class="text-lg font-semibold mb-2">"Accent Border"</h3>
                <p class="text-sm text-muted-foreground">
                    "Card with left accent border"
                </p>
            </div>
        </Card>
    }
}

Responsive Design Patterns

Use Tailwind's responsive utilities:

#[component]
pub fn ResponsiveLayout() -> impl IntoView {
    view! {
        // Container that adjusts padding
        <div class="container mx-auto px-4 sm:px-6 lg:px-8">
            // Grid that adjusts columns
            <div class="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-3 xl:grid-cols-4 gap-4">
                {move || {
                    (0..8).map(|i| {
                        view! {
                            <Card class="p-4">
                                <h3 class="font-semibold">"Item "{i}</h3>
                            </Card>
                        }
                    }).collect_view()
                }}
            </div>

            // Stack that changes direction
            <div class="flex flex-col md:flex-row gap-4 items-start">
                <div class="flex-1">"Sidebar content"</div>
                <div class="flex-[2]">"Main content"</div>
            </div>

            // Text that adjusts size
            <h1 class="text-2xl sm:text-3xl md:text-4xl lg:text-5xl font-bold">
                "Responsive Heading"
            </h1>
        </div>
    }
}

Theme Switcher Component

Create a component to switch between multiple themes:

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! {
        <div class="space-y-4">
            <h3 class="text-lg font-semibold">"Theme"</h3>
            <div class="flex flex-wrap gap-2">
                {move || {
                    [Theme::Light, Theme::Dark, Theme::Blue, Theme::Purple, Theme::Green]
                        .into_iter()
                        .map(|theme| {
                            let is_active = current_theme.get() == theme;
                            view! {
                                <Button
                                    variant=if is_active { "default" } else { "outline" }
                                    size="sm"
                                    on_click=move |_| apply_theme(theme)
                                >
                                    {theme.name()}
                                </Button>
                            }
                        })
                        .collect_view()
                }}
            </div>
        </div>
    }
}

Supporting CSS for custom themes:

/* 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:

* {
    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

// Good
<div class="flex items-center gap-4 p-4 bg-white rounded-lg shadow">

// Avoid
<div style="display: flex; align-items: center; padding: 1rem;">

2. Create Reusable Component Classes

// Define a common card variant
const CARD_VARIANT: &str = "rounded-lg border bg-card text-card-foreground shadow-sm";

view! {
    <div class={CARD_VARIANT}>
        // Content
    </div>
}

3. Use Semantic Color Variables

// Good - uses semantic variable
<div class="bg-primary text-primary-foreground">

// Avoid - hardcoded colors
<div class="bg-blue-500 text-white">

4. Maintain Spacing Consistency

// Use consistent spacing scale
<div class="p-4">    // 1rem
<div class="p-6">    // 1.5rem
<div class="p-8">    // 2rem

// Avoid arbitrary values
<div class="p-[13px]">

Complete Example: Themed Dashboard

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! {
        <div class="min-h-screen bg-background text-foreground transition-colors">
            // Header with theme toggle
            <header class="border-b bg-card">
                <div class="container mx-auto px-4 py-4 flex justify-between items-center">
                    <h1 class="text-2xl font-bold">"Dashboard"</h1>
                    <ThemeToggle/>
                </div>
            </header>

            // Main content
            <main class="container mx-auto px-4 py-8">
                <div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
                    // Stats cards
                    <Card class="p-6 bg-gradient-to-br from-primary/10 to-primary/5">
                        <div class="text-sm text-muted-foreground">"Total Users"</div>
                        <div class="text-3xl font-bold mt-2">"2,543"</div>
                        <div class="text-sm text-green-600 mt-2">"+12.5%"</div>
                    </Card>

                    <Card class="p-6 bg-gradient-to-br from-blue-500/10 to-blue-500/5">
                        <div class="text-sm text-muted-foreground">"Revenue"</div>
                        <div class="text-3xl font-bold mt-2">"$45,231"</div>
                        <div class="text-sm text-green-600 mt-2">"+8.2%"</div>
                    </Card>

                    <Card class="p-6 bg-gradient-to-br from-purple-500/10 to-purple-500/5">
                        <div class="text-sm text-muted-foreground">"Active Sessions"</div>
                        <div class="text-3xl font-bold mt-2">"1,234"</div>
                        <div class="text-sm text-red-600 mt-2">"-3.1%"</div>
                    </Card>
                </div>

                // Content section
                <Card class="mt-6 p-6">
                    <h2 class="text-xl font-semibold mb-4">"Recent Activity"</h2>
                    <div class="space-y-4">
                        {(0..5).map(|i| {
                            view! {
                                <div class="flex items-center justify-between py-3 border-b last:border-0">
                                    <div>
                                        <div class="font-medium">"Activity "{i}</div>
                                        <div class="text-sm text-muted-foreground">
                                            "Description of activity"
                                        </div>
                                    </div>
                                    <div class="text-sm text-muted-foreground">
                                        "2 hours ago"
                                    </div>
                                </div>
                            }
                        }).collect_view()}
                    </div>
                </Card>
            </main>
        </div>
    }
}

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?


Previous: Basic Form Patterns | Next: Form Components