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/// This controls the light/dark *mode* only. To customize the actual *colors*
42/// (brand palette and surfaces), use
43/// [`BootstrapThemeProvider`](crate::theme_vars::BootstrapThemeProvider); the
44/// two compose, one for the mode and one for the colors.
45///
46/// # Bootstrap HTML → Dioxus
47///
48/// ```html
49/// <!-- Bootstrap HTML (manual) -->
50/// <html data-bs-theme="dark">
51/// ```
52///
53/// ```rust,no_run
54/// # use dioxus::prelude::*;
55/// # use dioxus_bootstrap_css::prelude::*;
56/// # fn _doctest() -> Element {
57/// // Dioxus — reactive theme switching
58/// let theme = use_signal(|| Theme::Dark);
59/// rsx! {
60///     ThemeProvider { theme }
61///     BootstrapHead {}
62///     ThemeToggle { theme }  // sun/moon toggle button
63///     // your app content
64/// }
65/// # }
66/// ```
67#[derive(Clone, PartialEq, Props)]
68pub struct ThemeProviderProps {
69    /// Signal controlling the current theme.
70    pub theme: Signal<Theme>,
71}
72
73#[component]
74pub fn ThemeProvider(props: ThemeProviderProps) -> Element {
75    let theme_signal = props.theme;
76
77    // Reactively set data-bs-theme on <html> whenever the signal changes.
78    use_effect(move || {
79        let theme = *theme_signal.read();
80        let theme_str = theme.as_str();
81        document::eval(&format!(
82            "document.documentElement.setAttribute('data-bs-theme', '{theme_str}');"
83        ));
84    });
85
86    rsx! {}
87}
88
89/// A toggle button that switches between light and dark mode.
90///
91/// ```rust,no_run
92/// # use dioxus::prelude::*;
93/// # use dioxus_bootstrap_css::prelude::*;
94/// # fn _doctest() -> Element {
95/// let theme = use_signal(|| Theme::Dark);
96/// rsx! {
97///     ThemeProvider { theme: theme }
98///     ThemeToggle { theme: theme }
99/// }
100/// # }
101/// ```
102#[derive(Clone, PartialEq, Props)]
103pub struct ThemeToggleProps {
104    /// Signal controlling the current theme.
105    pub theme: Signal<Theme>,
106    /// Button color.
107    #[props(default)]
108    pub color: Option<Color>,
109    /// Additional CSS classes.
110    #[props(default)]
111    pub class: String,
112}
113
114#[component]
115pub fn ThemeToggle(props: ThemeToggleProps) -> Element {
116    let theme = *props.theme.read();
117    let mut theme_signal = props.theme;
118
119    let icon = match theme {
120        Theme::Light => "moon-stars",
121        Theme::Dark => "sun",
122    };
123
124    let label = match theme {
125        Theme::Light => "Switch to dark mode",
126        Theme::Dark => "Switch to light mode",
127    };
128
129    let btn_class = match &props.color {
130        Some(c) => format!("btn btn-outline-{c}"),
131        None => "btn btn-outline-secondary".to_string(),
132    };
133
134    let full_class = if props.class.is_empty() {
135        btn_class
136    } else {
137        format!("{btn_class} {}", props.class)
138    };
139
140    rsx! {
141        button {
142            class: "{full_class}",
143            r#type: "button",
144            title: "{label}",
145            "aria-label": "{label}",
146            onclick: move |_| {
147                let new_theme = theme.toggle();
148                theme_signal.set(new_theme);
149            },
150            i { class: "bi bi-{icon}" }
151        }
152    }
153}