Text Loading
A ::after pseudo-element animated by @keyframes cycles through four content states — empty, one dot, two dots, three dots — in a 1.5 s loop; one single div, zero 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. Loaders has 29 effects, including 4 free. Explore the category →
3 usage examples



How it works
The effect lives in a single <div class="loader-text">: the static label (here "Loading") is written directly in the HTML, and ::after carries the animation. The CSS sets font-size: 1.2rem, font-weight: 600 and color: #6366f1 (indigo) on the div. The pseudo-element starts with content: "" and receives animation: 1.5s ease 0s infinite normal none running loadingDots — duration 1.5 s, ease timing, zero delay, infinite loop, no fill mode. Measured in Chromium: 74.7 × 22 px for the text "Loading" at 19.2 px/600. No JavaScript.
@keyframes loadingDots defines four stops: 0% → "", 25% → ".", 50% → "..", 75% → "...". The content property is discrete: it does not interpolate, it switches. With the ease function, the switch happens when the Bézier curve reaches 50% of progress over each 375 ms interval — which does not coincide exactly with the half-interval mark. There is no 100% keyframe: "..." persists until the implicit midpoint of the 75%–100% interval, then the cycle restarts at "". Measured in Chromium: correct sequence, no console errors.
For four states of strictly equal duration (375 ms each), replace ease with steps(1, end) in the animation declaration, or add an explicit 100% { content: "" } keyframe. The effect is pure CSS: it integrates unchanged in an iframe, a Shadow DOM component or a button, without absolute positioning, without z-index and without any external dependency.
Accessibility
- prefers-reduced-motion not handled: no
@media (prefers-reduced-motion: reduce)rule suspends the animation. Measured in Playwright withreduceemulation:animationPlayStatestaysrunning. Fix:@media (prefers-reduced-motion: reduce) { .loader-text::after { animation: none; content: "…"; } }. - role="status" absent: a loading indicator must notify screen readers when it appears or disappears. Add
role="status"(oraria-live="polite") to the div so that VoiceOver and NVDA announce the element on DOM insertion. - The animated dots (::after) are not read by screen readers: pseudo-elements are absent from the accessibility tree. The static text ("Loading") is announced correctly — the dots are decorative, which is acceptable as long as the text describes the state. If the div is left empty (dots only), add
aria-label="Loading". - The effect is pure CSS and mouse-independent: it works identically on mobile, tablet and in any static container. No
position: fixed, noz-index, no overflow — the div follows normal document flow.
Browser compatibility
Pure CSS: requires animation, @keyframes and content on pseudo-elements. No site CSS variables, no dependencies, no JavaScript.
Without CSS animation support (IE 9 and earlier), the div shows the static text without dots — content remains readable, loading state communicated by the text alone. Without pseudo-element support, same result.
The code
HTML structure to paste into your page (CSS + JS available with a premium account):
<div class="loader-text">Loading</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 |
|---|---|---|
| Static label (HTML — div content) | "Loading" | Any string, including empty to show only the dots. The CSS does not change. |
Color (CSS — color: #6366f1) |
#6366f1 (indigo) | Color of both text and dots. The pseudo-element inherits color. |
Size (CSS — font-size: 1.2rem) |
1.2rem (19.2 px on a 16 px base) | Size of the entire block — text and dots. Measured render: 74.7 × 22 px for "Loading" at 19.2 px. |
Weight (CSS — font-weight: 600) |
600 | 400 for a subtle inline loader; 700 for a headline-style loading screen. |
Cycle duration (CSS — animation: 1.5s …) |
1.5s | 1s = fast dots (~250 ms per state); 2.4s = slow pace (~600 ms per state). |
Timing uniformity (CSS — ease in animation) |
ease | With ease, discrete content switches do not occur at strictly equal 375 ms intervals. Replace with steps(1, end) for a rigorously uniform rhythm. |
Dot states (CSS — content values in @keyframes) |
"" / "." / ".." / "..." | Replace with "•" / "••" / "•••" for middle dots, or reduce to two states ("" and "...") for a sharp on/off toggle. |
FAQ
animation declaration uses ease, a cubic Bézier curve. For content (a discrete property), the switch happens when the curve reaches 50% of progress over each 375 ms interval, which does not coincide with the half-duration mark. All four states remain visible, but their effective durations differ slightly. For a strictly uniform rhythm, replace ease with steps(1, end).clip-path on ::before/::after, communicating distortion or a retro aesthetic, not progress. Text Loading animates content on ::after to signal activity: the dot rhythm is a universal visual convention for waiting. Both effects target a text div, but they share neither the same render, nor the same use case, nor the same CSS technique.<div class="loader-text"></div>). The animation will display "", ".", "..", "..." — bare indigo dots that look like a chat typing indicator. In that case, add aria-label="Loading" to the div: with no static text, screen readers have nothing to read without that attribute.