Breathing Text
Pure CSS animation — each word pulses between scale(1) and scale(1.08) over 3 seconds, staggered by an individual --delay variable to create a living breath without any JavaScript.
You'll get access to the interactive demo with a free account.
This effect is part of Effect.Labs — 811 vanilla effects, some free, some premium. Text has 56 effects, including 4 free. Explore the category →
3 usage examples



How it works
The effect is driven by @keyframes ktBreathe: at 0% and 100%, each word sits at transform: scale(1) and opacity: 0.85; at the 50% peak it reaches scale(1.08) and opacity: 1. The ease-in-out curve over 3 seconds mimics a slow inhale and exhale. The animation runs infinitely on the .kt-breathing .word selector — display: inline-block is required because CSS transforms have no effect on bare inline elements.
Per-word staggering is achieved through a CSS custom property --delay placed in each <span class="word">'s style attribute. The rule animation-delay: var(--delay) reads this value element by element, with zero JavaScript. Adding a word is as simple as inserting a <span class="word" style="--delay: Xs"> — the step between words is entirely free.
The JS block defines a splitChars(el) helper (splits data-text into .char spans) and selects an element via getElementById('fadeInText'), but neither the function nor the variable is ever called at runtime — this is utility code inherited from another effect. The breathing animation runs without JS: the <script> block can safely be removed or replaced.
Each .word expands from its center thanks to the default transform-origin: 50% 50% — no layout shift, adjacent words stay put. The absence of will-change avoids reserving a GPU compositing layer at rest, keeping memory overhead low when multiple instances share the page.
Accessibility
- prefers-reduced-motion: absent. No
@media (prefers-reduced-motion: reduce)block is present in the CSS. The animation loops forever even when the OS signals a motion-reduction preference. Recommended addition:@media (prefers-reduced-motion: reduce) { .kt-breathing .word { animation: none; } }. - No
aria-*attributes orrolein the provided HTML. For purely decorative text this is acceptable; if the text carries meaning for screen readers, wrap the container in a<p aria-label="full text">and addaria-hidden="true"to each<span class="word">. - Contrast: white (#fff) on dark background (#0a0a0f) → ratio > 20:1, well above the WCAG AA threshold (4.5:1).
- No interactive element in the effect — keyboard navigation is unaffected, no keyboard event listeners are attached.
- The animation involves no flash, rapid flickering, or large-amplitude movement (scale 1 to 1.08 over 3 s) — no risk of triggering photosensitive seizures.
Browser compatibility
Requires CSS Custom Properties and CSS Animations — supported in all modern browsers. Zero external dependencies.
Without CSS custom property support (very old browsers), <code>animation-delay</code> falls back to 0 s (the initial value) — all words breathe in unison instead of staggering. The breathing effect remains present, just unsynchronized.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="kt-text kt-breathing" data-text="BREATHE IN OUT">
<span class="word" style="--delay: 0s;">BREATHE</span>
<span class="word" style="--delay: 0.5s;">IN</span>
<span class="word" style="--delay: 1s;">OUT</span>
</div>
Full HTML + CSS + JS, copy-paste ready — with hundreds of premium effects.
Customize
Options passed to the API or data-* attributes:
| Option / property | Default | Effect |
|---|---|---|
| --delay (inline on each .word) | 0s, 0.5s, 1s… | Per-word animation offset. Increase the step for a more pronounced wave, reduce it for near-simultaneous breathing. |
| animation-duration (in .kt-breathing .word CSS rule) | 3s | Full cycle duration. 2 s = nervous, 5–6 s = meditative. Can also be overridden per word via inline style. |
| scale() at 50% in @keyframes ktBreathe | scale(1.08) | Maximum breath amplitude. scale(1.04) = subtle, scale(1.15) = dramatic. Above scale(1.2), adjacent words begin to overlap. |
| opacity at 0%/100% in @keyframes ktBreathe | 0.85 | Resting opacity (trough of the breath). Set to 0.6 for a more marked pulse, 1 to suppress the opacity oscillation entirely. |
| font-size in .kt-text | 2.2rem | Text size. The effect reads clearly from 1.5 rem upward — below that, a scale of 1.08 is barely perceptible. |
| color in .kt-text | #fff | Text color. Any hex or rgba works; a light tint (e.g. #a5f3fc ice blue) accentuates a spiritual or organic register. |
| letter-spacing in .kt-text | 0.02em | Inter-letter spacing. Increase to 0.08em or more for a monumental look suited to posters or full-screen slides. |
| margin in .kt-breathing .word | 0 0.15em | Inter-word spacing. Increase so each word has its own halo and does not encroach on neighbours during the scale. |
| animation-timing-function in .kt-breathing .word | ease-in-out | Easing curve. linear = mechanical, ease-in-out = natural/respiratory. cubic-bezier(0.45,0,0.55,1) produces an even softer breath. |
FAQ
splitChars() helper and selects an element, but neither is ever called at runtime. The breathing animation is fully CSS-driven — the ktBreathe keyframe and the --delay variable are all it needs. The <script> block can be removed without breaking anything.style="animation-duration: 4s" to the 'INHALE' span, 7s to 'HOLD', and 8s to 'EXHALE'. Each word can carry its own duration: animation-duration is inherited but overridden via the inline style attribute, and each word's cycle runs independently.splitChars(el) function already in the JS splits data-text into <span class="char"> elements. Call it, replace .word with .char in the .kt-breathing CSS rule, and assign --delay incrementally (e.g. 0.08 s per letter) to get a letter-by-letter breathing effect with the same keyframe.