Skip to content
UI Elements & ComponentsAchilleselement

Weighted Press

A button whose six states were designed together, with an asymmetric press that gives the control apparent mass.

Built here · no external address

Achilles: Supreme. Highest learning value in the canon. Used sparingly.

Exhibit case

The component itself, in every state it can hold.

Arming the exhibit…

Why it matters

Almost every interface ships a button whose states were designed one at a time: a hover borrowed from a library, a focus ring inherited by accident, a press that either does nothing or snaps back in 150ms. The result is a control that behaves differently at every touchpoint and feels weightless.

This one treats the six states as a single system with a shared vocabulary — hairline at rest, border commitment on hover, ring on focus, compression on press — and makes the press asymmetric: 100ms in, 500ms out. That single asymmetry is what reads as mass. Physical objects compress instantly under force and recover slowly; matching that is the difference between a button that acknowledges input and one that feels like a thing.

Key techniques

  • Six states enumerated as one design pass, rendered side by side in the matrix above so no state can drift.
  • A force prop that renders hover, focus, and press states without needing the pointer — states that are otherwise impossible to hold still and review.
  • Asymmetric press timing: 100ms compress, 500ms release, both on the house curve.
  • Scale compression at 0.97, applied on transform only, so nothing reflows.
  • :focus-visible for the ring, never :focus — pointer users are not punished with a ring they did not ask for.
  • Loading is a real state, not a swap: aria-busy, dimmed label, hairline sweep, and the button keeps its exact dimensions.

What to steal

The transferable part.

The asymmetry. Compress fast, release slow. transition-duration: 100ms on the pressed selector and 500ms on the base is one line of CSS and it is the entire effect.

The forced-state prop. Any component with hover, focus, or press states needs a way to render those states statically, or they will never be reviewed. Add a force flag used only by your own documentation surface, and the state matrix maintains itself.

Loading that keeps its box. Measure the label, keep the width, dim the text, and put motion in a hairline. A button that resizes when it starts working moves the layout under the cursor that just clicked it.

What not to copy

Context-specific or costly.

Do not add the asymmetry to a control that fires repeatedly — a stepper, a volume nudge, a paginator. A 500ms release on a button pressed five times in a row queues visible lag behind the user's intent. Mass is appropriate for consequential actions; rapid controls should snap.

Do not extend the compression past 0.97. Anything deeper stops reading as force and starts reading as a size change, and text inside the button becomes visibly blurry mid-transition on non-integer scales.

Inclusion rationale

This is the smallest artifact in the canon that demonstrates the whole thesis: that quality lives in the states nobody screenshots. It is the reference implementation every other interactive entry here borrows its timing from, and the state matrix pattern it introduces is reused by every component study that follows. Achilles because the lesson transfers to literally every interactive control, at a complexity of two.

Technical register

Performance

Transform and opacity only. No layout property is touched in any of the six states, so every transition stays on the compositor. The component ships as 1.1kB of markup and class names with zero animation library — the entire behaviour is CSS transitions on state classes.

Mobile

Hover states are gated behind @media (hover: hover) so touch devices do not get the sticky-hover artefact where a tapped button stays lit. Minimum target is 44px in both axes at the default size. The press compression is more important on touch, not less: without a cursor, compression is the only feedback confirming the tap landed.

Accessibility

Focus ring is :focus-visible, 2px at 3px offset, and is never removed at any breakpoint or in any variant. Loading sets aria-busy="true" and keeps the accessible name stable so a screen reader does not announce a new button mid-request. Disabled uses the real disabled attribute — not aria-disabled with a click handler — and holds 40% opacity, which keeps the label above 3:1 against the surface. No state is signalled by colour alone: hover changes border weight, focus adds a ring, press changes size.