Details
Expandable disclosure widget
Intent
Details wraps the native <details> element
with brio's chrome and a smooth expand/collapse animation.
Use it for FAQ sections, progressive disclosure of secondary
information, collapsible settings groups, or any "tap to
reveal more" pattern where the content can live inline rather
than in an overlay.
Reach for alert when the message needs to be visible without interaction; reach for dialog when the content must be acknowledged before continuing. Details is for information that's useful to hide by default -- its value is in the folding, not the displaying.
Basic usage
A <details class="details"> with a
<summary> label and body content. The
widget handles keyboard, focus, and the [open]
state natively -- no JavaScript, no custom event wiring.
What's included in brio's core CSS?
Tokens, composition primitives, and components that a typical web-app UI needs on day one: alert, badge, button, card, code, details, dialog, dismiss, divider, drawer, icon, input, list, nav-link, navbar, sidebar, sidebar-nav, spinner, table.
<details class="details">
<summary>What's included in brio's core CSS?</summary>
<p>Tokens, composition primitives, and components that a typical web-app UI needs on day one: alert, badge, button,
card, code, details, dialog, dismiss, divider, drawer, icon, input, list, nav-link, navbar, sidebar,
sidebar-nav, spinner, table.</p>
</details>
The chevron on the right rotates when the widget opens; the
content height animates from 0 to its natural height via the
::details-content pseudo-element. See
Animation for the mechanism and how
to opt out.
Variants
.is-flush
.is-flush removes the border, radius, and inline
padding, keeping only a bottom border. Use for stacked
disclosures inside a parent container that already provides
framing -- an FAQ list inside a card, a settings panel where
each section is a collapsible row.
How do I override a component's tokens?
Set the component's scoped custom property on the element, a parent, or a theme-level selector. Every variant and state remap flows from these tokens, so override once and every derived state follows.
Where do I find the full token list?
The Tokens reference page catalogs every semantic token and its default value. Component-scoped tokens live in each component's reference page under Token overrides.
Can I use brio with a JS framework?
The CSS works anywhere. The small progressive-enhancement JS layer targets authored HTML via queries and attributes; it doesn't conflict with frameworks that render HTML server-side or client-side.
<div>
<details class="details is-flush">
<summary>How do I override a component's tokens?</summary>
<p>Set the component's scoped custom property on the element, a parent, or a theme-level selector. Every variant
and state remap flows from these tokens, so override once and every derived state follows.</p>
</details>
<details class="details is-flush">
<summary>Where do I find the full token list?</summary>
<p>The <a href="/brio/tokens">Tokens</a> reference page catalogs every semantic token and its default value.
Component-scoped tokens live in each component's reference page under Token overrides.</p>
</details>
<details class="details is-flush">
<summary>Can I use brio with a JS framework?</summary>
<p>The CSS works anywhere. The small progressive-enhancement JS layer targets authored HTML via queries and
attributes; it doesn't conflict with frameworks that render HTML server-side or client-side.</p>
</details>
</div>.is-filled
.is-filled uses a surface-sunken background and
a transparent border when closed, then flips to a raised
background and a visible border on open. The closed state
blends into the surrounding surface; the open state lifts,
making the expanded content feel like a peeled-back panel.
Why does brio use cascade layers?
Layers give us a reliable order (reset < tokens < animations < base < composition < blocks
< utilities < overrides) that doesn't depend on import order or source position. A component in
blocks always beats a composition rule with the same specificity; utilities always win over blocks;
overrides always win over everything. Specificity becomes a per-layer concern rather than a global fight.
<details class="details is-filled">
<summary>Why does brio use cascade layers?</summary>
<p>Layers give us a reliable order (<code>reset < tokens < animations < base < composition < blocks
< utilities < overrides</code>) that doesn't depend on import order or source position. A component in
<code>blocks</code> always beats a composition rule with the same specificity; utilities always win over blocks;
overrides always win over everything. Specificity becomes a per-layer concern rather than a global fight.</p>
</details>Callout colors
Status tones remap the border, background, and chevron colors for callouts that double as disclosure widgets -- a warning the reader should glance at before expanding, a success confirmation with optional detail, a danger note with the full explanation folded away.
Info: using brio with an existing design system
Expose brio's semantic tokens in your design system as the source of truth, and your existing components can consume them without fighting brio's cascade.
Ready: the .details animation works natively
No JavaScript is required for the expand/collapse animation -- it's built on ::details-content
and interpolate-size: allow-keywords.
Caution: don't hide critical information
Details is for secondary information the reader might want. Do not fold away error messages, accessibility notices, or content the reader must see to complete a task.
Destructive: uninstalling brio
Removing brio.css from your bundle returns every component to unstyled HTML. Double-check that no consumer code relies on brio's token values before removing.
<div class="stack is-snug">
<details class="details is-info">
<summary>Info: using brio with an existing design system</summary>
<p>Expose brio's semantic tokens in your design system as the source of truth, and your existing components can
consume them without fighting brio's cascade.</p>
</details>
<details class="details is-success">
<summary>Ready: the .details animation works natively</summary>
<p>No JavaScript is required for the expand/collapse animation -- it's built on <code>::details-content</code>
and <code>interpolate-size: allow-keywords</code>.</p>
</details>
<details class="details is-warning">
<summary>Caution: don't hide critical information</summary>
<p>Details is for secondary information the reader might want. Do not fold away error messages, accessibility
notices, or content the reader must see to complete a task.</p>
</details>
<details class="details is-danger">
<summary>Destructive: uninstalling brio</summary>
<p>Removing brio.css from your bundle returns every component to unstyled HTML. Double-check that no consumer
code relies on brio's token values before removing.</p>
</details>
</div>Animation
The expand/collapse animation is applied globally to every
<details> element (not scoped to
.details), so every disclosure widget in a brio
site -- including the sidebar-group chevrons in the docs nav
-- animates consistently.
The mechanism: ::details-content is the native
pseudo-element that wraps everything inside
<details> except <summary>.
brio transitions its block-size from
0 to auto, which is normally
untransitionable; interpolate-size: allow-keywords
is the modern CSS feature that enables the interpolation.
A second transition on content-visibility with
allow-discrete tells the browser to hold the
visible state during the exit animation rather than snapping
content away before height reaches 0.
Graceful degradation. Browsers without
::details-content or
interpolate-size support ignore these rules
entirely; the widget still opens and closes via the native
[open] attribute -- just without the animation.
Fully usable.
Opting out. If a specific details needs instant open/close (screen readers that stumble over the animation, test fixtures that race against it, or a dense settings panel where the animation reads as jitter), add a rule in your consumer CSS:
details.no-animate::details-content {
transition: none;
block-size: auto;
}
.no-animate is not a class brio provides; it's a
pattern consumer stylesheets can implement when the default
animation isn't right for a specific use. Adding the class to
a specific <details> element then bypasses
the animation on that element alone.
Grouping
Multiple details stacked form an FAQ or accordion-style list. Each widget manages its own open state -- opening one doesn't close the others. If you need exclusive open state (only one expanded at a time), reach for the custom-element accordion pattern rather than stacked details.
The docs nav uses the same primitive with different chrome:
each sidebar group is a <details
class="sidebar-group"> with a summary label and a
list of nav links. The animation rule is global, so the
sidebar's expand/collapse matches the rest of the site
without re-declaring anything.
Class reference
.details-- the chrome + animation on a native<details>element. Semantic wrapper -- no child classes; summary and content are styled via descendant selectors..is-flush-- remove outer frame, keep a bottom border. For stacking inside another container..is-filled-- sunken surface when closed, raised surface + border when open..is-info,.is-success,.is-warning,.is-danger-- callout color variants..no-border-- opt out of the default border without switching to a variant.
Customization
Details exposes scoped tokens for the outer frame (border
shorthand, padding shorthand) and the chevron (size, color).
The chevron size uses em so it scales with the
summary's font size. .no-border blanks the border
shorthand; callout variants bypass the token system in a
targeted way -- they set border-color and
background-color directly (rather than remapping
the whole border shorthand) and remap
--details-chevron-color to match the variant's
text color.
Full token list and defaults:
src/css/details.css.
Accessibility
-
Native widget, native keyboard.
<details>handles Enter / Space on the summary to toggle expansion, focus management, and the announced expanded-state role without any brio JS. Don't rebuild this with custom elements -- the native widget is fully accessible out of the box. - Don't hide critical content. Folding information behind a disclosure is a contract: "this is useful but not required." Error messages, required form fields, accessibility notices, and content the reader needs to complete a task should all be visible by default.
- Summary is the label. Screen readers announce the summary text as the widget's name. Keep summaries short and descriptive -- a question or a noun phrase reads clearly, a partial sentence or a redirect ("click here for more") reads poorly.
CSS Reference: Details
Styled native disclosure widget with animated expand
Source: src/css/details.css
Tokens
| Token | Default | Description |
|---|---|---|
--details-border | var(--border-width-default) solid var(--color-border) | Border shorthand for the details container |
--details-padding | var(--ui-pad-y-snug) var(--ui-pad-x-snug) | Inner padding for the details container |
--details-chevron-size | 0.5em | Size of the chevron indicator |
--details-chevron-color | var(--color-text-muted) | Color of the chevron indicator |
Classes
| Class | Description |
|---|---|
.details | component root |
.no-border | Removes the border entirely |
.is-flush | No border with bottom-only rule, for use inside containers |
.is-filled | Subtle fill when closed, raised surface when open |
.is-info | Info callout color variant |
.is-warning | Warning callout color variant |
.is-danger | Danger callout color variant |
.is-success | Success callout color variant |