Content
Disclosure
Show and hide content under a row whose chevron turns, like SwiftUI's DisclosureGroup.
import {
DisclosureGroup,
DisclosureHeader,
DisclosureItem,
DisclosurePanel,
DisclosureTrigger,
} from "macbetter/disclosure"
const topics = [
{
value: "backup",
title: "iCloud Backup",
body: "Backs up your photos, messages and app data every night while your device is locked, charging and on Wi-Fi.",
},
{
value: "keychain",
title: "Passwords and Keychain",
body: "Keeps passwords, passkeys and Wi-Fi networks up to date on every device signed in to your account.",
},
{
value: "private-relay",
title: "Private Relay",
body: "Hides your IP address and browsing activity in Safari from network providers and websites.",
},
]
export default function Example() {
return (
// A grouped box sits on the grouped background, the way iOS Settings draws it.
<div style={{ width: "100%", maxWidth: 512, padding: 16, borderRadius: 16, background: "var(--mb-bg-grouped)" }}>
<DisclosureGroup defaultValue={["backup"]}>
{topics.map((topic) => (
<DisclosureItem key={topic.value} value={topic.value}>
<DisclosureHeader>
<DisclosureTrigger>{topic.title}</DisclosureTrigger>
</DisclosureHeader>
<DisclosurePanel>{topic.body}</DisclosurePanel>
</DisclosureItem>
))}
</DisclosureGroup>
</div>
)
}When to use it
Use a disclosure to keep secondary content out of the way until someone asks for it: advanced settings, details behind a summary, answers under their questions. The row always shows; its chevron points at the content when it's closed and turns down when it's open.
- Write the row's label as the name of what's inside, like "Advanced", not as an instruction.
- Don't hide something people need to finish the task.
- Use an Outline for a hierarchy people browse, and a Sheet for a task.
Usage
import {
Disclosure,
DisclosureButton,
DisclosureContent,
DisclosureGroup,
DisclosureHeader,
DisclosureItem,
DisclosurePanel,
DisclosureTrigger,
} from "macbetter/disclosure"A group
DisclosureGroup holds several disclosures, from ui-sdk's Accordion. One opens at a time unless you pass multiple. Each trigger sits in a DisclosureHeader, an h3 by default, so screen reader users can move between them by heading. The trigger draws its chevron for you.
One on its own
Disclosure, from ui-sdk's Collapsible, is a single row and its content, like "Advanced" under a form.
import { Disclosure, DisclosureButton, DisclosureContent } from "macbetter/disclosure"
import { Switch } from "macbetter/switch"
export default function Example() {
return (
<Disclosure style={{ width: "100%", maxWidth: 420 }}>
<DisclosureButton>Advanced</DisclosureButton>
<DisclosureContent>
<label style={{ display: "flex", alignItems: "center", justifyContent: "space-between", gap: 12 }}>
Use hardware acceleration
<Switch defaultChecked />
</label>
</DisclosureContent>
</Disclosure>
)
}Styles
variant="grouped" draws the rows in an inset grouped box, like a Settings list, and is the default for a group. On iOS the box is white in light appearance, so put it on --mb-bg-grouped as Settings does, or it disappears into a white page. plain draws bare rows, and is the default for a single disclosure.
Platform behavior
- macOS. The chevron is small, secondary and leads the label, as in a SwiftUI form on the Mac. Content lines up with the label, past the chevron. Rows are 28 points tall, or 36 in a grouped box.
- iOS and iPadOS. The chevron trails the label in the accent color, as SwiftUI draws it in an iOS list, and rows are 44 points tall (48 in a grouped box). A pressed row fills.
- In right-to-left layouts the chevron mirrors: closed, it points left, and it turns down when open.
- On every platform the chevron turns 90 degrees on the snappy spring and the content's height animates open on the smooth spring. Closing is quicker than opening. Reduced motion skips the height animation.
Accessibility
- Triggers are buttons with
aria-expandedandaria-controls; Enter and Space open and close them. Tab moves between triggers. - Pick the heading level that fits your page with
renderonDisclosureHeader, likerender={<h2 />}. - The chevron is
aria-hidden; the label names the button. - Closed content leaves the page, so find in page can't reach it. Pass
hiddenUntilFoundto keep it findable.
API
DisclosureGroup
Takes every prop of ui-sdk's Accordion, including value, defaultValue, onValueChange, multiple and hiddenUntilFound, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "grouped" | "plain" | "grouped" | How the rows are drawn. |
DisclosureItem takes a value and disabled. DisclosureHeader, DisclosureTrigger and DisclosurePanel wrap ui-sdk's AccordionHeader, AccordionTrigger and AccordionPanel.
Disclosure
Takes every prop of ui-sdk's Collapsible, including open, defaultOpen and onOpenChange, plus:
| Prop | Type | Default | Description |
|---|---|---|---|
variant | "grouped" | "plain" | "plain" | How the row is drawn. |
DisclosureButton and DisclosureContent wrap ui-sdk's CollapsibleTrigger and CollapsiblePanel.
Data attributes and CSS variables
| Attribute or variable | On | Meaning |
|---|---|---|
data-variant | .mb-disclosure-group, .mb-disclosure | grouped or plain. |
data-open, data-closed | .mb-disclosure-trigger, .mb-disclosure-panel | Whether the content is showing. |
--mb-disclosure-chevron-order | platform | 0 puts the chevron before the label, 1 after it. |
--mb-disclosure-chevron-color | platform | The chevron's color. |
--mb-disclosure-row-height, --mb-disclosure-row-height-grouped | platform | Row heights. |