Design System

Design System

Disclosure

Surfaces

Open in Storybook: Disclosure (opens in a new tab)

On this page

Overview

A disclosure shows and hides a block in place. It is details with summary: the browser supplies the role, the name, the expanded state, Enter and Space, and keeps closed content out of the Tab order and the accessibility tree. Script and ARIA only duplicate it.

The rule. details with summary as its first child and only control. No role, no aria-expanded, no aria-controls, no tabindex, no wrapper around the content. Focus stays on the summary when it opens. name on every member makes a one-open group; leave it off where people compare. A hidden marker owes a visible, aria-hidden replacement. Motion only under prefers-reduced-motion: no-preference.

Anatomy

  1. Disclosure

    details

    The group. open is the state, and name on every member makes a one-open group.

  2. Summary

    summary

    First child, the only control and its name.

  3. Marker

    ::marker or svg aria-hidden

    The native marker, or a replacement that changes shape and is hidden from assistive technology.

  4. Panel

    ::details-content

    The panel box: its children are direct, with no wrapper.

Usage

Use cases

Seven recipes, each the real component and each working. They are the same two elements: only the name, the layout and the content change.

  • FAQ

    One answer open at a time

    • Can I change my delivery date?

      Yes, until the order is packed. Open the order in your account and choose a new date.

    • How do I return a machine?

      Book a pickup from your account. We collect it within 3 working days, in the original box.

    • Do capsules expire?

      The best-before date is printed on every sleeve. Sealed, they keep for a year.

    details name="faq" · summary with an h3 · a marker

    Opening one closes the others, with no script. Focus stays on the question.

  • Inline “read more”

    A detail folded into the sentence

    Grown at 1,800 m on volcanic soil, this Arabica is picked by hand and washed the same day
    Read more about the farmThe beans dry on raised beds for twelve days and are turned by hand, which the family calls café de altura.

    display: inline on details, summary and [open]::details-content

    The name says what opens. Foreign words in the panel get their own lang.

  • Spoiler · show solution

    An answer behind an explicit reveal

    Puzzle 4: which capsule has intensity 12?

    Show the solution

    Ristretto Intenso, the only sleeve printed with a 12.

    Closed content is out of view and out of the tree

    Nothing leaks to screen readers until it opens.

  • Filter group

    Several groups, open to compare

    • Intensity
    • Cup size
    • Aromatic notes

    details per group, no name · fieldset aria-labelledby the summary

    Several stay open so people can compare.

  • Spec in a card

    The spec sheet behind the price

    Vertuo Pop

    €99.00

    Technical specifications

    Water tank 0.6 l · 1260 W · 2.4 kg

    Cup sizes: 40, 80, 150, 230 ml

    No name · the summary is the only control in the card

    Card and disclosure stay separate: no stretched link over the card.

  • Code or log fold

    A long block folded behind a count

    Changes in this order (2)
    - capsules: 10+ capsules: 12  intensity: 8

    A monospace panel in ::details-content · ins and del

    Diff lines with ins and del; the summary counts the changes.

Props

Props
PropDoesDefault
titleThe name: what opens, the same open and closed. Never “More”Required
headingThe name is a heading, inside the summary, so the outline keeps itfalse
levelThe level of that heading. Left out, it is the one the page set with DisclosureLevel3
nameThe same on every member: one open at a time. Left out, it is the name of the DisclosureList around it—
openStarts expandedfalse
inlineFolds into the sentence: no marker, and the name is underlinedfalse
titleIdThe id of the summary, for a fieldset in the panel that names itself by it—
childrenThe panel: direct children, with no wrapper—
DisclosureListThree or more in a row: a ul role="list" of members, with a name for a group and a level for their headings—

Example

import { Disclosure, DisclosureList } from '@/components/Disclosure'

<DisclosureList name="faq">
  <Disclosure heading title="Can I change my delivery date?">
    <p>Yes, until the order is packed.</p>
  </Disclosure>
  <Disclosure heading title="How do I return a machine?">
    <p>Book a pickup from your account.</p>
  </Disclosure>
</DisclosureList>

Specs

Markup

<details>
  <summary>Shipping and returns</summary>
  <p>Free delivery from €40. Returns within 30 days, in the original box.</p>
  <p>Collection points close at 7 pm.</p>   <!-- direct children, no wrapper -->
</details>

<!-- A heading in summary keeps its level in the tree (DisclosureTriangle > heading 3) -->
<details>
  <summary><h3>Shipping and returns</h3></summary>
  <p>…</p>
</details>

<!-- Three or more in a row: a list, so the count is announced -->
<ul role="list">
  <li><details name="faq"><summary>Can I change my delivery date?</summary><p>…</p></details></li>
  <li><details name="faq"><summary>How do I return a machine?</summary><p>…</p></details></li>
  <li><details name="faq"><summary>Do capsules expire?</summary><p>…</p></details></li>
</ul>

Structure

Structure
RuleDetail
SummaryFirst child, the only control. Text, phrasing content or one h1–h6; the level follows the outline, never the size
Nothing interactive in itA link stays its own focusable link (measured): a second stop inside the one control
No ARIANo role, aria-expanded, aria-controls or tabindex on either element: they duplicate or contradict the browser
openIn the markup: starts expanded. Toggle the attribute; never hide the children yourself
NameWhat opens: “Shipping and returns”, never “More”. The same open and closed: the state is announced
ListsThree or more in a row: ul role="list" > li > details

Groups

The same name on every member: opening one closes the others, and closing the open one leaves none open (measured in Chromium 149 and WebKit 26.4). Exclusive hides one answer to show another, so it is for a FAQ and for the groups of a narrow menu. Leave it off where people compare, such as filters and specs.

Marker

Marker
RuleDetail
Restyle::marker for colour and size, or list-style-type
Hidedisplay: flex on the summary, or list-style: none. ::-webkit-details-marker alone no longer hides it in WebKit 26.4 (measured), so it is not needed
ReplaceA hidden marker owes a visible replacement: nothing else says it expands. aria-hidden="true" and no text; an emoji or a glyph is its own aria-hidden element
AnimateFrom details[open], with transform or rotate. No script
Forced colorsBorders, an SVG in currentColor or the native ::marker stay. A marker drawn with box-shadow or background vanishes (measured)
ContrastThe marker and the ring 3:1 against their background (1.4.11)

Panel and motion

details::details-content {
  block-size: 0;
  padding-block: 0;
  padding-inline: 2.5rem 0.5rem;   /* the same open and closed */
  overflow: clip;
  opacity: 0;
}
details[open]::details-content { block-size: auto; padding-block: 0.85rem; opacity: 1; }
details > summary + * { margin-block-start: 0; }
details > :last-child { margin-block-end: 0; }

@media (prefers-reduced-motion: no-preference) {
  details { interpolate-size: allow-keywords; }
  details::details-content {                  /* closing */
    transition: block-size 0.25s var(--ease), padding-block 0.25s var(--ease),
      opacity 0.12s ease-out, content-visibility 0.25s allow-discrete;
  }
  details[open]::details-content {            /* opening */
    transition: block-size 0.3s var(--ease), padding-block 0.3s var(--ease),
      opacity 0.2s ease-out 0.05s, content-visibility 0.3s allow-discrete;
  }
  summary .marker { transition: rotate 0.25s var(--ease); }
}
details[open] > summary .marker { rotate: 90deg; }
/* --ease: cubic-bezier(0.4, 0, 0.2, 1) */
Panel and motion
RuleDetail
Reset block-size on [open]Without it the closed 0 still applies: the open panel stays clipped (measured: 0px)
Only the block padding changesThe padding along the line is the same open and closed. Dropped when it closes, the text jumps wider at the first frame and may re-wrap while the panel is still collapsing (measured)
ClosingA little shorter than opening, and the text fades out before the box is gone: a box that shrinks over text slices it line by line. The box and the marker share one curve, without the long tail of ease
OpeningThe text fades in as the box has room for it
interpolate-sizeLets block-size animate to auto; without it the height snaps. WebKit 26.4 has ::details-content but not interpolate-size (measured), so it opens without animation, which is fine
Everything in no-preferenceMeasured: the transition is 0s under reduce (2.3.3)
Focus timingWith the content-visibility transition, the content takes focus about 20 ms after opening; instant under reduce
MarginsZero the block margins of the first and last child: no stray gap. ::details-content > :first-child does not match, so they are selected from the details

States

States
StateLook
ClosedThe chevron points right, and a 1px divider is under the row
HoverA row tint, only under @media (hover: hover)
FocusThe site ring on summary:focus-visible, not clipped: no overflow on details
OpenThe chevron points down, and the panel is under the summary
Open and focusFocus stays on the summary
Forced colorsA currentColor marker, the system ring and an underline

