Skip to main content

dioxus_bootstrap_css/
tabs.rs

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