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 element.
38///
39/// Place this component in your app to enable Bootstrap dark/light mode.
40/// It sets the `data-bs-theme` attribute on `<html>` reactively.
41///
42/// ```rust
43/// let theme = use_signal(|| Theme::Dark);
44/// rsx! {
45///     ThemeProvider { theme: theme }
46///     BootstrapHead {}
47///     // your app
48/// }
49/// ```
50#[derive(Clone, PartialEq, Props)]
51pub struct ThemeProviderProps {
52    /// Signal controlling the current theme.
53    pub theme: Signal<Theme>,
54}
55
56#[component]
57pub fn ThemeProvider(props: ThemeProviderProps) -> Element {
58    let theme_signal = props.theme;
59
60    // Reactively set data-bs-theme on <html> whenever the signal changes.
61    use_effect(move || {
62        let theme = *theme_signal.read();
63        let theme_str = theme.as_str();
64        document::eval(&format!(
65            "document.documentElement.setAttribute('data-bs-theme', '{theme_str}');"
66        ));
67    });
68
69    rsx! {}
70}
71
72/// A toggle button that switches between light and dark mode.
73///
74/// ```rust
75/// let theme = use_signal(|| Theme::Dark);
76/// rsx! {
77///     ThemeProvider { theme: theme }
78///     ThemeToggle { theme: theme }
79/// }
80/// ```
81#[derive(Clone, PartialEq, Props)]
82pub struct ThemeToggleProps {
83    /// Signal controlling the current theme.
84    pub theme: Signal<Theme>,
85    /// Button color.
86    #[props(default)]
87    pub color: Option<Color>,
88    /// Additional CSS classes.
89    #[props(default)]
90    pub class: String,
91}
92
93#[component]
94pub fn ThemeToggle(props: ThemeToggleProps) -> Element {
95    let theme = *props.theme.read();
96    let mut theme_signal = props.theme;
97
98    let icon = match theme {
99        Theme::Light => "moon-stars",
100        Theme::Dark => "sun",
101    };
102
103    let label = match theme {
104        Theme::Light => "Switch to dark mode",
105        Theme::Dark => "Switch to light mode",
106    };
107
108    let btn_class = match &props.color {
109        Some(c) => format!("btn btn-outline-{c}"),
110        None => "btn btn-outline-secondary".to_string(),
111    };
112
113    let full_class = if props.class.is_empty() {
114        btn_class
115    } else {
116        format!("{btn_class} {}", props.class)
117    };
118
119    rsx! {
120        button {
121            class: "{full_class}",
122            r#type: "button",
123            title: "{label}",
124            "aria-label": "{label}",
125            onclick: move |_| {
126                let new_theme = theme.toggle();
127                theme_signal.set(new_theme);
128            },
129            i { class: "bi bi-{icon}" }
130        }
131    }
132}