Tokens

Component tokens

Component tokens
PartPropertyTokenValue
SummaryMinimum height · padding—44px · 12px 8px
SummaryText colour · weightink#16161a · 600
MarkerSize · strokecurrentColor20px · 2px
HoverFillfill#f3f3f5
DividerColour · widthline#d7d7dc · 1px
PanelText colour · sizeink-soft#4b4b55 · 15px
PanelPadding—0.85rem · its text lines up with the name
RingColour · widthfocus-ring-primary#0066ff · 4px
MotionOpening · closing · marker—0.3s · 0.25s · 0.25s

Guidelines

Disclosure or something else?

Disclosure or something else?
NeedUse
Show or hide a block in placedetails
One answer open at a time (FAQ)details with the same name
Groups people compare or combine (filters, specs)details, no name: several stay open
A control whose only job is choosing which content showsOne details per option, never radios
A trigger apart from its content (a nav item, a table row)button with aria-expanded and aria-controls: details needs its panel right after summary
Commands, a dialog, a tooltipNot a disclosure: a menu, a dialog, a tooltip

Controls by scenario and context

Each row is a use case. A disclosure keeps its structure at every size: only spacing and wrapping change.

Controls by scenario and context
ScenarioDesktop · 640 px and upSmaller viewport · 400–639 pxZoom 400% · 320 CSS px
FAQList of details, name="faq"; summary with h3 and markerSame; summary full width, 44px tallQuestions wrap; the marker stays at the start; no clipping
Read moreInline summary at the end of the paragraphSame; wraps with the textSame; the open panel continues inline
Filter groupsSide column of details, all open by defaultAbove the results, closed by defaultClosed by default; one column; checkboxes 24px
Spec in a cardUnder the price, closedSameCard full width; the summary wraps
Code foldThe panel scrolls inside if lines are longSameOnly the pre scrolls sideways (the 1.4.10 exception for code)
Sticky headerscroll-margin on the summary = the header heightSameSticky header off at 400%; scroll-margin stays

Accessibility

Keyboard

Keyboard
KeyDoes
Tab · Shift+TabThe summary is the stop. Open content’s controls follow in DOM order; closed content is skipped (measured)
Enter · SpaceToggles. Focus stays on the summary, open or closed (measured)
ArrowsNothing: not tabs, not a menu
A link to an id insideOpens the panel (measured in Chromium)

Focus

Focus
MomentWhere focus goes
Open or closeStays on the summary. The person chose to expand, not to enter
A sticky header abovescroll-margin-block-start on the summary equal to the bar, or Tab leaves it hidden (2.4.11)
An animated openThe content takes focus about 20 ms after opening: a person cannot press that fast, but a test waits a frame

Screen readers

From the post, on real devices. The state is always read: never add it to the name.

Screen readers
PairReads
VoiceOver + Safariname, collapsed, summary
VoiceOver + Chrome, Firefoxname, collapsed, disclosure triangle, group
NVDA + Chrome, Firefoxname, button, collapsed
TalkBack + Chromecollapsed, name, disclosure triangle
TalkBack + Firefoxcollapsed, name, button, name, Space
Chromium accessibility treesummary = DisclosureTriangle, expanded true or false (measured)

Support

Support
BrowserChecked
ChromiumYes
WebKit (Safari)Yes
FirefoxNo

WCAG criteria

WCAG criteria
CriterionMet by
1.3.1 Info and Relationships (A)Native role and state
1.4.1 Use of Color (A)The marker changes shape, not only colour
1.4.4 Resize Text (AA)rem; no fixed height on the panel
1.4.10 Reflow (AA)No horizontal scroll at 320px; only code may scroll
1.4.11 Non-text Contrast (AA)The marker and the ring 3:1
2.1.1 Keyboard (A)Enter and Space on the summary
2.3.3 Animation from Interactions (AAA)Motion only under prefers-reduced-motion: no-preference
2.4.7 Focus Visible (AA)The ring on summary:focus-visible, not clipped by overflow
2.4.11 Focus Not Obscured, Minimum (AA)scroll-margin on the summary under a sticky bar
2.5.3 Label in Name (A)The visible text is the name
2.5.8 Target Size, Minimum (AA)The whole row, at least 24px tall (44px here)
4.1.2 Name, Role, Value (A)Native; nothing overridden

Related

This section is still in progress.

References

Read the post: Learn how to build a disclosure component using the native HTML details tag (opens in a new tab)