Interaction blueprint
Count change flip
A numeric flip expresses a discrete change without asking the user to hunt for it.
Build it
Choose a target and adapt the source to your interface.
<div class="count-demo">
<span class="count" role="status" aria-label="12"
><span class="count__old" aria-hidden="true">12</span
><span class="count__current" aria-hidden="true">12</span></span
><button class="count-control" type="button" aria-label="Increase count">
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M12 5v14M5 12h14" />
</svg>
</button>
</div>
<script>
const button = document.querySelector('.count-control');
const output = document.querySelector('.count');
const old = output.querySelector('.count__old');
const current = output.querySelector('.count__current');
let count = 12;
button.addEventListener('click', () => {
old.textContent = String(count);
current.textContent = String(++count);
output.setAttribute('aria-label', String(count));
for (const animation of output.getAnimations({ subtree: true }))
animation.cancel();
if (matchMedia('(prefers-reduced-motion: reduce)').matches) return;
old.animate(
[
{ opacity: 1, transform: 'rotateX(0deg)' },
{ opacity: 0, transform: 'translateY(10px) rotateX(-88deg)' },
],
{ duration: 120, easing: 'ease-in' },
);
current.animate(
[
{ opacity: 0, transform: 'translateY(-18px) rotateX(88deg)' },
{ opacity: 1, transform: 'rotateX(0deg)' },
],
{
delay: 100,
duration: 160,
fill: 'backwards',
easing: 'cubic-bezier(.22,1,.36,1)',
},
);
});
</script>
<style>
.count-demo {
display: inline-flex;
align-items: center;
gap: 28px;
color: var(--stage-ink, var(--ink, #252b34));
}
.count {
position: relative;
display: inline-grid;
place-items: center;
min-width: 2.4ch;
height: 1.3em;
font: 500 48px/1.2 system-ui;
font-variant-numeric: tabular-nums;
letter-spacing: -0.03em;
perspective: 240px;
}
.count span {
grid-area: 1 / 1;
backface-visibility: hidden;
transform-origin: center bottom;
}
.count__old {
opacity: 0;
}
.count-control {
display: grid;
place-items: center;
width: 44px;
height: 44px;
padding: 0;
border: 1px solid var(--stage-grid, var(--line, #cbd0d7));
border-radius: 50%;
background: transparent;
color: inherit;
cursor: pointer;
}
.count-control svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 1.7;
stroke-linecap: round;
}
.count-control:hover {
background: color-mix(in srgb, currentColor 6%, transparent);
}
.count-control:focus-visible {
outline: 2px solid var(--stage-link, var(--brand-deep, #175bc5));
outline-offset: 4px;
}
@media (prefers-reduced-motion: reduce) {
.count span {
transform: none;
}
}
</style>Animation breakdown
Frame phases, motion intent, implementation constraints and accessible alternatives.
Rest: current numeral visible.
0–120ms: old numeral rotates out on the X axis and fades.
100–260ms: new numeral rotates in from above.
260–280ms: hold.
Motion intent
A numeric flip expresses a discrete change without asking the user to hunt for it.
This duration leaves enough time to read the state change while keeping the interface responsive.
The fast acceleration and long ease-out make the state feel responsive while giving the final pixels time to settle.
Motion specification
- Trigger
- A count changes by one.
- Timing
- 280ms
- Easing
- cubic-bezier(.22, 1, .36, 1)
Animated properties
Implementation notes
- Keep 12 and 13 aligned to the same visual anchor.
- Drive the transition from semantic state, not a timer, when the trigger is interactive.
- Use transform, opacity and avoid layout work inside the animation loop.
Reduced motion
Swap to the final state instantly, preserving colour, label, and accessible state.
- Use a minimum 44 by 44 pixel interactive target.
- Expose the 12 and 13 states with aria-pressed, aria-expanded, or an updated accessible label.
- Keep visible keyboard focus independent from the animation.
Avoid
- Do not add bounce unless a real object pickup or spring settle is part of the interaction.
- Do not hide the final state from assistive technology.
- Do not make the user wait for animation before the underlying state updates.