Skip to main content

dioxus_bootstrap_css/
button.rs

1use dioxus::prelude::*;
2
3use crate::types::{Color, Size};
4
5/// Bootstrap Button component.
6///
7/// Renders a `<button>` by default. When `href` is set, renders an `<a>` element
8/// instead (Bootstrap link-button pattern).
9///
10/// Accepts all standard HTML attributes via `extends = GlobalAttributes`.
11/// This means `title`, `data-bs-toggle`, `aria-label`, `id`, etc. all work.
12///
13/// # Bootstrap HTML → Dioxus
14///
15/// | HTML | Dioxus |
16/// |---|---|
17/// | `<button class="btn btn-primary">` | `Button { color: Color::Primary, "Text" }` |
18/// | `<button class="btn btn-outline-danger btn-sm">` | `Button { color: Color::Danger, outline: true, size: Size::Sm, "Text" }` |
19/// | `<button class="btn btn-success btn-lg" disabled>` | `Button { color: Color::Success, size: Size::Lg, disabled: true, "Text" }` |
20/// | `<a class="btn btn-primary" href="/page">` | `Button { color: Color::Primary, href: "/page", "Link" }` |
21/// | `<button class="btn btn-primary" title="Tip">` | `Button { color: Color::Primary, title: "Tip", "Text" }` |
22///
23/// ```rust,no_run
24/// rsx! {
25///     Button { color: Color::Primary, "Click me" }
26///     Button { color: Color::Danger, outline: true, size: Size::Sm, "Delete" }
27///     Button { color: Color::Success, disabled: true, "Saved" }
28///     Button { color: Color::Warning, onclick: move |_| { /* handler */ }, "Action" }
29///     // Link button — renders <a> instead of <button>:
30///     Button { color: Color::Primary, href: "/page", "Go to Page" }
31///     // HTML attributes work directly:
32///     Button { color: Color::Secondary, title: "Tooltip text", "Hover me" }
33///     Button { color: Color::Primary, "data-bs-toggle": "modal", "Open Modal" }
34/// }
35/// ```
36#[derive(Clone, PartialEq, Props)]
37pub struct ButtonProps {
38    /// Button color variant.
39    #[props(default)]
40    pub color: Color,
41    /// Use outline style instead of filled.
42    #[props(default)]
43    pub outline: bool,
44    /// Button size.
45    #[props(default)]
46    pub size: Size,
47    /// Whether the button is disabled.
48    #[props(default)]
49    pub disabled: bool,
50    /// When set, renders an `<a>` element instead of `<button>` (link-button pattern).
51    #[props(default)]
52    pub href: Option<String>,
53    /// HTML button type attribute (ignored when `href` is set).
54    #[props(default = "button".to_string())]
55    pub r#type: String,
56    /// Click event handler.
57    #[props(default)]
58    pub onclick: Option<EventHandler<MouseEvent>>,
59    /// Active (pressed) state.
60    #[props(default)]
61    pub active: bool,
62    /// Additional CSS classes.
63    #[props(default)]
64    pub class: String,
65    /// Any additional HTML attributes (title, data-bs-toggle, aria-*, id, etc.)
66    #[props(extends = GlobalAttributes)]
67    attributes: Vec<Attribute>,
68    /// Child elements.
69    pub children: Element,
70}
71
72#[component]
73pub fn Button(props: ButtonProps) -> Element {
74    let style = if props.outline { "btn-outline" } else { "btn" };
75    let color = props.color;
76    let color_class = format!("{style}-{color}");
77
78    let size_class = match props.size {
79        Size::Md => String::new(),
80        s => format!(" btn-{s}"),
81    };
82
83    let active_class = if props.active { " active" } else { "" };
84
85    let full_class = if props.class.is_empty() {
86        format!("btn {color_class}{size_class}{active_class}")
87    } else {
88        format!(
89            "btn {color_class}{size_class}{active_class} {}",
90            props.class
91        )
92    };
93
94    if let Some(href) = &props.href {
95        // Link-button: render <a> with role="button"
96        let disabled_class = if props.disabled { " disabled" } else { "" };
97        let link_class = format!("{full_class}{disabled_class}");
98        rsx! {
99            a {
100                class: "{link_class}",
101                href: "{href}",
102                role: "button",
103                onclick: move |evt| {
104                    if let Some(handler) = &props.onclick {
105                        handler.call(evt);
106                    }
107                },
108                ..props.attributes,
109                {props.children}
110            }
111        }
112    } else {
113        rsx! {
114            button {
115                class: "{full_class}",
116                r#type: "{props.r#type}",
117                disabled: props.disabled,
118                onclick: move |evt| {
119                    if let Some(handler) = &props.onclick {
120                        handler.call(evt);
121                    }
122                },
123                ..props.attributes,
124                {props.children}
125            }
126        }
127    }
128}
129
130/// Bootstrap ButtonGroup component.
131///
132/// ```rust,no_run
133/// rsx! {
134///     ButtonGroup {
135///         Button { color: Color::Primary, "Left" }
136///         Button { color: Color::Primary, "Middle" }
137///         Button { color: Color::Primary, "Right" }
138///     }
139/// }
140/// ```
141#[derive(Clone, PartialEq, Props)]
142pub struct ButtonGroupProps {
143    /// Button group size.
144    #[props(default)]
145    pub size: Size,
146    /// Additional CSS classes.
147    #[props(default)]
148    pub class: String,
149    /// Child elements (buttons).
150    pub children: Element,
151}
152
153#[component]
154pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
155    let size_class = match props.size {
156        Size::Md => String::new(),
157        s => format!(" btn-group-{s}"),
158    };
159
160    let full_class = if props.class.is_empty() {
161        format!("btn-group{size_class}")
162    } else {
163        format!("btn-group{size_class} {}", props.class)
164    };
165
166    rsx! {
167        div {
168            class: "{full_class}",
169            role: "group",
170            {props.children}
171        }
172    }
173}
174
175/// Bootstrap ButtonToolbar — groups multiple ButtonGroups.
176///
177/// ```rust,no_run
178/// rsx! {
179///     ButtonToolbar {
180///         ButtonGroup {
181///             Button { color: Color::Primary, "1" }
182///             Button { color: Color::Primary, "2" }
183///         }
184///         ButtonGroup {
185///             Button { color: Color::Secondary, "A" }
186///         }
187///     }
188/// }
189/// ```
190#[derive(Clone, PartialEq, Props)]
191pub struct ButtonToolbarProps {
192    /// Additional CSS classes.
193    #[props(default)]
194    pub class: String,
195    /// Child elements (ButtonGroups).
196    pub children: Element,
197}
198
199#[component]
200pub fn ButtonToolbar(props: ButtonToolbarProps) -> Element {
201    let full_class = if props.class.is_empty() {
202        "btn-toolbar".to_string()
203    } else {
204        format!("btn-toolbar {}", props.class)
205    };
206
207    rsx! {
208        div {
209            class: "{full_class}",
210            role: "toolbar",
211            {props.children}
212        }
213    }
214}