Skip to main content

dioxus_bootstrap_css/
nav.rs

1use dioxus::prelude::*;
2
3use crate::types::{Color, NavbarExpand};
4
5/// Bootstrap Nav component — standalone navigation (not inside a Navbar).
6///
7/// # Bootstrap HTML → Dioxus
8///
9/// ```html
10/// <!-- Bootstrap HTML -->
11/// <ul class="nav nav-pills nav-fill">
12///   <li class="nav-item"><a class="nav-link active" href="#">Home</a></li>
13///   <li class="nav-item"><a class="nav-link" href="#">Profile</a></li>
14///   <li class="nav-item"><a class="nav-link disabled">Disabled</a></li>
15/// </ul>
16/// ```
17///
18/// ```rust,no_run
19/// rsx! {
20///     Nav { pills: true, fill: true,
21///         NavItem { NavLink { active: true, "Home" } }
22///         NavItem { NavLink { "Profile" } }
23///         NavItem { NavLink { disabled: true, "Disabled" } }
24///     }
25///     // Tabs style
26///     Nav { tabs: true, /* ... */ }
27///     // Underline style
28///     Nav { underline: true, /* ... */ }
29///     // Vertical with pills
30///     Nav { pills: true, vertical: true, /* ... */ }
31/// }
32/// ```
33///
34/// # Props
35///
36/// - `pills` — pill style
37/// - `tabs` — tab style
38/// - `underline` — underline style
39/// - `fill` — fill available width
40/// - `justified` — equal-width items
41/// - `vertical` — vertical layout
42#[derive(Clone, PartialEq, Props)]
43pub struct NavProps {
44    /// Use pill style.
45    #[props(default)]
46    pub pills: bool,
47    /// Use tab style.
48    #[props(default)]
49    pub tabs: bool,
50    /// Use underline style.
51    #[props(default)]
52    pub underline: bool,
53    /// Fill available width equally.
54    #[props(default)]
55    pub fill: bool,
56    /// Justify items to fill width (equal-width items).
57    #[props(default)]
58    pub justified: bool,
59    /// Vertical layout.
60    #[props(default)]
61    pub vertical: bool,
62    /// Additional CSS classes.
63    #[props(default)]
64    pub class: String,
65    /// Any additional HTML attributes.
66    #[props(extends = GlobalAttributes)]
67    attributes: Vec<Attribute>,
68    /// Child elements (NavItems).
69    pub children: Element,
70}
71
72#[component]
73pub fn Nav(props: NavProps) -> Element {
74    let mut classes = vec!["nav".to_string()];
75    if props.pills {
76        classes.push("nav-pills".to_string());
77    }
78    if props.tabs {
79        classes.push("nav-tabs".to_string());
80    }
81    if props.underline {
82        classes.push("nav-underline".to_string());
83    }
84    if props.fill {
85        classes.push("nav-fill".to_string());
86    }
87    if props.justified {
88        classes.push("nav-justified".to_string());
89    }
90    if props.vertical {
91        classes.push("flex-column".to_string());
92    }
93    if !props.class.is_empty() {
94        classes.push(props.class.clone());
95    }
96    let full_class = classes.join(" ");
97
98    rsx! {
99        ul { class: "{full_class}",
100            ..props.attributes,
101            {props.children}
102        }
103    }
104}
105
106/// Bootstrap Navbar component.
107///
108/// # Bootstrap HTML → Dioxus
109///
110/// ```html
111/// <!-- Bootstrap HTML -->
112/// <nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
113///   <div class="container-fluid">
114///     <a class="navbar-brand" href="#">MyApp</a>
115///     <button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">
116///       <span class="navbar-toggler-icon"></span>
117///     </button>
118///     <div class="collapse navbar-collapse" id="nav">
119///       <ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/">Home</a></li></ul>
120///     </div>
121///   </div>
122/// </nav>
123/// ```
124///
125/// ```rust,no_run
126/// // Dioxus equivalent
127/// let collapsed = use_signal(|| true);
128/// rsx! {
129///     Navbar { expand: NavbarExpand::Lg, class: "bg-body sticky-top",
130///         brand: rsx! { a { class: "navbar-brand", href: "#", "MyApp" } },
131///         NavbarToggler { collapsed: collapsed }
132///         NavbarCollapse { collapsed: collapsed,
133///             NavItem { NavLink { href: "/", active: true, "Home" } }
134///             NavItem { NavLink { href: "/about", "About" } }
135///         }
136///     }
137/// }
138/// ```
139#[derive(Clone, PartialEq, Props)]
140pub struct NavbarProps {
141    /// Navbar color scheme.
142    #[props(default)]
143    pub color: Option<Color>,
144    /// Responsive expand breakpoint.
145    #[props(default)]
146    pub expand: NavbarExpand,
147    /// Brand element (logo, app name).
148    #[props(default)]
149    pub brand: Option<Element>,
150    /// Additional CSS classes.
151    #[props(default)]
152    pub class: String,
153    /// Any additional HTML attributes.
154    #[props(extends = GlobalAttributes)]
155    attributes: Vec<Attribute>,
156    /// Child elements (nav items, collapse, etc.).
157    pub children: Element,
158}
159
160#[component]
161pub fn Navbar(props: NavbarProps) -> Element {
162    let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
163
164    let is_dark = matches!(props.color.as_ref(), Some(Color::Dark));
165
166    if let Some(ref color) = props.color {
167        match color {
168            Color::Dark => {
169                classes.push("bg-dark".to_string());
170            }
171            Color::Light => {
172                classes.push("bg-light".to_string());
173            }
174            c => {
175                classes.push(format!("bg-{c}"));
176            }
177        }
178    }
179
180    if !props.class.is_empty() {
181        classes.push(props.class.clone());
182    }
183
184    let full_class = classes.join(" ");
185
186    rsx! {
187        nav {
188            class: "{full_class}",
189            // Bootstrap 5.3: dark theme via HTML attribute, not CSS class
190            "data-bs-theme": if is_dark { "dark" } else { "" },
191            ..props.attributes,
192            div { class: "container-fluid",
193                if let Some(brand) = props.brand {
194                    {brand}
195                }
196                {props.children}
197            }
198        }
199    }
200}
201
202/// Navbar toggler button (hamburger menu) for responsive collapse.
203///
204/// # Bootstrap HTML → Dioxus
205///
206/// | HTML | Dioxus |
207/// |---|---|
208/// | `<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">` | `NavbarToggler { collapsed: signal }` |
209///
210/// ```rust
211/// rsx! {
212///     NavbarToggler { collapsed: collapsed_signal }
213/// }
214/// ```
215#[derive(Clone, PartialEq, Props)]
216pub struct NavbarTogglerProps {
217    /// Signal to toggle — will invert the value on click.
218    pub collapsed: Signal<bool>,
219    /// Additional CSS classes.
220    #[props(default)]
221    pub class: String,
222    /// Any additional HTML attributes.
223    #[props(extends = GlobalAttributes)]
224    attributes: Vec<Attribute>,
225}
226
227#[component]
228pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
229    let is_collapsed = *props.collapsed.read();
230    let mut signal = props.collapsed;
231
232    let full_class = if props.class.is_empty() {
233        "navbar-toggler".to_string()
234    } else {
235        format!("navbar-toggler {}", props.class)
236    };
237
238    rsx! {
239        button {
240            class: "{full_class}",
241            r#type: "button",
242            "aria-expanded": if !is_collapsed { "true" } else { "false" },
243            "aria-label": "Toggle navigation",
244            onclick: move |_| signal.set(!is_collapsed),
245            ..props.attributes,
246            span { class: "navbar-toggler-icon" }
247        }
248    }
249}
250
251/// Navbar collapsible content area.
252///
253/// # Bootstrap HTML → Dioxus
254///
255/// | HTML | Dioxus |
256/// |---|---|
257/// | `<div class="collapse navbar-collapse" id="nav">` | `NavbarCollapse { collapsed: signal, ... }` |
258///
259/// ```rust
260/// let collapsed = use_signal(|| true);
261/// rsx! {
262///     NavbarToggler { collapsed: collapsed }
263///     NavbarCollapse { collapsed: collapsed,
264///         NavItem { NavLink { href: "/", "Home" } }
265///     }
266/// }
267/// ```
268#[derive(Clone, PartialEq, Props)]
269pub struct NavbarCollapseProps {
270    /// Signal controlling collapsed state.
271    pub collapsed: Signal<bool>,
272    /// Additional CSS classes.
273    #[props(default)]
274    pub class: String,
275    /// Any additional HTML attributes.
276    #[props(extends = GlobalAttributes)]
277    attributes: Vec<Attribute>,
278    /// Child elements.
279    pub children: Element,
280}
281
282#[component]
283pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
284    let is_collapsed = *props.collapsed.read();
285    let show = if !is_collapsed { " show" } else { "" };
286
287    let full_class = if props.class.is_empty() {
288        format!("collapse navbar-collapse{show}")
289    } else {
290        format!("collapse navbar-collapse{show} {}", props.class)
291    };
292
293    rsx! {
294        div { class: "{full_class}",
295            ..props.attributes,
296            {props.children}
297        }
298    }
299}
300
301/// Bootstrap NavItem component.
302///
303/// # Bootstrap HTML → Dioxus
304///
305/// | HTML | Dioxus |
306/// |---|---|
307/// | `<li class="nav-item">` | `NavItem { ... }` |
308#[derive(Clone, PartialEq, Props)]
309pub struct NavItemProps {
310    /// Additional CSS classes.
311    #[props(default)]
312    pub class: String,
313    /// Any additional HTML attributes.
314    #[props(extends = GlobalAttributes)]
315    attributes: Vec<Attribute>,
316    /// Child elements (NavLink).
317    pub children: Element,
318}
319
320#[component]
321pub fn NavItem(props: NavItemProps) -> Element {
322    let full_class = if props.class.is_empty() {
323        "nav-item".to_string()
324    } else {
325        format!("nav-item {}", props.class)
326    };
327
328    rsx! {
329        li { class: "{full_class}", ..props.attributes, {props.children} }
330    }
331}
332
333/// Bootstrap NavLink component.
334///
335/// # Bootstrap HTML → Dioxus
336///
337/// | HTML | Dioxus |
338/// |---|---|
339/// | `<a class="nav-link active" href="/">Home</a>` | `NavLink { href: "/", active: true, "Home" }` |
340/// | `<a class="nav-link disabled">Disabled</a>` | `NavLink { disabled: true, "Disabled" }` |
341///
342/// ```rust
343/// rsx! {
344///     NavLink { href: "/dashboard", active: true, "Dashboard" }
345/// }
346/// ```
347#[derive(Clone, PartialEq, Props)]
348pub struct NavLinkProps {
349    /// Link href.
350    #[props(default = "#".to_string())]
351    pub href: String,
352    /// Active state.
353    #[props(default)]
354    pub active: bool,
355    /// Disabled state.
356    #[props(default)]
357    pub disabled: bool,
358    /// Click event handler.
359    #[props(default)]
360    pub onclick: Option<EventHandler<MouseEvent>>,
361    /// Additional CSS classes.
362    #[props(default)]
363    pub class: String,
364    /// Any additional HTML attributes.
365    #[props(extends = GlobalAttributes)]
366    attributes: Vec<Attribute>,
367    /// Child elements.
368    pub children: Element,
369}
370
371#[component]
372pub fn NavLink(props: NavLinkProps) -> Element {
373    let mut classes = vec!["nav-link".to_string()];
374    if props.active {
375        classes.push("active".to_string());
376    }
377    if props.disabled {
378        classes.push("disabled".to_string());
379    }
380    if !props.class.is_empty() {
381        classes.push(props.class.clone());
382    }
383    let full_class = classes.join(" ");
384
385    rsx! {
386        a {
387            class: "{full_class}",
388            href: "{props.href}",
389            "aria-current": if props.active { "page" } else { "" },
390            onclick: move |evt| {
391                if let Some(handler) = &props.onclick {
392                    handler.call(evt);
393                }
394            },
395            ..props.attributes,
396            {props.children}
397        }
398    }
399}