Skip to content

Refresh Button with the SegmentedControls rim light (4.11.0) - #182

Merged
tmccoy14 merged 13 commits into
mainfrom
button-visual-refresh
Oct 6, 2026
Merged

tmccoy14 merged 13 commits into
mainfrom
button-visual-refresh

Conversation

@tmccoy14

@tmccoy14 tmccoy14 commented Oct 2, 2026 •

Copy link
Copy Markdown
Contributor

Why

Button was the component that made Equality, and Governance Studio especially, look dated. Primary and danger were flat, heavy blocks, and tertiary looked like a form field. This brings Button in line with the SegmentedControls active segment. The only new values are control-surface tokens in theme-base.css, built from existing Pinwheel colours: edge colours, sheens, shadows and an easing curve. They're shared with SegmentedControls and the selected DateRangePicker day.

No props, variants or sizes change, and consumers don't need to change any code, so it's released as 4.11.0. Components built on Button pick up the new look: RadioDropdown, FilterDropdown, SortSelector, DateRangePicker and Pagination.

What

Filled variants (primary, danger, warning, secondary):

  • The rim light from SegmentedControls: a sharp 1px highlight (inset 0 1px 0, no blur) along the top edge.
  • A soft sheen beneath it: 10% white fading out by 35%, plus a 6% shade at the bottom, over the existing fill token.
  • A subtle drop shadow, faintly tinted with the button's own colour in light mode.
  • No border, so the rim sits on the edge rather than 1px inside it. Filled buttons get 1px more horizontal padding, so every size stays exactly as wide as before.

Tertiary:

  • White in light mode and near black in dark, both built on button-tertiary-fill.
  • The same sheen and lift as the filled variants, so it reads as a raised button, not a field.
  • A translucent edge (control-edge: text-primary at 16%, 22% in dark mode) replaces the grey border token. The SegmentedControls track uses the same edge. In dark mode the top of that edge is brighter (34%) and acts as the rim light.

Every variant except link and navigation:

  • Pressed state: the lift and rim give way to an inset shadow.
  • Motion: transitions use SegmentedControls' 250ms easing, animate only colour, shadow and underline (not transition-all), and respect reduced motion.
  • Hover is gentler. In dark mode, filled variants lighten by 6% instead of darkening, and tertiary hover is visible. Hover only applies on devices that can hover, so a tapped button doesn't stay highlighted on a touch screen.
  • Icon gap: 6px, up from 4px. Buttons with both an icon and a label are 2px wider.
1-button-variants 3-button-in-context

Visual comparison

Main vs this branch has side-by-side screenshots of origin/main and this branch, in light and dark mode. It covers every Button variant and state (rest, hover, pressed and keyboard focus), SegmentedControls, an open DateRangePicker with a range selected, the danger Toast, and the components built on Button. The link is only open to people it has been shared with.

The docs have no danger toast with an action, so for those two shots the with-action toast's variant class was swapped to danger in the page. Each side's CSS is real.

Accessibility

Contrast was measured from rendered pixels behind the label:

Dark, at rest Dark, hovered Light, at rest Light, hovered
Primary 5.12:1 4.56:1 5.12:1 7.25:1
Danger 5.21:1 4.62:1 5.21:1 7.38:1
  • The sheen stops at 35% so it never lightens the area behind the label. At 60% it pushed dark-mode hover below 4.5:1.
  • Tertiary's edge is about 1.65:1 against the page in light mode and 1.8:1 in dark. The label identifies the button, so WCAG 1.4.11 doesn't require a boundary; the edge is there so the button doesn't blend into the page.
  • The focus ring is unaffected. Every shadow uses Tailwind shadow utilities, which compose with focus-ring. A raw box-shadow would replace it, and button.module.css carries a comment warning about that.

Already broken on main, not fixed here: warning has white text on yellow at 2.41:1. Fixing it means changing button-warning-text in Pinwheel. The comment above .button--warning also says its text is dark, which is no longer true.

