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