A lane of cards on a board: a title with a capacity meter and an action, then groups of cards, each with a label and a count.
Agents
- Check the new color ladder against the contrast pairs.
- One line per change, in plain words.
Options
The board column has no choices: it is drawn one way.
Examples
Usage
A title with a capacity meter and an add button, then two groups of cards, each with a status dot, a label and a count.
Website
- Check the new color ladder against the contrast pairs.
- One line per change, in plain words.
With a summary line
A line of text in place of the meter.
Docs
2 open, 1 waiting on you
- The draft is ready for a look.
Scrolling inside
Given a height, the column scrolls inside itself and becomes a stop for the Tab key.
Design system
- Check the new color ladder against the contrast pairs.
- One line per change, in plain words.
Mobile
In a narrow space the column fills the width.
Shop
- Check the new color ladder against the contrast pairs.
- One line per change, in plain words.
Groups that fold on a phone
Each group's label is a heading in the page's outline. On a phone the finished and failed groups fold under their headings, closed at first; a press on a heading opens it. In a wide space they are plain groups.
Store
Waiting on you
1- Two drafts are ready to compare.
Failed
1
- The export file was empty.
A group that always folds
A group that folds on any screen, open at first: its caret turns as it closes.
Blog
In progress
1Guidelines
Purpose
One lane of a board: the work for one project, one person or one stage. Its head says what the lane is and how full it is; under it, its cards are grouped by where each one stands, such as "Waiting on you" and "In progress".
When to use
- Inside a board, one column per lane.
- On its own, for one lane of work in a narrow side panel.
When not to use
Usage rules
Do
- Give each column a short title of its own.
- Order the groups by what needs the reader first.
Don't
- Use a bare "Add" on every column.
- Let a group run on for dozens of cards; show a few and offer the rest.
Blog
Name the add button for what it adds and where, since every column has one.
News
A bare "Add" on every column leaves a screen reader user guessing which one they are on.
Anatomy
- The block, a lane pressed into the page: the canvas surface with the strong inset shadow, inside a subtle hairline, with the medium container corners.
- The head part: the heading part on the left, holding the title part in a small heading style and, under it, the meter part, and the action part at the end.
- The meter part holds a capacity meter, or a line of text that sums the lane up.
- The action part holds a small ghost icon button that adds to the lane.
- The group part, one per group: the group head part (the dot part, a status dot; the group label part in a small label style and the subtle text color; the count part, a small soft badge), then the cards part, a list with one card part per item, then the more part, a small ghost button that shows the rest. A group that folds is a disclosure: its group head is the row that opens and closes it, with the caret part at its end, and the group body part holds the cards and the more part.
Behavior
On a wide screen a column is the large column width; in a narrow space it fills the width it is given. It is as tall as its cards. Given a height by the page, it scrolls inside itself with the system's scrollbar: a thin, see-through bar over its cards that takes no room and shows while the mouse is over it or it scrolls.
A group shows the cards it is given; to keep a long group short, show a few and put a "Show all" button under them.
A group can fold under its head (fold): always, or narrow, only in a narrow space such as a phone, where the columns stack and a finished or failed group can wait closed under the work still to do. It starts closed unless defaultExpanded is set, or the page holds it with expanded and onExpandedChange. Pressing the head opens or closes it: the cards unfold with the expand motion as an accordion's item does, and the caret turns. A group that folds only in a narrow space is a plain group in a wide one, its cards always shown, and closed or open again as it was when the space turns narrow. Written by hand it is a details element with the smbk-board-column__group--fold-always or --fold-narrow class, its head the summary; in a wide space the stylesheet shows a narrow-only group's cards and hides its caret, where the browser lets a page show a closed disclosure's content.
Accessibility
What it does for you
- Each column is a region named by its title, so a screen reader can list the lanes and jump between them.
- The title is a third-level heading by default.
- Each group is a list named by its label and its count, such as "Waiting on you 6", one item per card.
- Given
headingLevel, a group's label is a heading at that level, so the group sits in the page's outline under its column. - A group that folds is a native disclosure: its head is one row that says whether it is open, and opens and closes with Enter and Space, with no script.
- The group's dot is decoration: the label says what the group is.
- While a column with a height has more to show than fits, it is a stop for the Tab key, so the arrow keys can scroll it, and shows the focus ring inside its edge.
What you must do
- Give each column a title of its own.
- Set
headingLevelto fit the page's outline, one below the heading above the board. A group'sheadingLevelgoes one below its column's, and the cards' titles one below that. - Name the add button for what it adds and where, such as "Add an agent to the blog", and the "Show all" button for what it shows, such as "Show all 6 waiting".
How to test
- Tab through a column: focus goes through the head, the cards and "Show all" in reading order, with the focus ring showing in both themes and in a forced-color mode.
- Give a column a height it cannot fit in: it takes focus as a stop, and the arrow keys scroll it.
- With a screen reader, each column is found as a region by its title, and each group as a list with its label and count.
- On a phone, Tab to a folding group's head and press Enter or Space: it opens and says it is expanded; press again and it closes.
- With text at 200% and the page 320 pixels wide, the title and cards wrap and nothing is cut off.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus through the head's buttons, the cards that are links or buttons, and "Show all". A column that scrolls is a stop of its own. |
| Up ArroworDown Arrow | Scrolls a column that has more to show than fits, while the column has focus. |
| Enter | Follows a focused link, presses a focused button, or opens or closes a folding group from its head. |
| Space | Presses a focused button, or opens or closes a folding group from its head. |
Source
The files the board column is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-board-column — Board column and Board group. Figma "Board column" 7118:731 (Meter and Action
* booleans) and "Board group" 7118:654 (Count, Dot and Show all booleans).
* A lane of cards pressed into the page: the canvas surface with the strong inset shadow inside a subtle
* hairline, the medium container corners, 12 all round. Its head holds the title in Heading XS, the
* capacity meter under it and an action at the end; then its groups, each a label with a status dot and a
* count, the cards in a list and an optional "Show all". On a wide screen a column is the large column
* width; in a narrow space it fills the width. A column given a height scrolls inside itself. A group can fold
* under its head, always or only in a narrow space: it is then a native details element, its head the summary
* with a caret at its end, its cards and "Show all" what it discloses (see the folding rules at the end). */
.smbk-board-column {
box-sizing: border-box;
display: flex;
flex: none;
flex-direction: column;
gap: var(--smbk-space-stack-xl);
width: var(--smbk-size-column-min-lg);
max-width: 100%;
padding: var(--smbk-space-inset-lg);
overflow-y: auto;
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: var(--smbk-radius-container-md);
background: var(--smbk-color-surface-canvas);
color: var(--smbk-color-text-default);
box-shadow: var(--smbk-elevation-inset-strong);
/* The system's scrollbar: thin, in the subtle border color, on a clear track. */
scrollbar-width: thin;
scrollbar-color: var(--smbk-color-border-subtle) transparent;
}
/* A column that scrolls is a region the keyboard reaches: its ring is drawn inside its edge. */
.smbk-board-column:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: calc(-1 * var(--smbk-focus-ring-width));
}
.smbk-board-column__head {
display: flex;
align-items: flex-start;
gap: var(--smbk-space-inline-md);
}
.smbk-board-column__heading {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--smbk-space-stack-sm);
min-width: 0;
}
.smbk-board-column__title {
margin: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-heading-xs-font-family);
font-size: var(--smbk-typography-heading-xs-font-size);
font-weight: var(--smbk-typography-heading-xs-font-weight);
line-height: var(--smbk-typography-heading-xs-line-height);
letter-spacing: var(--smbk-typography-heading-xs-letter-spacing);
text-transform: var(--smbk-typography-heading-xs-text-transform);
overflow-wrap: anywhere;
}
.smbk-board-column__meter {
display: flex;
}
.smbk-board-column__action {
display: flex;
flex-shrink: 0;
gap: var(--smbk-space-inline-xs);
}
/* ---- a group: its label row, its cards and "Show all" ---- */
.smbk-board-column__group {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--smbk-space-stack-md);
}
.smbk-board-column__group-head {
display: flex;
align-items: center;
gap: var(--smbk-space-inline-sm);
}
.smbk-board-column__group-label {
margin: 0;
color: var(--smbk-color-text-subtle);
font-family: var(--smbk-typography-label-sm-font-family);
font-size: var(--smbk-typography-label-sm-font-size);
font-weight: var(--smbk-typography-label-sm-font-weight);
line-height: var(--smbk-typography-label-sm-line-height);
letter-spacing: var(--smbk-typography-label-sm-letter-spacing);
text-transform: var(--smbk-typography-label-sm-text-transform);
}
.smbk-board-column__cards {
display: flex;
flex-direction: column;
gap: var(--smbk-space-stack-md);
align-self: stretch;
margin: 0;
padding: 0;
list-style: none;
}
.smbk-board-column__card {
display: block;
}
/* ---- a group that folds ----
A group given a fold is a details element: its head is the summary, so it opens and closes with Enter and Space and
says whether it is open with no script, and its cards and "Show all" sit in the body it discloses, the same room
above them as in a group that does not fold. The head is at least the extra-small control height, the least a press
needs, with a caret at its end in the extra-small icon size, the label's, that turns when the group is open; pointing at it darkens its
label and caret, and keyboard focus rings it. A group that folds only in a narrow space is a plain group in a wide
one: the React component draws it so, and in markup written by hand the stylesheet shows its body whatever the
open state and hides the caret. */
.smbk-board-column__group--fold-always,
.smbk-board-column__group--fold-narrow {
display: block;
}
.smbk-board-column__group-body {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: var(--smbk-space-stack-md);
padding-top: var(--smbk-space-stack-md);
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head {
box-sizing: border-box;
min-height: var(--smbk-size-control-xs);
border-radius: var(--smbk-radius-control-sm);
list-style: none;
cursor: pointer;
-webkit-tap-highlight-color: transparent;
touch-action: manipulation;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head::-webkit-details-marker {
display: none;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
}
@media (hover: hover) {
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head:hover .smbk-board-column__group-label {
color: var(--smbk-color-text-default);
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head:hover .smbk-board-column__caret {
color: var(--smbk-color-icon-default);
}
}
.smbk-board-column__caret {
flex-shrink: 0;
color: var(--smbk-color-icon-subtle);
font-size: var(--smbk-size-icon-xs);
transition: transform var(--smbk-motion-expand-duration) var(--smbk-motion-expand-easing);
}
/* A group the React component is closing (data-closing) is still open until its motion ends; its caret turns back as
the motion starts. */
.smbk-board-column__group[open]:not([data-closing]) > .smbk-board-column__group-head .smbk-board-column__caret {
transform: rotate(180deg);
}
/* ---- motion: the body unfolds with the expand purpose and folds back the same way, as an accordion's item does.
The group's height grows from its head to its own while the body 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 group data-motion
once it runs; while it moves (data-moving) it is clipped, with room for the head's focus ring. Markup written by
hand is moved by the stylesheet alone where the browser can animate to a natural height, and opens at once
elsewhere. Under reduced motion the height lands at once and the fade still plays. ---- */
.smbk-board-column__group[data-moving] {
overflow: clip;
overflow-clip-margin: calc(var(--smbk-focus-ring-width) + var(--smbk-focus-ring-offset));
}
@supports (interpolate-size: allow-keywords) {
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow):not([data-motion]) {
interpolate-size: allow-keywords;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow):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;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow):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) {
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow):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;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow):not([data-motion])[open]::details-content {
transition-delay: 0s;
}
}
}
@media (prefers-reduced-motion: reduce) {
.smbk-board-column__caret {
transition: none;
}
}
/* A group that folds only in a narrow space, written by hand, in a wide one: its body shows whatever the open state,
its head takes no press look and no caret, so it reads as a group that does not fold. */
@container style(--smbk-layout: wide) {
.smbk-board-column__group--fold-narrow > .smbk-board-column__group-head {
min-height: 0;
cursor: auto;
}
.smbk-board-column__group--fold-narrow .smbk-board-column__caret {
display: none;
}
.smbk-board-column__group.smbk-board-column__group--fold-narrow:not([data-motion])::details-content {
content-visibility: visible;
block-size: auto;
opacity: 1;
filter: none;
}
}
@media (hover: hover) {
@container style(--smbk-layout: wide) {
.smbk-board-column__group--fold-narrow > .smbk-board-column__group-head:hover .smbk-board-column__group-label {
color: var(--smbk-color-text-subtle);
}
}
}
/* ---- touch: a folding group's head 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) {
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head {
position: relative;
}
:is(.smbk-board-column__group--fold-always, .smbk-board-column__group--fold-narrow) > .smbk-board-column__group-head::after {
content: '';
position: absolute;
inset: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
}
/* ---- the narrow form: the column fills the width it is given ---- */
@container style(--smbk-layout: narrow) {
.smbk-board-column {
width: 100%;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.stack.xl--smbk-space-stack-xl | comfortable: 24pxcompact: 16pxchanges with density | Spacing | |
| size.column.min.lg--smbk-size-column-min-lg | 400px | Size | |
| space.inset.lg--smbk-space-inset-lg | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.container.md--smbk-radius-container-md | 12px | Radius | |
| color.surface.canvas--smbk-color-surface-canvas | light: #e7ebf0dark: #111213changes with theme | Color | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| elevation.inset-strong--smbk-elevation-inset-strong | light: inset -1px 2px 2px 0px #373d4333dark: inset -1px 2px 2px 0px #0000004d | Elevation | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.stack.sm--smbk-space-stack-sm | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| typography.heading-xs--smbk-typography-heading-xs-font-family--smbk-typography-heading-xs-font-size--smbk-typography-heading-xs-font-weight--smbk-typography-heading-xs-line-height--smbk-typography-heading-xs-letter-spacing--smbk-typography-heading-xs-text-transform | fontFamily: Funnel Display, Funnel Display Fallback, system-ui, sans-seriffontSize: min: 20px · max: 28pxfontWeight: 400lineHeight: 1.2letterSpacing: 0px | The quick brown fox | Typography |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| space.stack.md--smbk-space-stack-md | comfortable: 12pxcompact: 8pxchanges with density | Spacing | |
| space.inline.sm--smbk-space-inline-sm | comfortable: 6pxcompact: 4pxchanges with density | Spacing | |
| color.text.subtle--smbk-color-text-subtle | light: #4e555edark: #9ba5b1changes with theme | Color | |
| typography.label-sm--smbk-typography-label-sm-font-family--smbk-typography-label-sm-font-size--smbk-typography-label-sm-font-weight--smbk-typography-label-sm-line-height--smbk-typography-label-sm-letter-spacing--smbk-typography-label-sm-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| size.control.xs--smbk-size-control-xs | 24px | Size | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.icon.default--smbk-color-icon-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| color.icon.subtle--smbk-color-icon-subtle | light: #4e555edark: #c4cdd8changes with theme | Color | |
| size.icon.xs--smbk-size-icon-xs | 12px | Size | |
| 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 | |
| motion.expand.blur--smbk-motion-expand-blur | 2px | Motion | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
Manifest
{
"name": "board-column",
"block": "smbk-board-column",
"summary": "A lane of cards on a board: a title with a capacity meter and an action, then groups of cards, each with a label and a count.",
"adapt": "css",
"axes": {},
"flags": {},
"parts": [
"head",
"heading",
"title",
"meter",
"action",
"group",
"group-head",
"dot",
"group-label",
"count",
"caret",
"group-body",
"cards",
"card",
"more"
],
"figmaNodeId": "7118:731"
}