A width ruler for a preview: the layout states and breakpoints along one band, the screen widths, and a handle for the width now.
Options
The ruler has no choices: it is drawn one way.
Examples
Usage
The ruler above the preview it measures; the handle is the width now. Drag it, press the band or a width, or Tab to the handle and use the arrow keys.
The width alone
Without the numbers under the band, Fit or Hide, for a narrow tool bar.
Guidelines
Purpose
Sets and shows the width of a preview: one band from nothing to the widest width, shaded by layout state, with the breakpoints, the common screen widths and a handle for the width now. It is for tools that preview pages or components at different widths. To set a width that has nothing to do with layout, use a slider.
When to use
- Above a preview whose width the reader sets, to show which layout state each width falls in.
When not to use
- For a value that is not a width in pixels. Use a slider.
Usage rules
Do
- Give it the system's own breakpoints.
- Put it directly above the preview it sets.
Don't
- Make up breakpoints of its own, which shows states no page has.
Give it the system’s own breakpoints, so its states are the ones the pages switch at.
Make up breakpoints of its own, which shows states no page has.
Anatomy
- The block: a row of the scale, the width now and the buttons. In a narrow space the scale takes the whole row and the rest goes under it.
- The scale part: the band over the widths.
- The band part, 24 px tall on the sunken surface: one zone part per layout state, each after the first shaded and named inside its stretch; a full line (the break part) at each breakpoint; a short tick (the tick part) at each screen width; and the handle part, a brand bar at the width now.
- The widths part: the numbers under the band, each a width part. They can be left out. In a narrow space only the screen widths keep their numbers.
- The now part: the width now, such as "1024 px".
- The controls part: Fit, a toggle that makes the preview fill the space (the fit part), and Hide (the hide part). Each shows only when the page handles it.
Behavior
The page gives the layout states, each from the width it starts at, the screen widths to mark and the width at the far end; pass the system's own breakpoints so the states match the pages'. The ruler holds the width from a first value, or follows the one the page gives it, and reports each new width. A press on the band moves the handle there and dragging carries it; a press on a number sets that width. The handle stays between the narrowest screen width, or the least given, and the far end. Places along the band are set with logical properties, so a right-to-left page reads it from the right.
On a touch screen the band, each width and the buttons keep their look and take an invisible press area, out to the comfortable touch target where there is room and halfway to each other where they stand closer.
Accessibility
What it does for you
- The handle is a slider named "Preview width". It says its value in pixels with its layout state: "768 pixels, wide".
- The numbers under the band are buttons named in pixels ("320 pixels"); the one at the width now is pressed.
- Fit is a toggle button named "Fit the space"; Hide is named "Hide the ruler".
- The layout state is said in words, not told by shading alone.
- In forced colors mode the band's lines and the handle take the reader's colors.
- Every word it says by itself, such as the names of the widths, Fit and Hide, keeps its English unless the page gives its own.
- Dragging is never the only way: the keys move the handle too.
- On a touch screen the band, each width and the buttons take an invisible press area, out to the comfortable touch target where there is room and halfway to each other where they stand closer.
What you must do
- Give the names of the widths, Fit and Hide in the page's own language when the page is not in English.
- Give it the system's own breakpoints, so the layout states it says in words match the pages'.
How to test
- Tab to the handle: the focus ring shows. Press Left Arrow and Right Arrow: the width changes by 10 pixels, by 1 with Shift held; Page Up and Page Down change it by 100, and Home and End go to its ends.
- With a screen reader, the handle is read as a slider named "Preview width", with its value in pixels and its layout state.
- Press a number under the band: the handle moves to that width, and the number is read as pressed.
- With text at 200% and the page 320 pixels wide, the numbers and buttons stay reachable.
- In a forced-color mode, the band's lines and the handle are visible in the reader's colors.
Keyboard
| Key | What it does |
|---|---|
| Tab | Moves focus to the handle, then to the width buttons, Fit and Hide. |
| Left ArroworRight Arrow | Moves the handle 10 pixels, or 1 with Shift. On a right-to-left page the directions swap. |
| Up ArroworDown Arrow | Moves the handle wider or narrower by 10 pixels, or 1 with Shift. |
| Page UporPage Down | Moves the handle 100 pixels. |
| HomeorEnd | Moves the handle to its narrowest or widest end. |
| EnterorSpace | On a width button, Fit or Hide, presses it. |
Source
The files the ruler is made of. Its folder on GitHub (opens in a new tab) holds them with its wrapper and its document.
Stylesheet
/* smbk-ruler — Ruler. Figma "Ruler" 7417:374 (State: Default, Focus; Widths switch).
* A width ruler for a preview: one band from nothing to the widest width, 24 tall on the sunken surface inside a subtle
* edge, shaded by layout state (the first state plain, each later one on the subtle neutral surface, named inside it in
* Label SM), the breakpoints as full strong hairlines and the screen widths as short ticks at its foot. Under it, the
* widths in Body XS. The handle is the width now, a 4 px brand bar standing 4 px out of the band at both ends; when it
* has keyboard focus it draws the focus ring. Beside the band: the width now in Label MD, then the Fit and Hide icon
* buttons. Places along the band are the wrapper's, as percentages of its length, set with logical properties so a
* right-to-left page reads it from the right. In a narrow space the band takes the whole row, the width and the buttons
* go under it, and only the screen widths keep their numbers. */
.smbk-ruler {
display: flex;
align-items: center;
gap: var(--smbk-space-inline-md);
min-width: 0;
}
.smbk-ruler__scale {
display: flex;
flex: 1 1 auto;
flex-direction: column;
gap: var(--smbk-space-stack-xs);
min-width: 0;
}
.smbk-ruler__band {
position: relative;
box-sizing: border-box;
height: var(--smbk-size-24);
border: var(--smbk-border-width-hairline) solid var(--smbk-color-border-subtle);
border-radius: var(--smbk-radius-control-sm);
background: var(--smbk-color-surface-sunken);
touch-action: none;
cursor: pointer;
}
/* A layout state's stretch of the band; each after the first is shaded. Its name sits inside it and is cut off at its
end rather than running into the next. */
.smbk-ruler__zone {
position: absolute;
inset-block: 0;
display: flex;
align-items: center;
box-sizing: border-box;
overflow: hidden;
padding-inline-start: var(--smbk-space-inset-sm);
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);
white-space: nowrap;
pointer-events: none;
}
.smbk-ruler__zone + .smbk-ruler__zone {
background: var(--smbk-color-surface-neutral-subtle);
}
/* The last stretch takes the band's own corner at its end. */
.smbk-ruler__zone:last-of-type:not(:first-of-type) {
border-start-end-radius: var(--smbk-radius-control-sm);
border-end-end-radius: var(--smbk-radius-control-sm);
}
/* A breakpoint: a full strong hairline. A screen width: a short tick at the band's foot. */
.smbk-ruler__break,
.smbk-ruler__tick {
position: absolute;
width: var(--smbk-border-width-hairline);
background: var(--smbk-color-border-strong);
transform: translateX(-50%);
pointer-events: none;
}
.smbk-ruler__break {
inset-block: 0;
}
.smbk-ruler__tick {
bottom: 0;
height: var(--smbk-size-6);
}
[dir="rtl"] .smbk-ruler__break,
[dir="rtl"] .smbk-ruler__tick,
[dir="rtl"] .smbk-ruler__handle,
[dir="rtl"] .smbk-ruler__width {
transform: translateX(50%);
}
/* The handle: a brand bar standing out of the band at both ends, inside a 24 px wide target. */
.smbk-ruler__handle {
position: absolute;
inset-block: calc((var(--smbk-size-24) - var(--smbk-size-32)) / 2 - var(--smbk-border-width-hairline));
box-sizing: border-box;
width: var(--smbk-size-4);
border-radius: var(--smbk-radius-full);
background: var(--smbk-color-action-brand-background);
transform: translateX(-50%);
cursor: ew-resize;
outline: none;
}
.smbk-ruler__handle::before {
content: '';
position: absolute;
inset-block: 0;
inset-inline: calc((var(--smbk-size-4) - var(--smbk-size-24)) / 2);
}
/* Figma's focus style: the default surface in a gap, then the focus ring. The transparent outline is what forced colors
mode paints, since it drops shadows. */
.smbk-ruler__handle:focus-visible {
outline: var(--smbk-focus-ring-width) solid transparent;
outline-offset: var(--smbk-focus-ring-offset);
box-shadow:
0 0 0 var(--smbk-focus-ring-offset) var(--smbk-color-surface-default),
0 0 0 calc(var(--smbk-focus-ring-offset) + var(--smbk-focus-ring-width)) var(--smbk-color-border-focus);
}
/* The widths under the band, each centred under its place. */
.smbk-ruler__widths {
position: relative;
height: calc(var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height));
}
.smbk-ruler__width {
position: absolute;
top: 0;
margin: 0;
padding: 0;
border: 0;
background: none;
color: var(--smbk-color-text-subtlest);
font-family: var(--smbk-typography-body-xs-font-family);
font-size: var(--smbk-typography-body-xs-font-size);
font-weight: var(--smbk-typography-body-xs-font-weight);
line-height: var(--smbk-typography-body-xs-line-height);
letter-spacing: var(--smbk-typography-body-xs-letter-spacing);
font-variant-numeric: tabular-nums;
white-space: nowrap;
transform: translateX(-50%);
}
button.smbk-ruler__width {
cursor: pointer;
border-radius: var(--smbk-radius-control-xs);
outline: none;
}
/* A 24 px tall target round a width's figures. */
button.smbk-ruler__width::before {
content: '';
position: absolute;
inset-block: calc((var(--smbk-typography-body-xs-font-size) * var(--smbk-typography-body-xs-line-height) - var(--smbk-size-24)) / 2);
inset-inline: calc(-1 * var(--smbk-space-inline-xs));
}
@media (hover: hover) {
button.smbk-ruler__width:hover {
color: var(--smbk-color-text-default);
}
}
button.smbk-ruler__width[aria-pressed="true"] {
color: var(--smbk-color-text-default);
}
button.smbk-ruler__width:focus-visible {
outline: var(--smbk-focus-ring-width) solid var(--smbk-color-border-focus);
outline-offset: var(--smbk-focus-ring-offset);
}
.smbk-ruler__now {
flex-shrink: 0;
color: var(--smbk-color-text-default);
font-family: var(--smbk-typography-label-md-font-family);
font-size: var(--smbk-typography-label-md-font-size);
font-weight: var(--smbk-typography-label-md-font-weight);
line-height: var(--smbk-typography-label-md-line-height);
letter-spacing: var(--smbk-typography-label-md-letter-spacing);
font-variant-numeric: tabular-nums;
white-space: nowrap;
}
.smbk-ruler__controls {
display: flex;
flex-shrink: 0;
align-items: center;
gap: var(--smbk-space-inline-md);
}
/* ---- the narrow form: the band takes the row; the width and the buttons go under it; the breakpoints keep their lines
but not their numbers, which would run into the screen widths' ---- */
@container style(--smbk-layout: narrow) {
.smbk-ruler {
flex-wrap: wrap;
row-gap: var(--smbk-space-stack-sm);
}
.smbk-ruler__scale {
flex-basis: 100%;
}
.smbk-ruler__width[data-breakpoint] {
display: none;
}
}
/* ---- touch: the band, the widths and the buttons take invisible press areas ----
On a touch screen the band, which a finger drags along, takes an invisible area out to the comfortable target above
it, and below it only halfway to the widths; each width takes one out to the target, toward the band only halfway;
the buttons take theirs, side to side only halfway to each other. In the narrow form, where the widths and the
buttons wrap under the band, each reaches only halfway into the gaps between them. What is drawn stays as it is. */
@media (pointer: coarse) {
.smbk-ruler__band::before {
content: '';
position: absolute;
inset-inline: 0;
inset-block: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
.smbk-ruler__scale:has(button.smbk-ruler__width) .smbk-ruler__band::before {
inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-xs) / -2);
}
button.smbk-ruler__width::before {
inset-inline: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
inset-block-start: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-xs) / -2);
inset-block-end: min(0%, (100% - var(--smbk-size-target-comfortable)) / 2);
}
.smbk-ruler__controls .smbk-btn::after {
inset-inline: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
}
}
@container style(--smbk-layout: narrow) {
@media (pointer: coarse) {
.smbk-ruler__band::before {
inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-sm) / -2);
}
.smbk-ruler__scale:has(button.smbk-ruler__width) .smbk-ruler__band::before {
inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-xs) / -2);
}
button.smbk-ruler__width::before {
inset-inline: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-inline-md) / -2);
inset-block-end: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-sm) / -2);
}
.smbk-ruler__controls .smbk-btn::after {
inset-block: max(min(0%, (100% - var(--smbk-size-target-comfortable)) / 2), var(--smbk-space-stack-sm) / -2);
}
}
}
/* ---- high contrast (forced colors) ---- */
@media (forced-colors: active) {
.smbk-ruler__band {
border-color: CanvasText;
}
.smbk-ruler__break,
.smbk-ruler__tick {
background: CanvasText;
}
.smbk-ruler__handle {
background: Highlight;
}
.smbk-ruler__handle:focus-visible {
outline-color: CanvasText;
}
}Tokens it reads
| Token and CSS | Value | Preview | Foundation |
|---|---|---|---|
| space.inline.md--smbk-space-inline-md | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| space.stack.xs--smbk-space-stack-xs | 4px | Spacing | |
| size.24--smbk-size-24 | 24px | Size | |
| border-width.hairline--smbk-border-width-hairline | 1px | Border width | |
| color.border.subtle--smbk-color-border-subtle | light: #c4cdd8dark: #2d3237changes with theme | Color | |
| radius.control.sm--smbk-radius-control-sm | 4px | Radius | |
| color.surface.sunken--smbk-color-surface-sunken | light: #373d430ddark: #00000066changes with theme | Color | |
| space.inset.sm--smbk-space-inset-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 |
| color.surface.neutral-subtle--smbk-color-surface-neutral-subtle | light: #373d431adark: #606a7533changes with theme | Color | |
| color.border.strong--smbk-color-border-strong | light: #9ba5b1dark: #373d43changes with theme | Color | |
| size.6--smbk-size-6 | 6px | Size | |
| size.32--smbk-size-32 | 32px | Size | |
| size.4--smbk-size-4 | 4px | Size | |
| radius.full--smbk-radius-full | 9999px | Radius | |
| color.action.brand.background--smbk-color-action-brand-background | #cc0d0d | Color | |
| focus.ring.width--smbk-focus-ring-width | 2px | Focus | |
| focus.ring.offset--smbk-focus-ring-offset | 2px | Focus | |
| color.surface.default--smbk-color-surface-default | light: #f3f5f8dark: #181a1cchanges with theme | Color | |
| color.border.focus--smbk-color-border-focus | light: #cc0d0ddark: #f34336changes with theme | Color | |
| typography.body-xs--smbk-typography-body-xs-font-family--smbk-typography-body-xs-font-size--smbk-typography-body-xs-font-weight--smbk-typography-body-xs-line-height--smbk-typography-body-xs-letter-spacing--smbk-typography-body-xs-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 12pxfontWeight: 400lineHeight: 1.5letterSpacing: 0px | The quick brown fox | Typography |
| color.text.subtlest--smbk-color-text-subtlest | light: #606a75dark: #7c8692changes with theme | Color | |
| radius.control.xs--smbk-radius-control-xs | 2px | Radius | |
| space.inline.xs--smbk-space-inline-xs | 4px | Spacing | |
| color.text.default--smbk-color-text-default | light: #232527dark: #e7ebf0changes with theme | Color | |
| typography.label-md--smbk-typography-label-md-font-family--smbk-typography-label-md-font-size--smbk-typography-label-md-font-weight--smbk-typography-label-md-line-height--smbk-typography-label-md-letter-spacing--smbk-typography-label-md-text-transform | fontFamily: Geist, Geist Fallback, system-ui, sans-seriffontSize: 14pxfontWeight: 500lineHeight: 1.4letterSpacing: 0px | The quick brown fox | Typography |
| space.stack.sm--smbk-space-stack-sm | comfortable: 8pxcompact: 6pxchanges with density | Spacing | |
| size.target.comfortable--smbk-size-target-comfortable | 44px | Size |
Manifest
{
"name": "ruler",
"block": "smbk-ruler",
"summary": "A width ruler for a preview: the layout states and breakpoints along one band, the screen widths, and a handle for the width now.",
"adapt": "css",
"axes": {},
"flags": {},
"parts": [
"scale",
"band",
"zone",
"break",
"tick",
"handle",
"widths",
"width",
"now",
"controls",
"fit",
"hide"
],
"figmaNodeId": "7417:374"
}