Skip to main content

dioxus_bootstrap_css/
tabs.rs

1use dioxus::prelude::*;
2
3/// Definition for a single tab.
4///
5/// Used with [`TabList`] to define tab labels, icons, and content.
6///
7/// ```rust,no_run
8/// # use dioxus::prelude::*;
9/// # use dioxus_bootstrap_css::prelude::*;
10/// # fn _doctest() -> dioxus_bootstrap_css::tabs::TabDef {
11/// use dioxus_bootstrap_css::tabs::TabDef;
12///
13/// TabDef {
14///     label: "Home".into(),
15///     icon: Some("house".into()),  // Bootstrap Icon name without "bi-" prefix
16///     content: rsx! { p { "Home content" } },
17/// }
18/// # }
19/// ```
20#[derive(Clone, PartialEq)]
21pub struct TabDef {
22    /// Tab button label.
23    pub label: String,
24    /// Optional Bootstrap icon name (without "bi-" prefix).
25    pub icon: Option<String>,
26    /// Tab content.
27    pub content: Element,
28}
29
30/// Bootstrap Tabs component — signal-driven, no JavaScript.
31///
32/// Produces pixel-perfect Bootstrap 5.3 HTML with separated `<ul class="nav nav-tabs">`
33/// and `<div class="tab-content">` areas. This is the **recommended** component for tabs.
34///
35/// # Bootstrap HTML → Dioxus
36///
37/// ```html
38/// <!-- Bootstrap HTML -->
39/// <ul class="nav nav-tabs" role="tablist">
40///   <li class="nav-item"><button class="nav-link active">Home</button></li>
41///   <li class="nav-item"><button class="nav-link">Profile</button></li>
42/// </ul>
43/// <div class="tab-content border border-top-0 rounded-bottom p-3">
44///   <div class="tab-pane fade show active">Home content</div>
45///   <div class="tab-pane fade">Profile content</div>
46/// </div>
47/// ```
48///
49/// ```rust,no_run
50/// # use dioxus::prelude::*;
51/// # use dioxus_bootstrap_css::prelude::*;
52/// # fn _doctest() -> Element {
53/// use dioxus_bootstrap_css::tabs::TabDef;
54///
55/// let active = use_signal(|| 0usize);
56/// rsx! {
57///     TabList {
58///         active: active,
59///         content_class: "border border-top-0 rounded-bottom p-3",
60///         tabs: vec![
61///             TabDef { label: "Home".into(), icon: Some("house".into()),
62///                 content: rsx! { p { "Home content" } } },
63///             TabDef { label: "Profile".into(), icon: Some("person".into()),
64///                 content: rsx! { p { "Profile content" } } },
65///         ],
66///     }
67/// }
68/// # }
69/// ```
70///
71/// # Props
72///
73/// - `active` — `Signal<usize>` controlling active tab index
74/// - `tabs` — `Vec<TabDef>` defining each tab's label, icon, and content
75/// - `pills` — pill style instead of tabs
76/// - `fill` — fill available width
77/// - `justified` — equal-width items
78/// - `content_class` — additional CSS classes for the tab-content div
79///   (e.g., `"border border-top-0 rounded-bottom p-3"` for standard Bootstrap bordered tabs)
80#[derive(Clone, PartialEq, Props)]
81pub struct TabListProps {
82    /// Signal controlling the active tab index.
83    pub active: Signal<usize>,
84    /// Tab definitions.
85    pub tabs: Vec<TabDef>,
86    /// Use pill style.
87    #[props(default)]
88    pub pills: bool,
89    /// Fill available width.
90    #[props(default)]
91    pub fill: bool,
92    /// Justify items equally.
93    #[props(default)]
94    pub justified: bool,
95    /// Additional CSS classes for the nav.
96    #[props(default)]
97    pub class: String,
98    /// Additional CSS classes for the tab content area.
99    #[props(default)]
100    pub content_class: String,
101    /// Inline `style` for the tab content area — the inline-style sibling of
102    /// `content_class`, for a computed value (a `max-height` that makes the
103    /// panel scroll) that no class can name.
104    #[props(default)]
105    pub content_style: Option<String>,
106}
107
108#[component]
109pub fn TabList(props: TabListProps) -> Element {
110    let current = *props.active.read();
111    let mut active_signal = props.active;
112    let style = if props.pills { "nav-pills" } else { "nav-tabs" };
113
114    let mut nav_classes = vec![format!("nav {style}")];
115    if props.fill {
116        nav_classes.push("nav-fill".to_string());
117    }
118    if props.justified {
119        nav_classes.push("nav-justified".to_string());
120    }
121    if !props.class.is_empty() {
122        nav_classes.push(props.class.clone());
123    }
124    let nav_class = nav_classes.join(" ");
125
126    let content_class = if props.content_class.is_empty() {
127        "tab-content".to_string()
128    } else {
129        format!("tab-content {}", props.content_class)
130    };
131
132    rsx! {
133        ul { class: "{nav_class}", role: "tablist",
134            for (i, tab) in props.tabs.iter().enumerate() {
135                li { class: "nav-item", role: "presentation",
136                    button {
137                        class: if current == i { "nav-link active" } else { "nav-link" },
138                        r#type: "button",
139                        role: "tab",
140                        "aria-selected": if current == i { "true" } else { "false" },
141                        onclick: move |_| active_signal.set(i),
142                        if let Some(ref icon) = tab.icon {
143                            i { class: "bi bi-{icon} me-1" }
144                        }
145                        "{tab.label}"
146                    }
147                }
148            }
149        }
150        div {
151            class: "{content_class}",
152            style: props.content_style.clone(),
153            for (i, tab) in props.tabs.iter().enumerate() {
154                div {
155                    class: if current == i { "tab-pane fade show active" } else { "tab-pane fade" },
156                    role: "tabpanel",
157                    if current == i {
158                        {tab.content.clone()}
159                    }
160                }
161            }
162        }
163    }
164}
165
166/// Alias: `Tabs` works the same as `TabList`.
167///
168/// Both names produce identical output. `TabList` is the canonical name.
169#[component]
170pub fn Tabs(props: TabListProps) -> Element {
171    TabList(props)
172}
173
174/// Alias for TabListProps.
175pub type TabsProps = TabListProps;