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&#39;s included in brio&#39;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&#39;s tokens?</summary>
        <p>Set the component&#39;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&#39;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&#39;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 &lt; tokens &lt; animations &lt; base &lt; composition &lt; blocks
            &lt; utilities &lt; overrides</code>) that doesn&#39;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&#39;s semantic tokens in your design system as the source of truth, and your existing components can
            consume them without fighting brio&#39;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&#39;s built on <code>::details-content</code>
            and <code>interpolate-size: allow-keywords</code>.</p>
    </details>
    <details class="details is-warning">
        <summary>Caution: don&#39;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&#39;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

TokenDefaultDescription
--details-bordervar(--border-width-default) solid var(--color-border)Border shorthand for the details container
--details-paddingvar(--ui-pad-y-snug) var(--ui-pad-x-snug)Inner padding for the details container
--details-chevron-size0.5emSize of the chevron indicator
--details-chevron-colorvar(--color-text-muted)Color of the chevron indicator

Classes

ClassDescription
.detailscomponent root
.no-borderRemoves the border entirely
.is-flushNo border with bottom-only rule, for use inside containers
.is-filledSubtle fill when closed, raised surface when open
.is-infoInfo callout color variant
.is-warningWarning callout color variant
.is-dangerDanger callout color variant
.is-successSuccess callout color variant