A specimen card: Aa sweeps from hairline thin to black while a live readout counts the weight from 100 to 900 and a slider tracks it. One registered integer drives the font, the counter and the knob.
Also known as: variable font animation css, font weight animation, type specimen animation.
<div class="txt-type-specimen-weights"> <span class="txt-type-specimen-weights-glyph" aria-hidden="true">Aa</span> <span class="txt-type-specimen-weights-meta"><span>System UI</span><span class="txt-type-specimen-weights-value" role="img" aria-label="Weight 100 to 900"></span><span class="txt-type-specimen-weights-track" aria-hidden="true"></span></span> </div>
@property --txt-type-specimen-weights-w {
syntax: "<integer>";
inherits: true;
initial-value: 100;
}
.txt-type-specimen-weights {
--c: var(--txt-color, #3b82f6);
font-size: var(--txt-size, 32px);
font-family: system-ui, sans-serif;
font-weight: 700;
line-height: 1.2;
display: inline-flex;
align-items: center;
gap: 0.8em;
font-weight: 600;
animation: txt-type-specimen-weights-sweep 5.5s cubic-bezier(0.65, 0, 0.35, 1) infinite alternate;}
.txt-type-specimen-weights-glyph {
width: 1.5em;
font-size: 3em;
line-height: 1;
letter-spacing: -0.03em;
font-weight: var(--txt-type-specimen-weights-w);
}
.txt-type-specimen-weights-meta {
display: grid;
gap: 0.35em;
}
.txt-type-specimen-weights-meta > span:first-child {
font-size: 0.55em;
letter-spacing: 0.12em;
text-transform: uppercase;
color: color-mix(in srgb, currentColor 55%, transparent);
}
.txt-type-specimen-weights-value {
font-variant-numeric: tabular-nums;
counter-reset: txt-type-specimen-weights-n var(--txt-type-specimen-weights-w);
}
.txt-type-specimen-weights-value::before { content: "Weight "; color: color-mix(in srgb, currentColor 55%, transparent); font-weight: 500; }
.txt-type-specimen-weights-value::after { content: counter(txt-type-specimen-weights-n); color: var(--c); }
/* the slider: filled part and knob both sit at (w - 100) / 800 of the track */
.txt-type-specimen-weights-track {
position: relative;
width: 5.5em;
height: 0.16em;
margin-top: 0.15em;
border-radius: 0.08em;
background: color-mix(in srgb, currentColor 14%, transparent);
}
.txt-type-specimen-weights-track::before,
.txt-type-specimen-weights-track::after {
content: "";
position: absolute;
top: 50%;
translate: 0 -50%;
}
.txt-type-specimen-weights-track::before {
left: 0;
width: calc((var(--txt-type-specimen-weights-w) - 100) / 800 * 100%);
height: 100%;
border-radius: inherit;
background: var(--c);
}
.txt-type-specimen-weights-track::after {
left: calc((var(--txt-type-specimen-weights-w) - 100) / 800 * 100%);
width: 0.6em;
height: 0.6em;
margin-left: -0.3em;
border-radius: 50%;
background: var(--c);
box-shadow: 0 0 0 0.16em color-mix(in srgb, var(--c) 20%, transparent);
}
@keyframes txt-type-specimen-weights-sweep {
0%, 10% { --txt-type-specimen-weights-w: 100; }
90%, 100% { --txt-type-specimen-weights-w: 900; }
}
@media (prefers-reduced-motion: reduce) {
.txt-type-specimen-weights { animation: none; --txt-type-specimen-weights-w: 700; }
}
@media (prefers-reduced-motion: reduce) {
.txt-type-specimen-weights, .txt-type-specimen-weights *, .txt-type-specimen-weights::before, .txt-type-specimen-weights::after, .txt-type-specimen-weights *::before, .txt-type-specimen-weights *::after {
animation-duration: 0.01ms !important;
animation-iteration-count: 1 !important;
transition-duration: 0.01ms !important;
}
}--txt-color on the element or a parent.
Currently previewing #3b82f6.--txt-size. Every other dimension is in
em, so the whole thing scales together. Currently previewing 40px.transition or animation
shorthand. Staggered delays are relative, so they keep their rhythm.