Mira Holt
Motion designer
Mira Holt
Motion designer
import { useState } from 'react';
const BASE = 1284;
const DIGITS = Array.from({ length: 10 }, (_, i) => i);
const css = `
.card-follow { box-sizing: border-box; width: 240px; padding: 16px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-follow__top { display: flex; align-items: center; gap: 12px; }
.card-follow__avatar { position: relative; flex: none; width: 52px; height: 52px; }
.card-follow__face { position: absolute; inset: 5px; display: grid; place-items: center; border-radius: 50%; background: light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font-size: 14px; font-weight: 700; }
.card-follow__ring { position: absolute; inset: 0; width: 52px; height: 52px; transform: rotate(-90deg); }
.card-follow__ring circle { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: stroke-dashoffset 0.6s cubic-bezier(0.5, 0, 0.2, 1), opacity 0.15s; }
.card-follow--on .card-follow__ring circle { stroke-dashoffset: 0; opacity: 1; }
.card-follow__name { margin: 0; font-size: 14px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-follow__role { margin: 2px 0 0; font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
.card-follow__stats { display: flex; gap: 18px; margin: 14px 0; font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
.card-follow__stats b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 14px; font-weight: 600; }
.card-follow__count { display: flex; height: 18px; overflow: hidden; line-height: 18px; font-variant-numeric: tabular-nums; }
.card-follow__reel { display: flex; flex-direction: column; transition: transform 0.5s cubic-bezier(0.3, 1.3, 0.5, 1); }
.card-follow__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.card-follow__stats > span { position: relative; }
.card-follow__btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
height: 34px;
border: 0;
border-radius: 10px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.25s, color 0.25s, transform 0.15s;
}
.card-follow__btn:active { transform: scale(0.97); }
.card-follow__btn:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.card-follow--on .card-follow__btn { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); }
.card-follow__tick { width: 0; height: 14px; overflow: visible; transition: width 0.25s; }
.card-follow--on .card-follow__tick { width: 14px; }
.card-follow__tick path { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.3s ease 0.15s; }
.card-follow--on .card-follow__tick path { stroke-dashoffset: 0; }
@media (prefers-reduced-motion: reduce) {
.card-follow__ring circle, .card-follow__reel, .card-follow__tick, .card-follow__tick path { transition: none; }
}
`;
// A number drawn as rolling digit reels; commas stay put.
function Count({ n }: { n: number }) {
return (
<span className="card-follow__count" aria-hidden="true">
{n
.toLocaleString('en-US')
.split('')
.map((c, i) =>
c === ',' ? (
<span key={i}>,</span>
) : (
<span key={i} className="card-follow__reel" style={{ transform: `translateY(${-Number(c) * 18}px)` }}>
{DIGITS.map((d) => (
<span key={d}>{d}</span>
))}
</span>
),
)}
</span>
);
}
export function FollowCard() {
const [on, setOn] = useState(false);
const followers = BASE + (on ? 1 : 0);
return (
<>
<style>
{css}
</style>
<article className={`card-follow${on ? ' card-follow--on' : ''}`}>
<div className="card-follow__top">
<div className="card-follow__avatar" aria-hidden="true">
<span className="card-follow__face">MH</span>
<svg className="card-follow__ring" viewBox="0 0 52 52">
<circle cx={26} cy={26} r={25} pathLength={1} />
</svg>
</div>
<div>
<p className="card-follow__name">Mira Holt</p>
<p className="card-follow__role">Motion designer</p>
</div>
</div>
<div className="card-follow__stats">
<span>
<b>
<Count n={followers} />
</b>
<span className="card-follow__sr">{followers.toLocaleString('en-US')} </span>
Followers
</span>
<span>
<b>312</b>Following
</span>
</div>
<button type="button" className="card-follow__btn" aria-pressed={on} onClick={() => setOn((o) => !o)}>
<svg className="card-follow__tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="3" strokeLinecap="round" strokeLinejoin="round" aria-hidden="true">
<path d="M5 12.5l4.5 4.5L19 7.5" pathLength={1} />
</svg>
{on ? 'Following' : 'Follow'}
</button>
</article>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initCardFollow(el) on the mounted element instead.
<article class="card-follow">
<div class="card-follow__top">
<div class="card-follow__avatar" aria-hidden="true">
<span class="card-follow__face">MH</span>
<svg class="card-follow__ring" viewBox="0 0 52 52">
<circle cx="26" cy="26" r="25" pathLength="1"></circle>
</svg>
</div>
<div>
<p class="card-follow__name">Mira Holt</p>
<p class="card-follow__role">Motion designer</p>
</div>
</div>
<div class="card-follow__stats">
<span>
<b>
<span class="card-follow__count" aria-hidden="true">
<span class="card-follow__reel" style="transform:translateY(-18px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
<span>,</span>
<span class="card-follow__reel" style="transform:translateY(-36px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
<span class="card-follow__reel" style="transform:translateY(-144px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
<span class="card-follow__reel" style="transform:translateY(-72px)">
<span>0</span>
<span>1</span>
<span>2</span>
<span>3</span>
<span>4</span>
<span>5</span>
<span>6</span>
<span>7</span>
<span>8</span>
<span>9</span>
</span>
</span>
</b>
<span class="card-follow__sr">1,284 </span>
Followers
</span>
<span>
<b>312</b>
Following
</span>
</div>
<button type="button" class="card-follow__btn" aria-pressed="false">
<svg class="card-follow__tick" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="3" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true">
<path d="M5 12.5l4.5 4.5L19 7.5" pathLength="1"></path>
</svg>
Follow
</button>
</article>.card-follow { box-sizing: border-box; width: 240px; padding: 16px; border-radius: 16px; background: light-dark(#f3f4f7, #1a1c1f); box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035); }
.card-follow__top { display: flex; align-items: center; gap: 12px; }
.card-follow__avatar { position: relative; flex: none; width: 52px; height: 52px; }
.card-follow__face { position: absolute; inset: 5px; display: grid; place-items: center; border-radius: 50%; background: light-dark(#dbdde1, #2e3035); color: light-dark(#373a40, #f1f2f3); font-size: 14px; font-weight: 700; }
.card-follow__ring { position: absolute; inset: 0; width: 52px; height: 52px; transform: rotate(-90deg); }
.card-follow__ring circle { fill: none; stroke: light-dark(#373a40, #f1f2f3); stroke-width: 2; stroke-linecap: round; stroke-dasharray: 1; stroke-dashoffset: 1; opacity: 0; transition: stroke-dashoffset 0.6s cubic-bezier(0.5, 0, 0.2, 1), opacity 0.15s; }
.card-follow--on .card-follow__ring circle { stroke-dashoffset: 0; opacity: 1; }
.card-follow__name { margin: 0; font-size: 14px; font-weight: 600; color: light-dark(#373a40, #f1f2f3); }
.card-follow__role { margin: 2px 0 0; font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
.card-follow__stats { display: flex; gap: 18px; margin: 14px 0; font-size: 11px; color: light-dark(#5f626a, #a7abb2); }
.card-follow__stats b { display: block; color: light-dark(#373a40, #f1f2f3); font-size: 14px; font-weight: 600; }
.card-follow__count { display: flex; height: 18px; overflow: hidden; line-height: 18px; font-variant-numeric: tabular-nums; }
.card-follow__reel { display: flex; flex-direction: column; transition: transform 0.5s cubic-bezier(0.3, 1.3, 0.5, 1); }
.card-follow__sr { position: absolute; width: 1px; height: 1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.card-follow__stats > span { position: relative; }
.card-follow__btn {
display: flex;
align-items: center;
justify-content: center;
gap: 6px;
width: 100%;
height: 34px;
border: 0;
border-radius: 10px;
background: light-dark(#373a40, #f1f2f3);
color: light-dark(#fbfcfd, #121417);
font: inherit;
font-size: 12px;
font-weight: 600;
cursor: pointer;
transition: background-color 0.25s, color 0.25s, transform 0.15s;
}
.card-follow__btn:active { transform: scale(0.97); }
.card-follow__btn:focus-visible { outline: 2px solid light-dark(#50535b, #c6c8cd); outline-offset: 2px; }
.card-follow--on .card-follow__btn { background: light-dark(#ebedf0, #232529); color: light-dark(#373a40, #f1f2f3); }
.card-follow__tick { width: 0; height: 14px; overflow: visible; transition: width 0.25s; }
.card-follow--on .card-follow__tick { width: 14px; }
.card-follow__tick path { stroke-dasharray: 1; stroke-dashoffset: 1; transition: stroke-dashoffset 0.3s ease 0.15s; }
.card-follow--on .card-follow__tick path { stroke-dashoffset: 0; }
@media (prefers-reduced-motion: reduce) {
.card-follow__ring circle, .card-follow__reel, .card-follow__tick, .card-follow__tick path { transition: none; }
}function initCardFollow(root) {
var BASE = 1284;
var btn = root.querySelector('.card-follow__btn');
var reels = root.querySelectorAll('.card-follow__reel');
var sr = root.querySelector('.card-follow__sr');
var on = false;
function onClick() {
on = !on;
root.classList.toggle('card-follow--on', on);
btn.setAttribute('aria-pressed', String(on));
btn.lastChild.textContent = on ? 'Following' : 'Follow';
// Each digit is a reel of 0 to 9 rolled to its digit; the comma stays put.
var n = (BASE + (on ? 1 : 0)).toLocaleString('en-US');
var digits = n.replace(/,/g, '');
reels.forEach(function (reel, i) {
reel.style.transform = 'translateY(' + -digits[i] * 18 + 'px)';
});
sr.textContent = n + ' ';
}
btn.addEventListener('click', onClick);
return function () {
btn.removeEventListener('click', onClick);
};
}
document.querySelectorAll('.card-follow').forEach(initCardFollow);Goal: Create a "Follow" card.
Visual behavior: A profile card whose Follow button turns into Following with a drawn tick, while the follower count rolls up by one and a ring draws itself round the avatar.
Technique:
- The ring is an SVG circle with pathLength 1, a dash array of 1 and a dash offset of 1; following transitions the offset to 0, so it draws round from the top.
- The follower count is split into digit reels (a column of 0 to 9 in a one digit window) moved with translateY. Commas stay put, so only the digits that change roll.
- The tick in the button opens its width from 0 and draws its stroke a beat later, pushing the label across as it arrives.
Accessibility: The button is a toggle with aria-pressed and its label changes to Following. The reels are aria-hidden and a visually hidden copy of the count is read instead. Under prefers-reduced-motion the ring, count and tick change without moving.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Follow" card.
Visual behavior: A profile card whose Follow button turns into Following with a drawn tick, while the follower count rolls up by one and a ring draws itself round the avatar.
Technique:
- The ring is an SVG circle with pathLength 1, a dash array of 1 and a dash offset of 1; following transitions the offset to 0, so it draws round from the top.
- The follower count is split into digit reels (a column of 0 to 9 in a one digit window) moved with translateY. Commas stay put, so only the digits that change roll.
- The tick in the button opens its width from 0 and draws its stroke a beat later, pushing the label across as it arrives.
Accessibility: The button is a toggle with aria-pressed and its label changes to Following. The reels are aria-hidden and a visually hidden copy of the count is read instead. Under prefers-reduced-motion the ring, count and tick change without moving.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
A profile card whose Follow button turns into Following with a drawn tick, while the follower count rolls up by one and a ring draws itself round the avatar.
The button is a toggle with aria-pressed and its label changes to Following. The reels are aria-hidden and a visually hidden copy of the count is read instead. Under prefers-reduced-motion the ring, count and tick change without moving.
A gift from Ana
Tap the box to open it
Mira Holt
Prints, posters and slow animation.
We shipped the redesign two weeks early, and the motion finally feels like one product instead of five.To do2
Done1