Back to the library
Text reveal — whole block
Variant: Block
Four lines rising together on one 360ms curve — the cheapest reveal there is and, past about a paragraph, the correct one. Nothing in it claims that any line arrived before any other, which is exactly the point: it says "this section is here now" and then gets out of the reader’s way. Reach for it in dense product UI where the copy is the payload and the motion exists only so content does not appear out of nowhere.
textentrance
The source
A complete HTML document. Paste it into an empty .html file and it plays — no stylesheet, script, font, or image to fetch.
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Text reveal — whole block</title>
<style>
:root {
/* Neutrals — fixed, never remixed. */
--ivory: #faf9f5;
--paper: #ffffff;
--oat: #e3dacc;
--slate: #141413;
/* Knobs — the remix API. A knob exists iff declared here and used below. */
--hue: 38.8;
--lift: 0;
--chroma: 1;
--speed: 1;
/* Accents — derived from the knobs with the family's offsets baked in,
so one hue roll moves them together. Defaults reproduce the original hex. */
--clay: oklch(calc(0.6724 + var(--lift)) calc(0.1308 * var(--chroma)) var(--hue));
--clay-d: oklch(calc(0.5797 + var(--lift)) calc(0.127 * var(--chroma)) var(--hue));
--olive: oklch(
calc(0.6118 + var(--lift)) calc(0.0713 * var(--chroma)) calc(var(--hue) + 88.3)
);
--rust: oklch(
calc(0.5408 + var(--lift)) calc(0.1357 * var(--chroma)) calc(var(--hue) - 10.3)
);
/* Clock — every duration in the document runs off --t. */
--cycle: 6s;
--t: calc(var(--cycle) / var(--speed));
/* Asymmetric: off the mark immediately, then decelerating for most of
its length. Type has to look like it settled onto the baseline
rather than stopped at it. */
--ease-accent: cubic-bezier(0.2, 0.72, 0.12, 1);
/* Travel, not a duration — scales with the type rather than the clock. */
--_rise: clamp(5px, 1.3vw, 12px);
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
min-height: 100vh;
display: grid;
place-items: center;
padding: clamp(12px, 3vw, 28px);
background: var(--ivory);
font-family:
system-ui,
-apple-system,
'Segoe UI',
Roboto,
sans-serif;
color: var(--slate);
}
.reveal {
width: fit-content;
max-width: 100%;
animation: cycle var(--t) linear infinite;
}
/* The block is dark for the first 13% of every cycle. That window is not
a pause — it is where the units snap back to their start pose out of
sight, so the cascade never plays in reverse. It has to outlast the
longest stagger in the family (660ms), and every sibling carries the
same 13% so the four can be compared frame for frame. The exit is one
uniform fade: a staggered exit would be a second performance. */
@keyframes cycle {
0%,
13% {
opacity: 0;
}
14%,
95% {
opacity: 1;
animation-timing-function: ease;
}
98%,
100% {
opacity: 0;
}
}
.copy {
display: grid;
justify-items: start;
opacity: 0;
transform: translateY(var(--_rise));
animation: rise var(--t) linear infinite;
}
.line {
/* Lines are hard-broken, not wrapped, so all four siblings break in
exactly the same places at every stage width. */
white-space: nowrap;
}
.h {
font-size: clamp(19px, 4.5vw, 34px);
font-weight: 600;
letter-spacing: -0.02em;
line-height: 1.16;
}
.accent {
color: var(--clay);
}
.b {
font-size: clamp(12.5px, 2.95vw, 22px);
line-height: 1.42;
color: color-mix(in srgb, var(--slate) 58%, transparent);
}
.h + .b {
margin-top: clamp(8px, 1.2vw, 14px);
}
/* One unit, one offset: the whole block rises together. Nothing here
claims that any line arrived before any other, which is the entire
difference between this and the three siblings. */
@keyframes rise {
0%,
15% {
opacity: 0;
transform: translateY(var(--_rise));
animation-timing-function: var(--ease-accent);
}
21%,
100% {
opacity: 1;
transform: translateY(0);
}
}
@media (prefers-reduced-motion: reduce) {
/* Gentler, not absent. The travel goes and the block fades in place
on a 1.7x cycle — the opacity is what carried the meaning here
anyway. */
.reveal {
animation-duration: calc(var(--t) * 1.7);
}
.copy {
transform: none;
animation-name: rise-rm;
animation-duration: calc(var(--t) * 1.7);
}
@keyframes rise-rm {
0%,
15% {
opacity: 0;
animation-timing-function: ease;
}
21%,
100% {
opacity: 1;
}
}
}
</style>
</head>
<body>
<div class="reveal">
<div class="copy">
<p class="line h">Every deploy,</p>
<p class="line h accent">in one timeline.</p>
<p class="line b">Builds, migrations</p>
<p class="line b">and incidents.</p>
</div>
</div>
</body>
</html>
The variant family
Same idea, one axis moved. Compare them side by side before you commit.