1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
//! Accordion component — multiple collapsibles with optional single-open mode.
use maud::{html, Markup};
/// Individual accordion item
#[derive(Clone, Debug)]
pub struct Item {
/// Unique identifier for aria-controls and content linking
pub id: String,
/// Label text displayed in the trigger button
pub trigger: String,
/// Markup content displayed when expanded
pub content: Markup,
/// Initial open state (default false)
pub open: bool,
}
/// Accordion rendering properties
#[derive(Clone, Debug)]
pub struct Props {
/// Array of accordion items
pub items: Vec<Item>,
/// If true, multiple items can be open; if false, only one item at a time
pub multiple: bool,
/// Optional accessible label for the accordion wrapper (emitted as `aria-label`).
/// shadcn/Radix exposes this on the root — provide a descriptive label when the
/// accordion has no surrounding heading (e.g. "FAQ", "Product specifications").
pub aria_label: Option<String>,
}
impl Default for Props {
fn default() -> Self {
Self {
items: vec![],
multiple: false,
aria_label: None,
}
}
}
/// Render an accordion with the given properties
pub fn render(props: Props) -> Markup {
let multiple_attr = if props.multiple { "true" } else { "false" };
html! {
div class="mui-accordion"
data-mui="accordion"
data-multiple=(multiple_attr)
aria-label=[props.aria_label.as_deref()]
{
@for item in props.items {
div class="mui-accordion__item" {
button type="button"
class="mui-accordion__trigger"
id=(format!("{}-trigger", item.id))
aria-expanded=(if item.open { "true" } else { "false" })
aria-controls=(format!("{}-content", item.id))
{
span class="mui-accordion__label" { (item.trigger) }
span class="mui-accordion__chevron" aria-hidden="true" { "\u{25BE}" }
}
div class="mui-accordion__content"
id=(format!("{}-content", item.id))
aria-labelledby=(format!("{}-trigger", item.id))
role="region"
hidden[!item.open]
{
(item.content)
}
}
}
}
}
}
/// Showcase all accordion use cases
pub fn showcase() -> Markup {
html! {
div.mui-showcase__grid {
// FAQ — single open (shadcn-style)
div {
h3 class="mui-showcase__caption" { "FAQ" }
(render(Props {
items: vec![
Item {
id: "faq-accessible".to_string(),
trigger: "Is it accessible?".to_string(),
content: html! {
p { "Yes. It adheres to the WAI-ARIA design pattern." }
},
open: true,
},
Item {
id: "faq-styled".to_string(),
trigger: "Is it styled?".to_string(),
content: html! {
p { "Yes. It comes with a default theme that matches shadcn." }
},
open: false,
},
Item {
id: "faq-animated".to_string(),
trigger: "Is it animated?".to_string(),
content: html! {
p { "Yes. JavaScript handles expand/collapse with ARIA state." }
},
open: false,
},
],
multiple: false,
aria_label: Some("Frequently asked questions".to_string()),
}))
}
// Multiple — multiple items can be open simultaneously
div {
h3 class="mui-showcase__caption" { "Multiple" }
(render(Props {
items: vec![
Item {
id: "multi-acc-a".to_string(),
trigger: "Section A".to_string(),
content: html! { p { "Content for Section A with relevant information." } },
open: true,
},
Item {
id: "multi-acc-b".to_string(),
trigger: "Section B".to_string(),
content: html! { p { "Content for Section B with additional details." } },
open: false,
},
Item {
id: "multi-acc-c".to_string(),
trigger: "Section C".to_string(),
content: html! { p { "Content for Section C with more information." } },
open: true,
},
],
multiple: true,
aria_label: Some("Expandable sections".to_string()),
}))
}
}
}
}