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/// ```rust
201/// rsx! {
202///     NavbarToggler { collapsed: collapsed_signal }
203/// }
204/// ```
205#[derive(Clone, PartialEq, Props)]
206pub struct NavbarTogglerProps {
207    /// Signal to toggle — will invert the value on click.
208    pub collapsed: Signal<bool>,
209    /// Additional CSS classes.
210    #[props(default)]
211    pub class: String,
212    /// Any additional HTML attributes.
213    #[props(extends = GlobalAttributes)]
214    attributes: Vec<Attribute>,
215}
216
217#[component]
218pub fn NavbarToggler(props: NavbarTogglerProps) -> Element {
219    let is_collapsed = *props.collapsed.read();
220    let mut signal = props.collapsed;
221
222    let full_class = if props.class.is_empty() {
223        "navbar-toggler".to_string()
224    } else {
225        format!("navbar-toggler {}", props.class)
226    };
227
228    rsx! {
229        button {
230            class: "{full_class}",
231            r#type: "button",
232            "aria-expanded": if !is_collapsed { "true" } else { "false" },
233            "aria-label": "Toggle navigation",
234            onclick: move |_| signal.set(!is_collapsed),
235            ..props.attributes,
236            span { class: "navbar-toggler-icon" }
237        }
238    }
239}
240
241/// Navbar collapsible content area.
242///
243/// ```rust
244/// let collapsed = use_signal(|| true);
245/// rsx! {
246///     NavbarToggler { collapsed: collapsed }
247///     NavbarCollapse { collapsed: collapsed,
248///         NavItem { NavLink { href: "/", "Home" } }
249///     }
250/// }
251/// ```
252#[derive(Clone, PartialEq, Props)]
253pub struct NavbarCollapseProps {
254    /// Signal controlling collapsed state.
255    pub collapsed: Signal<bool>,
256    /// Additional CSS classes.
257    #[props(default)]
258    pub class: String,
259    /// Any additional HTML attributes.
260    #[props(extends = GlobalAttributes)]
261    attributes: Vec<Attribute>,
262    /// Child elements.
263    pub children: Element,
264}
265
266#[component]
267pub fn NavbarCollapse(props: NavbarCollapseProps) -> Element {
268    let is_collapsed = *props.collapsed.read();
269    let show = if !is_collapsed { " show" } else { "" };
270
271    let full_class = if props.class.is_empty() {
272        format!("collapse navbar-collapse{show}")
273    } else {
274        format!("collapse navbar-collapse{show} {}", props.class)
275    };
276
277    rsx! {
278        div { class: "{full_class}",
279            ..props.attributes,
280            ul { class: "navbar-nav me-auto mb-2 mb-lg-0",
281                {props.children}
282            }
283        }
284    }
285}
286
287/// Bootstrap NavItem component.
288#[derive(Clone, PartialEq, Props)]
289pub struct NavItemProps {
290    /// Additional CSS classes.
291    #[props(default)]
292    pub class: String,
293    /// Any additional HTML attributes.
294    #[props(extends = GlobalAttributes)]
295    attributes: Vec<Attribute>,
296    /// Child elements (NavLink).
297    pub children: Element,
298}
299
300#[component]
301pub fn NavItem(props: NavItemProps) -> Element {
302    let full_class = if props.class.is_empty() {
303        "nav-item".to_string()
304    } else {
305        format!("nav-item {}", props.class)
306    };
307
308    rsx! {
309        li { class: "{full_class}", ..props.attributes, {props.children} }
310    }
311}
312
313/// Bootstrap NavLink component.
314///
315/// ```rust
316/// rsx! {
317///     NavLink { href: "/dashboard", active: true, "Dashboard" }
318/// }
319/// ```
320#[derive(Clone, PartialEq, Props)]
321pub struct NavLinkProps {
322    /// Link href.
323    #[props(default = "#".to_string())]
324    pub href: String,
325    /// Active state.
326    #[props(default)]
327    pub active: bool,
328    /// Disabled state.
329    #[props(default)]
330    pub disabled: bool,
331    /// Click event handler.
332    #[props(default)]
333    pub onclick: Option<EventHandler<MouseEvent>>,
334    /// Additional CSS classes.
335    #[props(default)]
336    pub class: String,
337    /// Any additional HTML attributes.
338    #[props(extends = GlobalAttributes)]
339    attributes: Vec<Attribute>,
340    /// Child elements.
341    pub children: Element,
342}
343
344#[component]
345pub fn NavLink(props: NavLinkProps) -> Element {
346    let mut classes = vec!["nav-link".to_string()];
347    if props.active {
348        classes.push("active".to_string());
349    }
350    if props.disabled {
351        classes.push("disabled".to_string());
352    }
353    if !props.class.is_empty() {
354        classes.push(props.class.clone());
355    }
356    let full_class = classes.join(" ");
357
358    rsx! {
359        a {
360            class: "{full_class}",
361            href: "{props.href}",
362            "aria-current": if props.active { "page" } else { "" },
363            onclick: move |evt| {
364                if let Some(handler) = &props.onclick {
365                    handler.call(evt);
366                }
367            },
368            ..props.attributes,
369            {props.children}
370        }
371    }
372}