Other files

  • badge.module.css and date-range-picker.module.css: the Badge close button and the calendar day cells are built on tertiary, so they set bg-none to opt out of the sheen. Their hover colours are also limited to devices that can hover.
  • segmented-controls.module.css: the track uses tertiary's translucent edge, and the active segment uses the shared raised shadow. That tints it faintly in light mode and deepens its shadow in dark.
  • button.mdx: typo fixes, the variants intro moved into the overview, and disabled warning and link examples.
  • date-range-picker.module.css: a selected day matches the new filled Button: no border, rim light, sheen, shadow and pressed state.
  • toast-components.module.css: a danger toast outlines its ToastAction with an inset ring instead of a border, so the action is as wide as in other toasts. Hovering the toast turns the action the danger button colours instead of raw red palette classes.

For consumers

  • Overriding Button's background with bg-none! next to a bg-*! colour class gets merged away by tailwind-merge in cn(). Use [background-image:none]! instead.
  • Overriding a filled button's border: filled buttons have no border now, so a border colour class alone won't show. Add border as well.
  • Studio: DateTimePicker forces the field look on its trigger, and is updated to also switch off the sheen and lift. That change lives in the guardian repo.

🤖 Generated with Claude Code

tmccoy14 and others added 6 commits October 1, 2026 21:09
…nButton

- DateRangePicker: day cells drop the bordered-box grid. Selected days use the primary
  button colours (dark mode was 2.3:1, now meets AA), in-range days the primary badge tint,
  and hover the IconButton lilac tint, through its own hover rule rather than Button's.
- DropdownMenu: checked radio and checkbox items use the primary fill, so RadioDropdown and
  FilterDropdown match Checkbox and RadioGroup.
- RadioGroup: the checked dot is back to 10px.
- IconButton: md and lg corners match Button's 6px.
- ToastAction is built on Button instead of a copy of its styles.
- Badge's close button opts out of shadows and hover a future tertiary Button could add.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ames

A selected range passed the whole CSS module object to cn, which added every
key in it as an unhashed class. Left over from #51 removing popover-trigger--active.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… dark mode

Lightening the primary fill by 14% put the day's white text at about 4.0:1.
6% lightens it visibly and keeps 4.74:1.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Filled variants get a soft top sheen, a tinted lift and no visible border.
Tertiary gets the same sheen and lift with a translucent edge, staying near
black in dark mode. Every variant except link and navigation has a pressed
state, transitions use SegmentedControls' easing and respect reduced motion,
and the icon gap is 6px.

Dark-mode hover lightens filled variants by 6% so white labels stay above
4.5:1, and the sheen ends at 35% so it never sits behind a label.

Badge's close button and DateRangePicker's day cells, both built on tertiary,
opt out of the sheen; a selected day matches the new filled Button.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmccoy14 tmccoy14 self-assigned this Oct 2, 2026
@Shrinks99
Shrinks99 self-requested a review October 2, 2026 18:35
Following design review, filled variants get the sharp 1px rim light of the
SegmentedControls active segment over a softer sheen, with a subtler drop
shadow. They lose their border so the rim sits on the edge, with 1px more
padding so they stay the same size. Tertiary is white in light mode and near
black in dark, where its brighter top edge is the rim light.

A selected DateRangePicker day matches, and a danger toast keeps the border
it gives its ToastAction. Released as a minor version: no props, variants or
sizes change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmccoy14 tmccoy14 changed the title Refresh Button with a sheen, lift and softer tertiary (5.0.0) Refresh Button with the SegmentedControls rim light (4.11.0) Oct 2, 2026
@matthewmr-eqty

matthewmr-eqty commented Oct 5, 2026 •

Copy link
Copy Markdown
Contributor

@tmccoy14 IMO it certainly looks nice.

@tmccoy14 and @Shrinks99 one ask and and two questions.

Lift the filled-button shadow into a shared token. The same shadow is now written out twice: button.module.css lines 23–24 and date-range-picker.module.css lines 96–97. Something like this, in the @theme inline block of theme-base.css:

--shadow-control-raised: 0 1px 2px rgb(0 0 0 / 0.18),
0 2px 6px -2px color-mix(in oklch, var(--mix-color, transparent) 25%, transparent),
inset 0 1px 0 rgb(255 255 255 / 0.25);
--shadow-control-raised-dark: 0 1px 2px rgb(0 0 0 / 0.45), inset 0 1px 0 rgb(255 255 255 / 0.25);
Both then use @apply shadow-control-raised dark:shadow-control-raised-dark;. It has to be in the inline block so var(--mix-color) resolves on each button. There's no visual change. Button's other new values are each used once, so they can stay where they are.

Should SegmentedControls move onto it here? Its shadow would change from 0.2 to 0.18 in light mode and from 0.2 to 0.45 in dark mode.

Does the fill-coloured glow need to stay? Without it, dark mode doesn't need its own token.
Bringing controls onto #41's elevation scale is a follow-up ticket

tmccoy14 and others added 3 commits October 6, 2026 13:29
# Conflicts:
#	packages/ui/CHANGELOG.md
#	packages/ui/package.json
Button and the selected DateRangePicker day wrote the same shadow out
twice. Both now use shadow-control-raised and its dark counterpart.
No visual change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…ol shadow

The active segment now shares shadow-control-raised with Button and the
selected DateRangePicker day. Its drop shadow goes from 0.2 to 0.18 in
light mode and 0.45 in dark, which screenshots show no visible change
against the black dark-mode track.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmccoy14

tmccoy14 commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor Author

@matthewmr-eqty thanks, all three addressed:

Shared token. Lifted into --shadow-control-raised / --shadow-control-raised-dark in the @theme inline block of theme-base.css, and Button and the selected DateRangePicker day both use @apply shadow-control-raised dark:shadow-control-raised-dark; (6597bb4). The built CSS is unchanged apart from whitespace.

SegmentedControls. Moved onto the token too (376aa9c). I screenshotted it before and after at 3x and diffed them: dark mode is pixel-identical, since the track behind the indicator is pure black, and light mode differs by at most 4/255 on a channel. Worth remembering for MIC-80: if the dark track ever gets lighter, the 0.45 shadow will start to show. The indicator doesn't set --mix-color, so it gets no glow.

segmented-controls-comparison

Copy link
Copy Markdown
Member

Shadows not showing in dark mode is natural and we should come up with some more component shading tokens / a better system than elevation to account for it. My going thought for interactive elements like this is that rim lights help distinguish buttons and clickable stuff in dark mode, and shadows do the same for light mode.

@matthewmr-eqty matthewmr-eqty left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Looks great. The next pass of moving shadow colours into Pinwheel can happen in MIC-80.

tmccoy14 and others added 3 commits October 6, 2026 14:13
…hat can hover

Button's edge colours, sheens, shadows and easing move into theme tokens
that SegmentedControls and the selected DateRangePicker day now share, so
each value is written once. Hover colours on Button, DateRangePicker days
and Badge's close button only apply under (hover: hover), so a tapped
control no longer stays highlighted on a touch screen.

The filled-button 1px padding is a variable the size rules read, so it no
longer depends on rule order. A selected day gets the filled pressed
state, and the SegmentedControls track uses tertiary's translucent edge.

The danger Toast action uses an inset ring so it stays as wide as in
other toasts, and its hover takes the danger button tokens instead of
raw red palette classes. A :focus rule that could never apply is gone.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…led variant

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@tmccoy14

tmccoy14 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@tmccoy14

tmccoy14 commented Oct 6, 2026

Copy link
Copy Markdown
Contributor Author

@Shrinks99 Agreed, and this PR mostly follows that already: dark mode leans on the 1px rim (filled buttons, the active segment, and tertiary's brighter top edge), and light mode leans on the drop shadow and tint, which is dropped in dark.

One nuance for MIC-80: dark drop shadows aren't fully invisible. On background-base they disappear, but on background-overlay and background-top (dialogs, popovers) they still separate a button slightly. So I'd treat the rim as the main cue in dark and the shadow as the main cue in light, rather than one each.

For the token system, rather than elevation levels, how about tokens by role (rim and lift) whose values are set per theme next to the colour tokens? Components would use one class instead of pairing shadow-control-raised dark:shadow-control-raised-dark in four places, and we could tune rim strength per theme, e.g. stronger in dark. Worth folding into MIC-80.

@tmccoy14
tmccoy14 merged commit f1f63d7 into main Oct 6, 2026
1 check passed
@tmccoy14
tmccoy14 deleted the button-visual-refresh branch October 6, 2026 18:25
@Shrinks99

Copy link
Copy Markdown
Member

Didn't have a chance to catch this before it got merged, but the rim light on the tertiary button style has a different cut-off effect compared to the rest of the styles, presumably due to the border?

Screenshot 2026-10-06 at 8 41 17 PM

There's a few different ways we can fix this, would address when we get to tokenizing this @matthewmr-eqty

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants