Weight Breathing
Pure CSS animation: font-weight oscillates between 300 and 900 with compensated letter-spacing — text breathes without a single line of JavaScript.
This effect is free — the Text category contains 56 effects total, including 4 free. Effect.Labs has 811 vanilla effects. Explore the category →
Usage examples
How it works
The effect is driven by a single @keyframes vfBreathe that simultaneously interpolates font-weight (300 → 900 → 300) and letter-spacing (2px → -1px → 2px) over 3 seconds. The ease-in-out curve produces gentle acceleration mid-oscillation — this rhythm is what makes the motion feel like breathing rather than a mechanical blink.
Smooth font-weight interpolation only works with a variable font (continuous wght axis): with Inter, Roboto Flex, or any variable typeface, the weight transitions smoothly from 300 to 900. With a static font, the browser snaps between available weights — usually just 400 and 700.
The simultaneous letter-spacing animation compensates for optical expansion: heavier weights take more horizontal space, and pulling tracking down to -1px at the peak visually stabilizes the word width. Without this adjustment, the text would appear to push its neighbors outward on every cycle.
The effect is 100% CSS — no requestAnimationFrame, no JS dependency. A single <span class="vf-breathing-text"> and the @keyframes rule are enough: the browser delegates all interpolation to its rendering engine.
Accessibility
- prefers-reduced-motion not implemented: the provided code contains no
@media (prefers-reduced-motion: reduce)block. To respect OS settings, add:@media (prefers-reduced-motion:reduce){.vf-breathing-text{animation:none;font-weight:600}}. - The animated text is a standard inline
<span>— normally readable by screen readers, without anyaria-hiddenmasking it. - Contrast: white text
rgb(255,255,255)on backgroundrgb(10,10,15)— ratio > 19:1, well above the WCAG AA threshold (4.5:1). - No keyboard interaction or focus required — the effect is purely decorative and creates no focus trap.
- No content flashes faster than 3 Hz: the 3-second cycle is far below the WCAG 2.3.1 seizure risk threshold.
Browser compatibility
CSS animation is supported everywhere since IE 10; smooth font-weight interpolation requires a variable font with a wght axis.
Without a variable font, <code>font-weight</code> snaps to available static weights (usually 400/700) — the animation still runs but steps instead of interpolating smoothly.
The code
Copy the three blocks into your page. No dependencies.
<div class="vf-preview" style="width:100%;min-height:300px;">
<div class="vf-breathing-scene">
<span class="vf-breathing-text">Breathe</span>
<span class="vf-breathing-sub">font-weight: 300 ↔ 900</span>
</div>
</div>
.vf-breathing-scene {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 16px;
}
.vf-breathing-text {
font-family: Inter, sans-serif;
font-size: 2.5rem;
color: rgb(255, 255, 255);
animation: 3s ease-in-out 0s infinite normal none running vfBreathe;
}
.vf-breathing-sub {
font-size: 0.75rem;
color: rgba(255, 255, 255, 0.4);
}
@keyframes vfBreathe {
0%, 100% {
font-weight: 300;
letter-spacing: 2px;
}
50% {
font-weight: 900;
letter-spacing: -1px;
}
}
// Effet CSS pur — pas de JavaScript nécessaire
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| animation-duration: 3s | 3s | Breathing cycle speed. 2 s = fast, 5 s = meditative. Edit the value in the .vf-breathing-text selector. |
| font-weight 300 → 900 | 300 / 900 | Weight range inside @keyframes vfBreathe (at 0%/100% and 50%). Narrow to 400→700 for a subtler oscillation. |
| letter-spacing 2px → -1px | 2px / -1px | Tracking compensation inside @keyframes vfBreathe. Tune if your font has different optical spacing behavior. |
| font-family: Inter | Inter, sans-serif | Replace with any variable font with a wght axis (Roboto Flex, Manrope, Cabinet Grotesk…). Without a variable font, the animation will be stepped. |
| font-size: 2.5rem | 2.5rem | Text size in .vf-breathing-text. The effect is more striking from 3 rem upward. |
| animation-timing-function | ease-in-out | Cycle curve. cubic-bezier(0.4,0,0.2,1) = Material-style; linear = regular mechanical oscillation. |
| color: rgb(255,255,255) | white | Text color in .vf-breathing-text. Compatible with background-clip:text to animate a gradient alongside the weight. |
FAQ
The animation is jerky — why?
Smooth font-weight interpolation requires a variable font with a continuous wght axis. With a static typeface, the browser snaps between available weights (usually 400 and 700). Switch to Inter Variable, Roboto Flex, or any other variable font for a true analog transition from 300 to 900.
How do I apply the effect to multiple texts with staggered timing?
Add animation-delay to each .vf-breathing-text. A negative delay (e.g. -1.5s) starts the second element mid-cycle, creating a wave effect: the first is light when the second is heavy. A negative delay starts immediately and avoids an initial flash at font-weight 300.
How do I respect prefers-reduced-motion?
The provided code does not include this media query. Add after the effect rules: @media (prefers-reduced-motion:reduce){.vf-breathing-text{animation:none;font-weight:600}}. This freezes the text at a readable intermediate weight without hiding it.