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