Desktop Material

Material ripple state layer and theme reveal pulse

Desktop Material mirrors the Desktop Material v2.dc.html prototype's two app-wide motion primitives:

Both consume the global dmRipple / dmReveal @keyframes declared in app/styles/material/_motion.scss (pre-staged for this feature) and are suppressed under reduced motion.

Behavior

Ripple

Theme reveal

Styling

app/styles/ui/_ripple.scss styles the two transient elements and gives .button-component a positioning context. Clipping to the pill silhouette is free: .button-component already sets overflow: hidden via the ellipsis mixin, and overflow: hidden respects the button's border-radius, so the scaling circle never escapes rounded corners. The partial is registered in app/styles/_ui.scss.

Reduced motion

prefersReducedMotion() returns true when either the OS prefers-reduced-motion: reduce media query matches or the app's own data-dm-motion="reduced" appearance preference is set on <body>. In that case:

Because AppTheme.applyAppearance() writes data-dm-motion before the reveal is evaluated, switching the app to reduced motion suppresses the pulse on the same interaction. As a defense in depth, _ripple.scss and the global rules in _material-shell.scss also force any such animation to an instant fade.

Failure modes

Scope boundary

Controls that do not render through the shared Button — notably the toolbar ToolbarButton/ToolbarDropdown family used by the top-level menu bar — do not yet ripple. Wiring attachRipple into those toolbar controls is a follow-up owned by the toolbar surface. That band is now behind Settings → Appearance → Show the classic toolbar (shipped on) since the MD3 shell landed, which narrows how much of the interface the gap covers but does not close it — the follow-up still stands for as long as the band ships.

The shell added a second family in the same position: Md3IconButton, Md3TonalButton and Md3GhostButton in app/src/ui/md3/md3-primitives.tsx render their own <button> rather than the shared Button, and none of them calls attachRipple today. They carry the contract's own :hover and :focus-visible treatments, so they are not unstyled — but they do not ripple, and wiring attachRipple into the three MD3 primitives is the same follow-up, now owned by the shell surface.

Verification

app/test/unit/ripple-motion-test.tsx covers: