Range picker

The segmented control over the trends chart: 3 months, 6 months, 12 months. A selection and not an action, which is why it is not three buttons. It is also the one component in the system with a form nobody named until the browser was asked what it actually renders.

Molecule3 grey pages9 segments1 axis, 4 values4 states, both themesswitches the screen

Anatomy

A group and its segments: two levels and nothing else. Everything below is drawn by design/system/components/range-picker.css through the same entry point a product screen loads, so this page cannot drift from the screens.

Variants and sizes

One axis and no size axis at all: a segment's width comes from the group and its height is the 44px floor everywhere. But the axis is a cross rather than a list, and drawing it as a list is exactly how the fourth form went unnamed for a stage. Availability has two values and selection has two, so there are four cells, and all four render in the product today.

Availability × selection

Availability is carried by COLOUR, selection by WEIGHT. Two axes on two different properties is what keeps both readable when they are true at once

Tokens Where it stands
Available, not selected [aria-pressed="false"] --bg-surface
--line-control
--text-body
4 of the 9. The offer: a range you could look at
Available, selected [aria-pressed="true"] --bg-selected
--line-selected
--text-action
weight 600
2 of the 9. The current selection, which is the second of the four jobs the One Voice Rule lets petrol do
Unavailable, not selected [disabled] --bg-recessed
--line-divider
--text-muted
2 of the 9, both on the Pro gate. Repainted rather than faded: there is no opacity token in this system
Unavailable, selected [disabled][aria-pressed="true"] --bg-recessed
--line-divider
--text-muted
weight 600
1 of the 9, on the Pro gate. The form nobody named. Grey like its neighbours and bold like a selection, because behind the gate both facts are true: the control cannot be used, and 3 months is still the range the empty frame below is drawn for

The fourth cell was found by measuring, not by reading. The census walked the rendered product and reported that on history-trends-locked the "3 months" segment is disabled AND bold: [aria-pressed="true"] sets the weight, [disabled] overrides colour, fill and border and leaves the weight alone, and the two attributes sit at the same specificity so source order decided it. Nobody chose that. Step 2 decided it is the right answer anyway and that it becomes a declared form, so the rule now exists at its own specificity with its reason attached. Not one pixel moves: the declaration writes down what the cascade was already producing.

Width is deliberately not an axis, and neither is count. Three segments is what the product has, and a fourth would fit by arithmetic alone; what it would cost is legibility at 360px, where the three already measure 99px each on the coloured Your trends screen as it renders today.

2026-08-18: three buttons became one segmented control

The founder, on the coloured Pro gate at a desktop width: “я не понимаю, что это у нас тут вообще огромные кнопки зачем-то”. They were three separate bordered buttons, each flex: 1 of an uncapped pane. Measured before: 250.67px a segment at a 1440 window, three 250 by 44 slabs across the top of the screen - heavier than the chart whose range they choose, and heavier than the primary action further down the page.

A segmented control is one object with three parts, and that is the whole change. A recessed track, three transparent segments inside it, and no border on any of them: the three borders are what made the group read as three controls, and with the track carrying the shape the selection is a figure on a ground instead of an outline among outlines. Measured after: the track is 320 by 48 from a 520px window up, and 100% of the column below that, so a segment never goes under the 44px floor on a phone. It was 384 by 52 until 2026-08-19; see the cap below.

The cap is 20rem and it is arithmetic, not taste. “12 months” is the widest label; at --type-body with 16px of side padding a segment needs about 102px, so three of them plus the track's own padding need roughly 314. 20rem is 320, the tightest register that still clears it. flex: 1 stays and its old reason with it: the three segments divide the track, so “12 months” and “3 months” are the same size and the group reads as one thing.

It came down from 24rem on 2026-08-19, on the founder's decision: "кнопки переключения очень большие". At 384 by 52, alone on a line across a 1280 screen, it was the largest object above the fold on a screen whose subject is a number. Two values moved and neither is the tap target: the cap 24rem to 20rem, so a segment is 104 rather than 126, and the track's padding --space-4 to --space-2, so the control is 48 tall rather than 52. min-height: var(--tap) on the segment did not move and may not - 44px is the floor a pointer target may not go under, and shrinking a control by shrinking what a thumb has to hit is the one way to get this wrong. The control lost 64px of width and 4px of height; the thing a person presses is exactly the size it was.

What did NOT change is the selection, and that was deliberate. The chosen segment keeps --bg-selected and --text-action: petrol on the current selection is the second of the four jobs the One Voice Rule allows it, and lifting the selection onto a white pill with a shadow - the other common way to draw this control - would have quietly spent that decision. Only the border went, on every state, because there is no border on a segment any more. The hover is the ink coming up instead of an edge appearing.

When to use it

A person meets this on Your trends, which is a Pro screen, and most of them meet it behind the gate first: the canonical person in this product is on Free, so the locked view is the only view of that screen she can have. That is why the disabled forms matter more here than they would anywhere else. They are not an edge case, they are the majority of the renders.

Its job is to re-frame what is already on the screen, never to fetch a different screen. "How your monthly total has moved over time. Nothing to act on here, just the shape of it." The picker changes the window on that sentence and nothing about the sentence's job, which is why it is a selection and not an action, and why choosing a range is not something a person can get wrong.

The rule, and the anti-rule

Behind the gate, selection survives

All three grey, one of them bold. The colour says the control is not yours yet; the weight says which range the frame under it is drawn for. A person on Free is looking at their own five category labels on an empty chart, and "3 months" is what that frame means.

Never flatten it to one grey

Resetting the weight would have been the tidy fix, and it deletes information: three identical grey segments say the frame belongs to none of them. Keeping the petrol tint instead would be the opposite mistake, because a control that looks live and does nothing is worse than one that says so.

Use a range picker

Two or more mutually exclusive views of the same data, all of them visible at once, none of them destructive, and the current one legible without a tap. The person is adjusting a window, not making a decision.

Use a button instead

The moment a choice does something rather than re-framing what is already on screen, it is a button: it commits, it can fail, and it gets the one filled accent of its zone. "Export as CSV" sits two blocks below this picker on the same screen and is deliberately not a fourth segment. And a choice that changes which screen you are on is neither: that is the tab bar, which is navigation and lives in the chrome.

Limits

U13, context. PRESS. A segment goes down by one nudge while it is held. The disabled segment does not move: it is the Pro range on a Free account, and moving it would answer a press the product will not honour. Everything in this product that moves travels exactly one --nudge, which is 2px, and nothing anywhere travels two. Hover and release run at --dur-state (150ms) on --ease-arrive; a press runs at --dur-press (90ms), because it answers a finger that is already down. The full language and the live specimens are on Motion; the table with sources is Usage rules.

It switches the screen, since 2026-08-19

The other half of the founder's sentence was "и не работают", and it was true: three segments that had never moved a pixel on the screen behind them since the wireframe. A control that does nothing is a defect and not a style, so this is the fix, and none of it is in this file.

The data lives on the button that selects it. Each segment carries data-points and data-scale for the range it names, and design/system/behaviour.js copies them onto every chart in the screen and asks it to redraw from its own two attributes. Nothing here knows the geometry, and nothing about the picker's own CSS changed to make it work.

What switches besides the picture is declared in the markup, not built by the script. Any element in the screen carrying data-view names which ranges it belongs to, as a space-separated list of the same keys the buttons use. That is how a sentence written for three months and a row that only moved over twelve stay product copy with an owner in voice/docs/microcopy.md, rather than strings a script assembles. Buttons are skipped: a data-view on a button names what it SELECTS, not where it appears.

With no script the screen is the three-month view, whole and correct, and this control looks pressed on it because the markup says so. That is the same bargain the chart makes with its authored path. The disabled form behind the Pro gate carries no data at all, and the script leaves it alone: a control that cannot be used must not move anything.

States

Four states, and they exist because a segment is a real button. Each one is a token override rather than a style of its own: no hex and no number appears inside :hover, so the dark theme costs three lines in tokens.css rather than a rewrite here.

Hover is declared before selection, and that order is the decision. The two rules are the same specificity, so the later one wins on a segment that is both, and the selected segment therefore does not repaint under the pointer. That is right rather than accidental: the pointer is asking "what happens if I press this", and for the current range the answer is nothing you have not already got.

Rest and disabled, live

The second group is genuinely disabled, by the attribute rather than by a class the product does not carry. Tab through both: the first group takes three stops and the second takes none, which is the attribute doing a job no class could do.

The four, in both themes

The states are described, not photographed. Founder's decision, 2026-08-11: the snapshot strip went from every page in this kit, 120 images across 18 pages. They were framed as measurements rather than illustrations, and that framing was honest, but the bill came due all session: a value moves, and every picture of it is quietly stale until somebody re-shoots it. A stale picture is worse than no picture, because it looks checked. What is left is what a picture never carried anyway: the state, and the token it reads. The resting component is live and interactive above, so hover it, tab to it, and read the value off the real thing rather than off a photograph of it.

Restlight
--line-control, --bg-surface, and --bg-selected on the current one
Restdark
the same roles, the other half of each pair. The tint goes from a near-white petrol to a deep one, because a tint has to sit on its own ground
Hoverlight
--bg-hover, --line-control-hover. The edge must STRENGTHEN: 3.46:1 at rest, 5.78:1 here
Hoverdark
the fill goes LIGHTER here and darker in the light theme: a hover moves away from its ground, and the ground swapped ends
Focus visiblelight
--color-focus, 6.23:1 as a line. focus-visible and not focus
Focus visibledark
--color-focus, 6.82:1. The ring is the only thing telling a keyboard user which of three identical boxes they are on
Disabledlight
--bg-recessed, --line-divider, --text-muted, and weight 600 on the first
Disableddark
no opacity token: all three paints land on roles that already exist, so inventing one would either move pixels or sit unread
Retake, and this is the whole recipe, because there is no node Playwright in this repo: serve the repo, then for theme in (none, data-theme="dark" on <html>): goto /design/kit/range-picker.html move the pointer away -> screenshot the FIRST .range in #live-rest -> range-picker-rest-<theme>.png hover #shot-rest -> screenshot the FIRST .range -> range-picker-hover-<theme>.png focus #shot-pressed, press Tab, assert :focus-visible -> range-picker-focus-<theme>.png blur, screenshot the SECOND .range in #live-rest -> range-picker-disabled-<theme>.png The disabled shot is the one to check twice: it is the only picture in the system of two axes crossing, and the thing to look for is that exactly one of the three is bold.

Active is not shown and that is not an omission. The component declares none: pressing a segment selects it, so what a person sees on release is the selected form, and a separate pressed paint between the two would be a third appearance in a quarter of a second.

The technical half

ReadsWhich tokenGrowing from
the resting fill--bg-surface--paper
the resting edge--line-control--edge. A segment is identified BY its edge, so it owes 3:1 and gets 3.46:1
the resting label--text-body--slate
the selected fill, edge and ink--bg-selected, --line-selected, --text-action--petrol-tint and --petrol twice: three roles on two values, three thresholds
hover fill and edge--bg-hover, --line-control-hover--panel, --muted
the focus ring--color-focus--petrol
disabled, in both forms--bg-recessed, --line-divider, --text-mutedthree roles that already existed
the gap and the block spacing--space-8, --space-24primitive: a spacing has nothing to override
corner and floor--radius-sm, --tapprimitive, straight

Lives in design/system/components/range-picker.css. Stands on 3 grey pages and all three of their coloured copies: Your trends, the Pro gate, its loading state. It has no class name of its own and is selectable only as .range button, which the census filed as a control with no family. Step 6 renamed what the map listed and this is not on it, so the finding is still open and still not corrected here.

<div class="range" role="group" aria-label="Choose a time range"> <button type="button" aria-pressed="true">3 months</button> <button type="button" aria-pressed="false">6 months</button> <button type="button" aria-pressed="false">12 months</button> </div> <!-- behind the Pro gate: disabled on every segment, aria-pressed unchanged --> <div class="range" role="group" aria-label="Choose a time range, part of Tendd Pro"> <button type="button" aria-pressed="true" disabled>3 months</button> ... </div>