Skip to content
milan.swiss Halbton v0.1.0
Concept project

Foundations

Motion

Two durations and one curve. Halbton's state changes are small enough that a longer transition reads as lag rather than polish — and the whole system resolves to instant under reduced-motion.

The tokens

Two durations and one easing. Halbton's state changes are small enough that a longer curve reads as lag rather than polish.

Three values. A system this small does not need a motion library.
Token Value Use
--ht-dur-fast 120ms Colour changes on hover and active. Fast enough to feel like a response rather than an animation.
--ht-dur-base 180ms Something entering or leaving the page — a dialog, a drawer, a toast.
--ht-ease cubic-bezier(0.2, 0, 0, 1) One curve for everything. It starts immediately and decelerates, which is what makes a change feel caused by the click.

Try them

Hover each square. The difference between 120ms and 180ms is small and that is the point — the first feels like the surface responding, the second like something moving. Above roughly 250ms a hover state starts to feel broken when you move quickly across a dense page.

120ms
180ms
400ms — too slow

Focusable with the keyboard as well as hoverable, because a demonstration only reachable with a pointer is not a demonstration for everyone.

What is never animated

  • The theme flip. Transitioning colour on data-theme produces a visible sweep on every toggle and a flash of half-applied colour on first paint. It is instant, and the tokens are declared with transition-property: none to keep it that way.
  • Anything on page load. No entrance animations, no staggered reveals. A tool should be usable the moment it renders.
  • Position. Nothing lifts, scales or translates on hover — see the half-step argument on Elevation.

Reduced motion

Every animation in the system resolves to its end state, not to a shorter version of itself. A 60ms spin is not a courtesy; it is the same problem, faster.

Spins normally; a static ring under reduced motion.
the whole reduced-motion policy
@media (prefers-reduced-motion: reduce) {
  .ht-btn {
    transition: none;
  }

  /* Resolved, not shortened. The ring stays visible so the
     button still reads as busy — the meaning survives, the
     movement does not. */
  .ht-btn__spinner {
    animation: none;
    border-right-color: currentColor;
  }
}

The pattern generalises: ask what the animation was communicating, then deliver that without movement. A spinner communicates "busy" — a static ring plus aria-busy does too. A dialog transition communicates "this arrived over the page" — a border and a scrim do too.

When to add motion at all

Only to explain a change the user caused. If something appears, moves or disappears and the user did not act, it does not need easing — it needs removing.