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    if let Some(ref color) = props.color {
165        match color {
166            Color::Dark => {
167                classes.push("bg-dark".to_string());
168                classes.push("[data-bs-theme=dark]".to_string());
169            }
170            Color::Light => {
171                classes.push("bg-light".to_string());
172            }
173            c => {
174                classes.push(format!("bg-{c}"));
175            }
176        }
177    }
178
179    if !props.class.is_empty() {
180        classes.push(props.class.clone());
181    }
182
183    let full_class = classes.join(" ");
184
185    rsx! {
186        nav { class: "{full_class}",
187            ..props.attributes,
188            div { class: "container-fluid",
189                if let Some(brand) = props.brand {
190                    {brand}
191                }
192                {props.children}
193            }
194        }
195    }
196}
197
198/// Navbar toggler button (hamburger menu) for responsive collapse.
199///
200/// # Bootstrap HTML → Dioxus
201///
202/// | HTML | Dioxus |
203/// |---|---|
204/// | `<button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">` | `NavbarToggler { collapsed: signal }` |
205///
206/// ```rust
207/// rsx! {
208///     NavbarToggler { collapsed: collapsed_signal }
209/// }
210/// ```
211#[derive(Clone, PartialEq, Props)]
212pub struct NavbarTogglerProps {
213    /// Signal to toggle — will invert the value on click.
214    pub collapsed: Signal<bool>,
215    /// Additional CSS classes.
216    #[props(default)]
217    pub class: String,
218    /// Any additional HTML attributes.
219    #[props(extends = GlobalAttributes)]
220    attributes: Vec<Attribute>,
221}
222
223#[component]
224pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
225    let is_collapsed = *props.collapsed.read();
226    let mut signal = props.collapsed;
227
228    let full_class = if props.class.is_empty() {
229        "navbar-toggler".to_string()
230    } else {
231        format!("navbar-toggler {}", props.class)
232    };
233
234    rsx! {
235        button {
236            class: "{full_class}",
237            r#type: "button",
238            "aria-expanded": if !is_collapsed { "true" } else { "false" },
239            "aria-label": "Toggle navigation",
240            onclick: move |_| signal.set(!is_collapsed),
241            ..props.attributes,
242            span { class: "navbar-toggler-icon" }
243        }
244    }
245}
246
247/// Navbar collapsible content area.
248///
249/// # Bootstrap HTML → Dioxus
250///
251/// | HTML | Dioxus |
252/// |---|---|
253/// | `<div class="collapse navbar-collapse" id="nav">` | `NavbarCollapse { collapsed: signal, ... }` |
254///
255/// ```rust
256/// let collapsed = use_signal(|| true);
257/// rsx! {
258///     NavbarToggler { collapsed: collapsed }
259///     NavbarCollapse { collapsed: collapsed,
260///         NavItem { NavLink { href: "/", "Home" } }
261///     }
262/// }
263/// ```
264#[derive(Clone, PartialEq, Props)]
265pub struct NavbarCollapseProps {
266    /// Signal controlling collapsed state.
267    pub collapsed: Signal<bool>,
268    /// Additional CSS classes.
269    #[props(default)]
270    pub class: String,
271    /// Any additional HTML attributes.
272    #[props(extends = GlobalAttributes)]
273    attributes: Vec<Attribute>,
274    /// Child elements.
275    pub children: Element,
276}
277
278#[component]
279pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
280    let is_collapsed = *props.collapsed.read();
281    let show = if !is_collapsed { " show" } else { "" };
282
283    let full_class = if props.class.is_empty() {
284        format!("collapse navbar-collapse{show}")
285    } else {
286        format!("collapse navbar-collapse{show} {}", props.class)
287    };
288
289    rsx! {
290        div { class: "{full_class}",
291            ..props.attributes,
292            ul { class: "navbar-nav me-auto mb-2 mb-lg-0",
293                {props.children}
294            }
295        }
296    }
297}
298
299/// Bootstrap NavItem component.
300///
301/// # Bootstrap HTML → Dioxus
302///
303/// | HTML | Dioxus |
304/// |---|---|
305/// | `<li class="nav-item">` | `NavItem { ... }` |
306#[derive(Clone, PartialEq, Props)]
307pub struct NavItemProps {
308    /// Additional CSS classes.
309    #[props(default)]
310    pub class: String,
311    /// Any additional HTML attributes.
312    #[props(extends = GlobalAttributes)]
313    attributes: Vec<Attribute>,
314    /// Child elements (NavLink).
315    pub children: Element,
316}
317
318#[component]
319pub fn NavItem(props: NavItemProps) -> Element {
320    let full_class = if props.class.is_empty() {
321        "nav-item".to_string()
322    } else {
323        format!("nav-item {}", props.class)
324    };
325
326    rsx! {
327        li { class: "{full_class}", ..props.attributes, {props.children} }
328    }
329}
330
331/// Bootstrap NavLink component.
332///
333/// # Bootstrap HTML → Dioxus
334///
335/// | HTML | Dioxus |
336/// |---|---|
337/// | `<a class="nav-link active" href="/">Home</a>` | `NavLink { href: "/", active: true, "Home" }` |
338/// | `<a class="nav-link disabled">Disabled</a>` | `NavLink { disabled: true, "Disabled" }` |
339///
340/// ```rust
341/// rsx! {
342///     NavLink { href: "/dashboard", active: true, "Dashboard" }
343/// }
344/// ```
345#[derive(Clone, PartialEq, Props)]
346pub struct NavLinkProps {
347    /// Link href.
348    #[props(default = "#".to_string())]
349    pub href: String,
350    /// Active state.
351    #[props(default)]
352    pub active: bool,
353    /// Disabled state.
354    #[props(default)]
355    pub disabled: bool,
356    /// Click event handler.
357    #[props(default)]
358    pub onclick: Option<EventHandler<MouseEvent>>,
359    /// Additional CSS classes.
360    #[props(default)]
361    pub class: String,
362    /// Any additional HTML attributes.
363    #[props(extends = GlobalAttributes)]
364    attributes: Vec<Attribute>,
365    /// Child elements.
366    pub children: Element,
367}
368
369#[component]
370pub fn NavLink(props: NavLinkProps) -> Element {
371    let mut classes = vec!["nav-link".to_string()];
372    if props.active {
373        classes.push("active".to_string());
374    }
375    if props.disabled {
376        classes.push("disabled".to_string());
377    }
378    if !props.class.is_empty() {
379        classes.push(props.class.clone());
380    }
381    let full_class = classes.join(" ");
382
383    rsx! {
384        a {
385            class: "{full_class}",
386            href: "{props.href}",
387            "aria-current": if props.active { "page" } else { "" },
388            onclick: move |evt| {
389                if let Some(handler) = &props.onclick {
390                    handler.call(evt);
391                }
392            },
393            ..props.attributes,
394            {props.children}
395        }
396    }
397}