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