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