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#[derive(Clone, PartialEq)]
7pub struct TabDef {
8    /// Tab button label.
9    pub label: String,
10    /// Optional Bootstrap icon name (without "bi-" prefix).
11    pub icon: Option<String>,
12    /// Tab content.
13    pub content: Element,
14}
15
16/// Bootstrap Tabs component — signal-driven, no JavaScript.
17///
18/// ```rust
19/// let active = use_signal(|| 0usize);
20/// rsx! {
21///     Tabs { active: active,
22///         Tab { label: "Home",
23///             p { "Home content" }
24///         }
25///         Tab { label: "Profile", icon: "person",
26///             p { "Profile content" }
27///         }
28///     }
29/// }
30/// ```
31#[derive(Clone, PartialEq, Props)]
32pub struct TabsProps {
33    /// Signal controlling the active tab index.
34    pub active: Signal<usize>,
35    /// Use pill style instead of tabs.
36    #[props(default)]
37    pub pills: bool,
38    /// Active tab color (for pills).
39    #[props(default)]
40    pub color: Option<Color>,
41    /// Additional CSS classes for the nav container.
42    #[props(default)]
43    pub class: String,
44    /// Child elements (Tab components).
45    pub children: Element,
46}
47
48#[component]
49pub fn Tabs(props: TabsProps) -> Element {
50    let style = if props.pills { "nav-pills" } else { "nav-tabs" };
51    let nav_class = if props.class.is_empty() {
52        format!("nav {style}")
53    } else {
54        format!("nav {style} {}", props.class)
55    };
56
57    rsx! {
58        div {
59            ul { class: "{nav_class}", role: "tablist",
60                {props.children}
61            }
62        }
63    }
64}
65
66/// A single Tab within a Tabs component.
67///
68/// Must be a direct child of Tabs.
69#[derive(Clone, PartialEq, Props)]
70pub struct TabProps {
71    /// Tab button label.
72    pub label: String,
73    /// Optional Bootstrap icon name (without "bi-" prefix).
74    #[props(default)]
75    pub icon: String,
76    /// Tab index (0-based). Set this to match the tab's position.
77    pub index: usize,
78    /// Signal controlling the active tab (shared with parent Tabs).
79    pub active: Signal<usize>,
80    /// Additional CSS classes for the tab pane.
81    #[props(default)]
82    pub class: String,
83    /// Tab content.
84    pub children: Element,
85}
86
87#[component]
88pub fn Tab(props: TabProps) -> Element {
89    let is_active = *props.active.read() == props.index;
90    let mut active_signal = props.active;
91
92    let btn_class = if is_active {
93        "nav-link active"
94    } else {
95        "nav-link"
96    };
97
98    let pane_class = if is_active {
99        "tab-pane fade show active"
100    } else {
101        "tab-pane fade"
102    };
103
104    let pane_class = if props.class.is_empty() {
105        pane_class.to_string()
106    } else {
107        format!("{pane_class} {}", props.class)
108    };
109
110    let index = props.index;
111
112    rsx! {
113        // Tab button
114        li { class: "nav-item", role: "presentation",
115            button {
116                class: "{btn_class}",
117                r#type: "button",
118                role: "tab",
119                "aria-selected": if is_active { "true" } else { "false" },
120                onclick: move |_| active_signal.set(index),
121                if !props.icon.is_empty() {
122                    i { class: "bi bi-{props.icon} me-1" }
123                }
124                "{props.label}"
125            }
126        }
127        // Tab pane (rendered but hidden when not active via CSS classes)
128        div { class: "{pane_class}", role: "tabpanel",
129            {props.children}
130        }
131    }
132}
133
134/// A simpler Tabs API using TabDef structs instead of child components.
135///
136/// ```rust
137/// let active = use_signal(|| 0usize);
138/// rsx! {
139///     TabList {
140///         active: active,
141///         tabs: vec![
142///             TabDef { label: "Home".into(), icon: None, content: rsx! { "Home" } },
143///             TabDef { label: "About".into(), icon: Some("info-circle".into()), content: rsx! { "About" } },
144///         ],
145///     }
146/// }
147/// ```
148#[derive(Clone, PartialEq, Props)]
149pub struct TabListProps {
150    /// Signal controlling the active tab index.
151    pub active: Signal<usize>,
152    /// Tab definitions.
153    pub tabs: Vec<TabDef>,
154    /// Use pill style.
155    #[props(default)]
156    pub pills: bool,
157    /// Additional CSS classes for the nav.
158    #[props(default)]
159    pub class: String,
160    /// Additional CSS classes for the tab content area.
161    #[props(default)]
162    pub content_class: String,
163}
164
165#[component]
166pub fn TabList(props: TabListProps) -> Element {
167    let current = *props.active.read();
168    let mut active_signal = props.active;
169    let style = if props.pills { "nav-pills" } else { "nav-tabs" };
170
171    let nav_class = if props.class.is_empty() {
172        format!("nav {style}")
173    } else {
174        format!("nav {style} {}", props.class)
175    };
176
177    let content_class = if props.content_class.is_empty() {
178        "tab-content".to_string()
179    } else {
180        format!("tab-content {}", props.content_class)
181    };
182
183    rsx! {
184        ul { class: "{nav_class}", role: "tablist",
185            for (i, tab) in props.tabs.iter().enumerate() {
186                li { class: "nav-item", role: "presentation",
187                    button {
188                        class: if current == i { "nav-link active" } else { "nav-link" },
189                        r#type: "button",
190                        role: "tab",
191                        "aria-selected": if current == i { "true" } else { "false" },
192                        onclick: move |_| active_signal.set(i),
193                        if let Some(ref icon) = tab.icon {
194                            i { class: "bi bi-{icon} me-1" }
195                        }
196                        "{tab.label}"
197                    }
198                }
199            }
200        }
201        div { class: "{content_class}",
202            for (i, tab) in props.tabs.iter().enumerate() {
203                div {
204                    class: if current == i { "tab-pane fade show active" } else { "tab-pane fade" },
205                    role: "tabpanel",
206                    if current == i {
207                        {tab.content.clone()}
208                    }
209                }
210            }
211        }
212    }
213}