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    /// Child elements (NavItems).
66    pub children: Element,
67}
68
69#[component]
70pub fn Nav(props: NavProps) -> Element {
71    let mut classes = vec!["nav".to_string()];
72    if props.pills {
73        classes.push("nav-pills".to_string());
74    }
75    if props.tabs {
76        classes.push("nav-tabs".to_string());
77    }
78    if props.underline {
79        classes.push("nav-underline".to_string());
80    }
81    if props.fill {
82        classes.push("nav-fill".to_string());
83    }
84    if props.justified {
85        classes.push("nav-justified".to_string());
86    }
87    if props.vertical {
88        classes.push("flex-column".to_string());
89    }
90    if !props.class.is_empty() {
91        classes.push(props.class.clone());
92    }
93    let full_class = classes.join(" ");
94
95    rsx! {
96        ul { class: "{full_class}",
97            {props.children}
98        }
99    }
100}
101
102/// Bootstrap Navbar component.
103///
104/// # Bootstrap HTML → Dioxus
105///
106/// ```html
107/// <!-- Bootstrap HTML -->
108/// <nav class="navbar navbar-expand-lg bg-dark" data-bs-theme="dark">
109///   <div class="container-fluid">
110///     <a class="navbar-brand" href="#">MyApp</a>
111///     <button class="navbar-toggler" data-bs-toggle="collapse" data-bs-target="#nav">
112///       <span class="navbar-toggler-icon"></span>
113///     </button>
114///     <div class="collapse navbar-collapse" id="nav">
115///       <ul class="navbar-nav"><li class="nav-item"><a class="nav-link" href="/">Home</a></li></ul>
116///     </div>
117///   </div>
118/// </nav>
119/// ```
120///
121/// ```rust,no_run
122/// // Dioxus equivalent
123/// let collapsed = use_signal(|| true);
124/// rsx! {
125///     Navbar { expand: NavbarExpand::Lg, class: "bg-body sticky-top",
126///         brand: rsx! { a { class: "navbar-brand", href: "#", "MyApp" } },
127///         NavbarToggler { collapsed: collapsed }
128///         NavbarCollapse { collapsed: collapsed,
129///             NavItem { NavLink { href: "/", active: true, "Home" } }
130///             NavItem { NavLink { href: "/about", "About" } }
131///         }
132///     }
133/// }
134/// ```
135#[derive(Clone, PartialEq, Props)]
136pub struct NavbarProps {
137    /// Navbar color scheme.
138    #[props(default)]
139    pub color: Option<Color>,
140    /// Responsive expand breakpoint.
141    #[props(default)]
142    pub expand: NavbarExpand,
143    /// Brand element (logo, app name).
144    #[props(default)]
145    pub brand: Option<Element>,
146    /// Additional CSS classes.
147    #[props(default)]
148    pub class: String,
149    /// Child elements (nav items, collapse, etc.).
150    pub children: Element,
151}
152
153#[component]
154pub fn Navbar(props: NavbarProps) -> Element {
155    let mut classes = vec!["navbar".to_string(), props.expand.to_string()];
156
157    if let Some(ref color) = props.color {
158        match color {
159            Color::Dark => {
160                classes.push("bg-dark".to_string());
161                classes.push("[data-bs-theme=dark]".to_string());
162            }
163            Color::Light => {
164                classes.push("bg-light".to_string());
165            }
166            c => {
167                classes.push(format!("bg-{c}"));
168            }
169        }
170    }
171
172    if !props.class.is_empty() {
173        classes.push(props.class.clone());
174    }
175
176    let full_class = classes.join(" ");
177
178    rsx! {
179        nav { class: "{full_class}",
180            div { class: "container-fluid",
181                if let Some(brand) = props.brand {
182                    {brand}
183                }
184                {props.children}
185            }
186        }
187    }
188}
189
190/// Navbar toggler button (hamburger menu) for responsive collapse.
191///
192/// ```rust
193/// rsx! {
194///     NavbarToggler { collapsed: collapsed_signal }
195/// }
196/// ```
197#[derive(Clone, PartialEq, Props)]
198pub struct NavbarTogglerProps {
199    /// Signal to toggle — will invert the value on click.
200    pub collapsed: Signal<bool>,
201    /// Additional CSS classes.
202    #[props(default)]
203    pub class: String,
204}
205
206#[component]
207pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
208    let is_collapsed = *props.collapsed.read();
209    let mut signal = props.collapsed;
210
211    let full_class = if props.class.is_empty() {
212        "navbar-toggler".to_string()
213    } else {
214        format!("navbar-toggler {}", props.class)
215    };
216
217    rsx! {
218        button {
219            class: "{full_class}",
220            r#type: "button",
221            "aria-expanded": if !is_collapsed { "true" } else { "false" },
222            "aria-label": "Toggle navigation",
223            onclick: move |_| signal.set(!is_collapsed),
224            span { class: "navbar-toggler-icon" }
225        }
226    }
227}
228
229/// Navbar collapsible content area.
230///
231/// ```rust
232/// let collapsed = use_signal(|| true);
233/// rsx! {
234///     NavbarToggler { collapsed: collapsed }
235///     NavbarCollapse { collapsed: collapsed,
236///         NavItem { NavLink { href: "/", "Home" } }
237///     }
238/// }
239/// ```
240#[derive(Clone, PartialEq, Props)]
241pub struct NavbarCollapseProps {
242    /// Signal controlling collapsed state.
243    pub collapsed: Signal<bool>,
244    /// Additional CSS classes.
245    #[props(default)]
246    pub class: String,
247    /// Child elements.
248    pub children: Element,
249}
250
251#[component]
252pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
253    let is_collapsed = *props.collapsed.read();
254    let show = if !is_collapsed { " show" } else { "" };
255
256    let full_class = if props.class.is_empty() {
257        format!("collapse navbar-collapse{show}")
258    } else {
259        format!("collapse navbar-collapse{show} {}", props.class)
260    };
261
262    rsx! {
263        div { class: "{full_class}",
264            ul { class: "navbar-nav me-auto mb-2 mb-lg-0",
265                {props.children}
266            }
267        }
268    }
269}
270
271/// Bootstrap NavItem component.
272#[derive(Clone, PartialEq, Props)]
273pub struct NavItemProps {
274    /// Additional CSS classes.
275    #[props(default)]
276    pub class: String,
277    /// Child elements (NavLink).
278    pub children: Element,
279}
280
281#[component]
282pub fn NavItem(props: NavItemProps) -> Element {
283    let full_class = if props.class.is_empty() {
284        "nav-item".to_string()
285    } else {
286        format!("nav-item {}", props.class)
287    };
288
289    rsx! {
290        li { class: "{full_class}", {props.children} }
291    }
292}
293
294/// Bootstrap NavLink component.
295///
296/// ```rust
297/// rsx! {
298///     NavLink { href: "/dashboard", active: true, "Dashboard" }
299/// }
300/// ```
301#[derive(Clone, PartialEq, Props)]
302pub struct NavLinkProps {
303    /// Link href.
304    #[props(default = "#".to_string())]
305    pub href: String,
306    /// Active state.
307    #[props(default)]
308    pub active: bool,
309    /// Disabled state.
310    #[props(default)]
311    pub disabled: bool,
312    /// Click event handler.
313    #[props(default)]
314    pub onclick: Option<EventHandler<MouseEvent>>,
315    /// Additional CSS classes.
316    #[props(default)]
317    pub class: String,
318    /// Child elements.
319    pub children: Element,
320}
321
322#[component]
323pub fn NavLink(props: NavLinkProps) -> Element {
324    let mut classes = vec!["nav-link".to_string()];
325    if props.active {
326        classes.push("active".to_string());
327    }
328    if props.disabled {
329        classes.push("disabled".to_string());
330    }
331    if !props.class.is_empty() {
332        classes.push(props.class.clone());
333    }
334    let full_class = classes.join(" ");
335
336    rsx! {
337        a {
338            class: "{full_class}",
339            href: "{props.href}",
340            "aria-current": if props.active { "page" } else { "" },
341            onclick: move |evt| {
342                if let Some(handler) = &props.onclick {
343                    handler.call(evt);
344                }
345            },
346            {props.children}
347        }
348    }
349}