One row that shows or hides the content under it, for detail most readers can skip.
Show older releases
Version 2.3 added saved filters, so a view you build once comes back the next time you open the board.
Version 2.2 was the first public release, with shared boards, comments and an export to spreadsheets.
Options
| Choice | Values | Default | What it does |
|---|---|---|---|
disabledsmbk-collapsible--disabled | On or off | Off | The row dims and cannot be opened or closed. Figma: State Disabled. |
Examples
Usage
A release-notes page with the older releases folded away under one row, shown open.
Release notes
Every change to the app, newest first.
Show older releases
Version 2.3 added saved filters, so a view you build once comes back the next time you open the board.
Version 2.2 was the first public release, with shared boards, comments and an export to spreadsheets.
Closed
Closed, the row alone: its caret points down.
Show older releases
Version 2.3 added saved filters, so a view you build once comes back the next time you open the board.
Version 2.2 was the first public release, with shared boards, comments and an export to spreadsheets.
Without an icon
The row without its icon, holding settings only some people change.
Advanced settings
Disabled
A disabled row cannot be opened, and says so to a screen reader.
Billing history
Your invoices, newest first.
Guidelines
Purpose
One row that shows or hides the content under it, so detail most readers can skip stays out of the way until someone asks for it: older releases under the latest one, the advanced settings under the usual ones. It is a single quiet row, with no card around it. Several sections that each open on their own belong in an accordion.
When to use
- Detail most readers can skip, under the content they need: earlier versions, the full list behind a short one, settings only some people change.
- One optional section on a page or in a form, where a card would weigh too much.
When not to use
Usage rules
Do
- Keep the label to one line, and say what is inside.
- Start it closed, unless most readers need what it holds; then do not hide it.
- In markup written by hand, give the block
smbk-collapsible, the summary the trigger part and the content the panel part, and mark the summaryaria-disabled="true"with the disabled modifier on the block when it cannot be opened.
Don't
- Hide something a reader must act on, such as an error or a required field.
- Stack several one under another to make a list of sections. That is an accordion.
Show older releases
Version 2.3 added saved filters, so a view you build once comes back the next time you open the board.
Version 2.2 was the first public release, with shared boards, comments and an export to spreadsheets.
Say what is inside, so a reader knows whether to open it.
More
Version 2.3 added saved filters, so a view you build once comes back the next time you open the board.
Version 2.2 was the first public release, with shared boards, comments and an export to spreadsheets.
A row that only says "More" makes the reader open it to find out whether they need it.
Anatomy
- The block, a native disclosure element around the row and its panel.
- The trigger part, the row that opens and closes it: the disclosure's summary, the medium control height, padded at its sides.
- The icon part, optional, before the label.
- The label part, the row's words, taking the room the icon and the caret leave.
- The caret part, at the far end, which turns while the block is open.
- The panel part, what shows while open, set in from the sides to line up with the label's row.
Behavior
Pressing the row opens or closes the panel, and the caret turns with it. The panel unfolds with the expand motion and folds back the same way: the height grows from the row while the content fades in from a slight blur, and the foot of what shows fades rather than meeting the edge with a hard cut. A press halfway turns it round from where it is. The React component moves it, the same in every browser, and a page that holds the open state (expanded) moves it with the same motion. Markup written by hand is moved by the stylesheet where the browser can animate to a natural height, and opens at once elsewhere. Under reduced motion the height changes at once and the fade still plays.
Hover and keyboard focus lay a soft surface under the row; focus adds its ring. The block fills the width it is given. On a touch screen the row keeps its height and takes an invisible press area out to the comfortable touch target.
A browser's find in page can open it to show a match inside; the React component then reports it open.
Accessibility
What it does for you
- It is a native disclosure element, so Enter and Space open and close it and its open state is announced, with no script of yours.
- The row's name is its label. The icon and the caret are hidden from assistive technology.
- A disabled collapsible cannot be opened or closed, by the mouse, the keyboard or a screen reader's press. Its row stays in the tab order and is announced as unavailable.
What you must do
- Write a label that says what the panel holds or what pressing does, such as "Show older releases".
- Wrap the label in a heading element when the panel is a section of the page with a heading of its own.
- Use the React component for a disabled collapsible, or leave it out. In markup written by hand only the mouse is kept off a disabled row, since a screen reader can still press it.
How to test
- Tab to the row: the focus ring shows, in both themes and in a forced-color mode.
- Press Enter, then Space: each opens or closes the panel.
- With a screen reader, the row is read as a disclosure by its label, with its open or closed state.
- With text at 200% and the page 320 pixels wide, the label wraps and nothing is cut off.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the row, a disabled one included. |
| EnterorSpace | Opens the panel, or closes it when it is open. A disabled row does nothing. |
Source
The files the collapsible is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-collapsible — Collapsible. Figma "Collapsible" 8196:2813 (State: Default, Hover, Focus, Disabled, Expanded).
* One row that shows or hides what is under it. The block is a native details element, so opening, closing and the
* keyboard are the browser's; the trigger is its summary. A row the medium control height, an optional icon, a Label LG
* label and a caret at the far end that turns when open. Hover and keyboard focus lay the hover surface under the row,
* focus with its ring. The panel unfolds with the expand purpose, as an accordion's item does (see the motion rules at
* the end). */
.smbk-collapsible {
box-sizing: border-box;
width: 100%;
color: var(--smbk-color-text-default);
}
.smbk-collapsible__trigger {
box-sizing: border-box;
display: flex;
align-items: center;
gap: var(--smbk-space-inline-md);
min-height: var(--smbk-size-control-md);
padding: 0 var(--smbk-space-inset-lg);
border-radius: var(--smbk-radius-control-sm);
list-style: none;
cursor: pointer;
font-family: var(--smbk-typography-label-lg-font-family);
font-size: var(--smbk-typography-label-lg-font-size);
font-weight: var(--smbk-typography-label-lg-font-weight);
line-height: var(--smbk-typography-label-lg-line-height);
letter-spacing: var(--smbk-typography-label-lg-letter-spacing);
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
.smbk-collapsible__trigger::-webkit-details-marker {
display: none;
}
@media (hover: hover) {
.smbk-collapsible__trigger:hover {
background: var(--smbk-color-surface-hover);
}
}
.smbk-collapsible__trigger:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
background: var(--smbk-color-surface-hover);
}
.smbk-collapsible__icon {
flex-shrink: 0;
color: var(--smbk-color-icon-subtle);
font-size: var(--smbk-size-icon-md);
}
/* The label takes the room the icon and the caret leave, and wraps rather than being cut short. */
.smbk-collapsible__label {
flex: 1 1 auto;
min-width: 0;
}
.smbk-collapsible__caret {
flex-shrink: 0;
color: var(--smbk-color-icon-subtle);
font-size: var(--smbk-size-icon-md);
}
/* A collapsible the React component is closing (data-closing) is still open until its motion ends; its caret turns
back as the motion starts. */
.smbk-collapsible[open]:not([data-closing]) > .smbk-collapsible__trigger .smbk-collapsible__caret {
transform: rotate(180deg);
}
.smbk-collapsible__panel {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-sm);
padding: var(--smbk-space-stack-xs) var(--smbk-space-inset-lg) var(--smbk-space-stack-md);
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-body-md-font-family);
font-size: var(--smbk-typography-body-md-font-size);
font-weight: var(--smbk-typography-body-md-font-weight);
line-height: var(--smbk-typography-body-md-line-height);
letter-spacing: var(--smbk-typography-body-md-letter-spacing);
}
/* Paragraphs in the panel are spaced by its gap, not by their own margins. */
.smbk-collapsible__panel > * {
margin: 0;
}
/* ---- disabled: the row dims and takes no press ---- */
.smbk-collapsible--disabled > .smbk-collapsible__trigger {
color: var(--smbk-color-text-disabled);
cursor: not-allowed;
pointer-events: none;
}
.smbk-collapsible--disabled .smbk-collapsible__icon,
.smbk-collapsible--disabled .smbk-collapsible__caret {
color: var(--smbk-color-icon-disabled);
}
/* ---- motion: the panel unfolds with the expand purpose and folds back the same way, as an accordion's item does.
The block's height grows from the trigger row to its own while the panel fades in from the purpose's slight blur,
and the caret turns with it. The React component moves it, the same in every browser, and marks the block
data-motion once it runs. While it moves (data-moving) it is clipped, with room to spare for the trigger's focus
ring, and the foot of what shows fades out over a spacing step, or over what is still hidden when that is less.
Markup written by hand is moved by the stylesheet alone where the browser can animate to a natural height, without
the fading foot, and opens at once elsewhere. Under reduced motion the height lands at once and the fade and the
blur still play. ---- */
/* How much of the panel shows while the block moves, from its top: the React component moves it with the height. */
@property --smbk-collapsible-reveal {
syntax: "<length-percentage>";
inherits: false;
initial-value: 100%;
}
.smbk-collapsible__caret {
transition: transform var(--smbk-motion-expand-duration) var(--smbk-motion-expand-easing);
}
.smbk-collapsible[data-moving] {
overflow: clip;
overflow-clip-margin: calc(var(--smbk-focus-ring-width) + var(--smbk-focus-ring-offset));
}
/* The mask reads only how opaque its paint is, so the text color stands in for "shown". */
.smbk-collapsible[data-moving] > .smbk-collapsible__panel {
mask-image: linear-gradient(currentcolor calc(var(--smbk-collapsible-reveal) - min(var(--smbk-space-16), 100% - var(--smbk-collapsible-reveal))), transparent var(--smbk-collapsible-reveal));
}
@supports (interpolate-size: allow-keywords) {
.smbk-collapsible:not([data-motion]) {
interpolate-size: allow-keywords;
}
.smbk-collapsible:not([data-motion])::details-content {
block-size: 0;
opacity: 0;
filter: blur(var(--smbk-motion-expand-blur));
overflow: clip;
overflow-clip-margin: calc(var(--smbk-focus-ring-width) + var(--smbk-focus-ring-offset));
transition-property: block-size, opacity, filter, content-visibility;
transition-duration: var(--smbk-motion-expand-duration);
transition-timing-function: var(--smbk-motion-expand-easing);
transition-behavior: allow-discrete;
}
.smbk-collapsible:not([data-motion])[open]::details-content {
block-size: auto;
opacity: 1;
filter: none;
}
/* Under reduced motion the height steps: at once as it opens, and only once it has faded as it closes. */
@media (prefers-reduced-motion: reduce) {
.smbk-collapsible:not([data-motion])::details-content {
transition-duration: 0s, var(--smbk-motion-expand-duration), var(--smbk-motion-expand-duration), var(--smbk-motion-expand-duration);
transition-delay: var(--smbk-motion-expand-duration), 0s, 0s, 0s;
}
.smbk-collapsible:not([data-motion])[open]::details-content {
transition-delay: 0s;
}
}
}
@media (prefers-reduced-motion: reduce) {
.smbk-collapsible__caret {
transition: none;
}
}
/* ---- touch: the trigger row takes an invisible press area ----
On a touch screen an invisible area, centred on it, takes the press out to the comfortable target in each direction
it falls short of it; its look, its focus ring and its place stay as drawn. */
@media (pointer: coarse) {
.smbk-collapsible__trigger {
position: relative;
}
.smbk-collapsible__trigger::after {
content: '';
position: absolute;
inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
}
/* ---- high contrast (forced colors) ---- */
/* In Windows high-contrast mode the browser drops fills and shadows; this puts the meaning back with the
user's own colors. Nothing here applies outside that mode. */
@media (forced-colors: active) {
.smbk-collapsible--disabled > .smbk-collapsible__trigger,
.smbk-collapsible--disabled .smbk-collapsible__icon,
.smbk-collapsible--disabled .smbk-collapsible__caret {
color: GrayText;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| size.control.md--smbk-size-control-md | comfortable: 40pxcompact: 32pxchanges with density | Size | |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| typography.label-lg--smbk-typography-label-lg-font-family--smbk-typography-label-lg-font-size--smbk-typography-label-lg-font-weight--smbk-typography-label-lg-line-height--smbk-typography-label-lg-letter-spacing--smbk-typography-label-lg-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| color.surface.hover--smbk-color-surface-hover | light: #373d4326dark: #606a754dchanges with theme | Color | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.icon.subtle--smbk-color-icon-subtle | light: #4e555edark: #c4cdd8changes with theme | Color | |
| size.icon.md--smbk-size-icon-md | 20px | Size | |
| space.stack.sm--smbk-space-stack-sm | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.body-md--smbk-typography-body-md-font-family--smbk-typography-body-md-font-size--smbk-typography-body-md-font-weight--smbk-typography-body-md-line-height--smbk-typography-body-md-letter-spacing--smbk-typography-body-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 16pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.disabled--smbk-color-text-disabled | light: #7c8692dark: #606a75changes with theme | Color | |
| color.icon.disabled--smbk-color-icon-disabled | light: #9ba5b1dark: #606a75changes with theme | Color | |
| motion.expand.duration--smbk-motion-expand-duration | 320ms | Motion | |
| motion.expand.easing--smbk-motion-expand-easing | cubic-bezier(0.215, 0.61, 0.355, 1) | Motion | |
| space.16--smbk-space-16 | 16px | Spacing | |
| motion.expand.blur--smbk-motion-expand-blur | 2px | Motion | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
Manifest
{
"name": "collapsible",
"block": "smbk-collapsible",
"summary": "One row that shows or hides the content under it, for detail most readers can skip.",
"adapt": "css",
"axes": {},
"flags": {
"disabled": {
"description": "The row dims and cannot be opened or closed. Figma: State Disabled."
}
},
"parts": [
"trigger",
"icon",
"label",
"caret",
"panel"
],
"figmaNodeId": "8196:2813"
}