Every SKILL.md already declared the new name — name: frames, frames-cli, frames-core, and so on — but the directories were never moved, so the tree carried frames-* in content and hyperframes-* on disk. Everything that resolves a skill by path missed: the core-skill list, the plugin marketplace pin, and the docs the CLI reads at runtime. Eight tests failed on main because the files they open do not exist. The directories now match what the files say they are, and the guide the claude-design contract reads takes the name that contract already used.
5.8 KiB
name, description, metadata
| name | description | metadata | ||
|---|---|---|---|---|
| counting-dynamic-scale | Counter animation where the value counts up while transform scale grows to its final size, creating escalating visual weight without per-frame text reflow. |
|
Counting with Dynamic Scale
A number counts from A → B while its transform scale grows to the final size — escalating visual weight ("this is impressive") without tweening font-size or forcing text layout on every frame. The final font size is static CSS; only the transform changes.
How It Works
Two synchronized tweens at the SAME timeline position with the SAME ease: (1) a proxy value rendered as text via onUpdate (Math.round(...).toLocaleString()), (2) the counter's transform scale: START_SCALE → 1, where START_SCALE = START_SIZE / END_SIZE. A suffix (%, ×, +) slides in AFTER the count lands — the number gets its own beat — and a label fades in early.
Recipe
<!-- inside a standard scene clip (frames-core) -->
<div class="counter-wrap">
<span class="counter" id="counter">0</span><span class="counter-suffix">{suffix}</span>
</div>
<div class="counter-label">{label}</div>
.counter-wrap {
display: flex;
align-items: baseline;
justify-content: center;
width: {counterContainerWidth}; /* fixed width — no layout shift as digit count changes */
}
.counter {
font-variant-numeric: tabular-nums; /* MANDATORY — digits keep equal width */
display: inline-block;
font-size: {endSize}; /* final size is static; GSAP animates scale, not font-size */
transform-origin: center center;
}
.counter-suffix {
opacity: 0;
transform: translateY(20px);
}
const counter = document.getElementById("counter");
const state = { value: 0 };
const START_SCALE = START_SIZE / END_SIZE;
// Count value — onUpdate changes text only
tl.to(
state,
{
value: TARGET_VALUE,
duration: COUNT_DUR,
ease: COUNT_EASE,
onUpdate: () => {
counter.textContent = Math.round(state.value).toLocaleString();
},
},
0,
);
// Visual growth — compositor transform sharing the count's timing + ease
tl.fromTo(counter, { scale: START_SCALE }, { scale: 1, duration: COUNT_DUR, ease: COUNT_EASE }, 0);
// Suffix slides in AFTER the count completes
tl.to(
".counter-suffix",
{ opacity: 1, y: 0, duration: SUFFIX_DUR, ease: `back.out(${SUFFIX_BOUNCE_FACTOR})` },
COUNT_DUR,
);
// Label fades in early
tl.from(".counter-label", { opacity: 0, y: 12, duration: LABEL_DUR, ease: "power2.out" }, LABEL_AT);
Variations
- Direct
innerTexttween (no proxy) — GSAP can tweeninnerTextdirectly for a number-only counter; keep the proxy form when you need locale formatting or suffix logic. The scale tween stays separate either way:
tl.to(
counter,
{ innerText: TARGET_VALUE, duration: COUNT_DUR, ease: COUNT_EASE, snap: { innerText: 1 } },
0,
);
- 3D depth entry — add a
tl.from(".counter", { z: -300, ... }, 0)push-in; requiresperspectiveon.counter-wrapandtransform-style: preserve-3don the counter. - Multi-stat coordinated reveal — 3 stats counting in parallel share the SAME ease, duration, and start position so they finish together (a chord, not an arpeggio). Each stat usually also needs a paired graphic (bar / ring / stars) — don't stop at the number; see stat-bars-and-fills.md.
Values
| token | range | notes |
|---|---|---|
| TARGET_VALUE | 2–3 digits ideal | 4+ digits needs a wider container; must fit at END_SIZE without clipping |
| START_SIZE / END_SIZE | START ≈ 40–60% of END | design inputs used once for START_SCALE; never tween either |
| COUNT_DUR | 1.2–2.5s | below ~0.8s reads as a flash — the eye must read the digits scrolling past |
| COUNT_EASE | power2.out / power3.out ⭐ / expo.out |
shared by value + scale; more .out = more dramatic deceleration at the peak |
| SUFFIX_DUR | 0.3–0.6s | fires at COUNT_DUR, never during the count |
| SUFFIX_BOUNCE_FACTOR | 1.4–2.0 | overshoot is fine on the suffix (it's punctuation, not data) |
| LABEL_AT / LABEL_DUR | AT < COUNT_DUR/2; 0.4–0.7s | label arrives before the count peaks |
Critical Constraints
tabular-numsmandatory + fixed-width container as belt-and-suspenders — without them digit-count transitions (9 → 10 → 100) jitter as glyph widths change.- Never set
fontSizeinonUpdate— final type size is static CSS; only the transform changes per frame. KeeponUpdateO(1): set text only, no style writes or DOM creation. Math.round, notMath.floor— halfway through the final integer should already display the final value.- Avoid
back.out/elastic.outon the counter itself — overshoot makes the number look unstable (it's data, not decoration). Grow in place, don't bounce. - Label is BIG TEXT, not a page-style caption — a tiny paragraph under a hero-size number reads as visual noise in video. Display-size, uppercase, tracked: the label is part of the headline.
See also
stat-bars-and-fills (the paired graphic — give it the same ease/duration so number and fill land as one beat) · svg-path-draw (icons drawing in around the number) · center-outward-expansion (icons bursting outward at the count peak).