Skip to main content

dioxus_bootstrap_css/
theme.rs

1use dioxus::prelude::*;
2
3use crate::types::Color;
4
5/// Bootstrap theme mode.
6#[derive(Clone, Copy, Debug, Default, PartialEq)]
7pub enum Theme {
8    #[default]
9    Light,
10    Dark,
11}
12
13impl Theme {
14    /// Toggle between light and dark.
15    pub fn toggle(self) -> Self {
16        match self {
17            Theme::Light => Theme::Dark,
18            Theme::Dark => Theme::Light,
19        }
20    }
21
22    /// CSS value for `data-bs-theme` attribute.
23    pub fn as_str(&self) -> &'static str {
24        match self {
25            Theme::Light => "light",
26            Theme::Dark => "dark",
27        }
28    }
29}
30
31impl std::fmt::Display for Theme {
32    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
33        write!(f, "{}", self.as_str())
34    }
35}
36
37/// Applies `data-bs-theme` to the document root for Bootstrap dark/light mode.
38///
39/// Place this at the top of your app. It reactively sets `data-bs-theme` on `<html>`.
40///
41/// # Bootstrap HTML → Dioxus
42///
43/// ```html
44/// <!-- Bootstrap HTML (manual) -->
45/// <html data-bs-theme="dark">
46/// ```
47///
48/// ```rust,no_run
49/// // Dioxus — reactive theme switching
50/// let theme = use_signal(|| Theme::Dark);
51/// rsx! {
52///     ThemeProvider { theme }
53///     BootstrapHead {}
54///     ThemeToggle { theme }  // sun/moon toggle button
55///     // your app content
56/// }
57/// ```
58#[derive(Clone, PartialEq, Props)]
59pub struct ThemeProviderProps {
60    /// Signal controlling the current theme.
61    pub theme: Signal<Theme>,
62}
63
64#[component]
65pub fn ThemeProvider(props: ThemeProviderProps) -> Element {
66    let theme_signal = props.theme;
67
68    // Reactively set data-bs-theme on <html> whenever the signal changes.
69    use_effect(move || {
70        let theme = *theme_signal.read();
71        let theme_str = theme.as_str();
72        document::eval(&format!(
73            "document.documentElement.setAttribute('data-bs-theme', '{theme_str}');"
74        ));
75    });
76
77    rsx! {}
78}
79
80/// A toggle button that switches between light and dark mode.
81///
82/// ```rust
83/// let theme = use_signal(|| Theme::Dark);
84/// rsx! {
85///     ThemeProvider { theme: theme }
86///     ThemeToggle { theme: theme }
87/// }
88/// ```
89#[derive(Clone, PartialEq, Props)]
90pub struct ThemeToggleProps {
91    /// Signal controlling the current theme.
92    pub theme: Signal<Theme>,
93    /// Button color.
94    #[props(default)]
95    pub color: Option<Color>,
96    /// Additional CSS classes.
97    #[props(default)]
98    pub class: String,
99}
100
101#[component]
102pub fn ThemeToggle(props: ThemeToggleProps) -> Element {
103    let theme = *props.theme.read();
104    let mut theme_signal = props.theme;
105
106    let icon = match theme {
107        Theme::Light => "moon-stars",
108        Theme::Dark => "sun",
109    };
110
111    let label = match theme {
112        Theme::Light => "Switch to dark mode",
113        Theme::Dark => "Switch to light mode",
114    };
115
116    let btn_class = match &props.color {
117        Some(c) => format!("btn btn-outline-{c}"),
118        None => "btn btn-outline-secondary".to_string(),
119    };
120
121    let full_class = if props.class.is_empty() {
122        btn_class
123    } else {
124        format!("{btn_class} {}", props.class)
125    };
126
127    rsx! {
128        button {
129            class: "{full_class}",
130            r#type: "button",
131            title: "{label}",
132            "aria-label": "{label}",
133            onclick: move |_| {
134                let new_theme = theme.toggle();
135                theme_signal.set(new_theme);
136            },
137            i { class: "bi bi-{icon}" }
138        }
139    }
140}