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!("btn{variant_class}{size_class}{active_class} {}", props.class)
122 };
123
124 if let Some(href) = &props.href {
125 // Link-button: render <a> with role="button"
126 let disabled_class = if props.disabled { " disabled" } else { "" };
127 let link_class = format!("{full_class}{disabled_class}");
128 let target = props.target.clone();
129 let download = props.download.clone();
130 rsx! {
131 a {
132 class: "{link_class}",
133 href: "{href}",
134 role: "button",
135 target: target,
136 download: download,
137 onclick: move |evt| {
138 if let Some(handler) = &props.onclick {
139 handler.call(evt);
140 }
141 },
142 ..props.attributes,
143 {props.children}
144 }
145 }
146 } else {
147 rsx! {
148 button {
149 class: "{full_class}",
150 r#type: "{props.r#type}",
151 disabled: props.disabled,
152 onclick: move |evt| {
153 if let Some(handler) = &props.onclick {
154 handler.call(evt);
155 }
156 },
157 ..props.attributes,
158 {props.children}
159 }
160 }
161 }
162}
163
164/// Bootstrap ButtonGroup component.
165///
166/// ```rust,no_run
167/// # use dioxus::prelude::*;
168/// # use dioxus_bootstrap_css::prelude::*;
169/// # fn _doctest() -> Element {
170/// rsx! {
171/// ButtonGroup {
172/// Button { color: Color::Primary, "Left" }
173/// Button { color: Color::Primary, "Middle" }
174/// Button { color: Color::Primary, "Right" }
175/// }
176/// }
177/// # }
178/// ```
179#[derive(Clone, PartialEq, Props)]
180pub struct ButtonGroupProps {
181 /// Button group size.
182 #[props(default)]
183 pub size: Size,
184 /// Additional CSS classes.
185 #[props(default)]
186 pub class: String,
187 /// Child elements (buttons).
188 pub children: Element,
189}
190
191#[component]
192pub fn ButtonGroup(props: ButtonGroupProps) -> Element {
193 let size_class = match props.size {
194 Size::Md => String::new(),
195 s => format!(" btn-group-{s}"),
196 };
197
198 let full_class = if props.class.is_empty() {
199 format!("btn-group{size_class}")
200 } else {
201 format!("btn-group{size_class} {}", props.class)
202 };
203
204 rsx! {
205 div {
206 class: "{full_class}",
207 role: "group",
208 {props.children}
209 }
210 }
211}
212
213/// Bootstrap ButtonToolbar — groups multiple ButtonGroups.
214///
215/// ```rust,no_run
216/// # use dioxus::prelude::*;
217/// # use dioxus_bootstrap_css::prelude::*;
218/// # fn _doctest() -> Element {
219/// rsx! {
220/// ButtonToolbar {
221/// ButtonGroup {
222/// Button { color: Color::Primary, "1" }
223/// Button { color: Color::Primary, "2" }
224/// }
225/// ButtonGroup {
226/// Button { color: Color::Secondary, "A" }
227/// }
228/// }
229/// }
230/// # }
231/// ```
232#[derive(Clone, PartialEq, Props)]
233pub struct ButtonToolbarProps {
234 /// Additional CSS classes.
235 #[props(default)]
236 pub class: String,
237 /// Child elements (ButtonGroups).
238 pub children: Element,
239}
240
241#[component]
242pub fn ButtonToolbar(props: ButtonToolbarProps) -> Element {
243 let full_class = if props.class.is_empty() {
244 "btn-toolbar".to_string()
245 } else {
246 format!("btn-toolbar {}", props.class)
247 };
248
249 rsx! {
250 div {
251 class: "{full_class}",
252 role: "toolbar",
253 {props.children}
254 }
255 }
256}