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