모션·인터랙션 / Cadence
Gradient Text
텍스트 · 로딩 모션. 미리보기와 원본 코드·프롬프트를 이곳에서 확인하세요.
미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
HTML · CSS · JavaScript 전체 코드
<!doctype html>
<!--
Gradient Text
A headline whose letters rise in one by one out of a blur, each painted solid black.
Once they are all in, the headline takes over with background-clip: text and a
black → grey gradient, invisibly (the letters' own fill still covers it); fading that
fill off reveals the gradient. A soft band of light then sweeps across the gradient
every few seconds, and the subline fades in last. The sequence holds, fades out and
plays again; a click replays it at once.
Dependencies: none (plain CSS + JS).
Tweak (CSS custom properties on :root, mirrored by the constants in the script):
- --letter-step: 85ms gap between one letter and the next
- --letter-dur: 560ms each letter's fade, rise and un-blur
- --letter-rise: 0.35em how far each letter rises
- --letter-blur: 8px starting blur
- --letter-ease: cubic-bezier(0.22, 1, 0.36, 1) rise easing
- --settle-dur: 700ms solid black fading into the gradient
- --reveal-dur: 650ms subline fade
- --gradient the resting gradient (#000 → #8e8e8e)
- --sheen / --sheen-dur the light band and its cycle (3.6s: sweep in the first 65%, then rest)
- HOLD_MS (2500) / OUT_MS (400) in the script: how long the end state holds, fade-out before the loop
Based on the headline intro of the Fetched site (own project).
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Gradient Text</title>
<style>
:root {
--letter-step: 85ms;
--letter-dur: 560ms;
--letter-rise: 0.35em;
--letter-blur: 8px;
--letter-ease: cubic-bezier(0.22, 1, 0.36, 1);
--settle-dur: 700ms;
--reveal-dur: 650ms;
--gradient: linear-gradient(90deg, #000 0%, #000 26%, #8e8e8e 100%);
--sheen: linear-gradient(100deg, transparent 42%, rgb(255 255 255 / 0.45) 50%, transparent 58%);
--sheen-dur: 3.6s;
}
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
min-height: 100svh;
display: flex;
background: #fff;
color: #000;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
h1, p { margin: 0; }
.gt-stage {
flex: 1;
display: flex;
align-items: center;
justify-content: center;
padding: 0 clamp(16px, 2.5vw, 32px) 6vh;
cursor: pointer; /* a click replays the sequence */
}
.gt-type {
display: flex;
flex-direction: column;
align-items: center;
text-align: center;
transition: opacity 0.4s ease;
}
/* The whole block fades out before the sequence loops */
.gt-type.is-out { opacity: 0; }
/* The h1 paints nothing until the letters are all in (see .is-clipped). */
.gt-headline {
font-size: clamp(44px, 8vw, 140px);
font-weight: 400;
line-height: 1.04;
letter-spacing: -0.035em;
-webkit-background-clip: text;
background-clip: text;
color: transparent;
-webkit-text-fill-color: transparent;
}
.gt-word { white-space: nowrap; }
/* Once the letters are in, the h1 paints them with the gradient (+ the light band).
The letters still cover it with their own black at that instant, so the handover
is invisible; fading that black away is what lands the gradient. */
.gt-headline.is-clipped {
background-image: var(--sheen), var(--gradient);
background-size: 300% 100%, 100% 100%;
background-position: 100% 0, 0 0;
background-repeat: no-repeat;
animation: gt-sheen var(--sheen-dur) cubic-bezier(0.45, 0, 0.25, 1) var(--settle-dur) infinite;
}
/* The band enters off the left edge, leaves off the right, then rests off-screen. */
@keyframes gt-sheen {
0% { background-position: 100% 0, 0 0; }
65%, 100% { background-position: 0% 0, 0 0; }
}
/* Each letter paints its own solid black, rises and comes into focus. */
.gt-letter {
display: inline-block;
color: #000;
-webkit-text-fill-color: #000;
opacity: 0;
transform: translateY(var(--letter-rise));
filter: blur(var(--letter-blur));
transition:
opacity var(--letter-dur) ease,
transform var(--letter-dur) var(--letter-ease),
filter var(--letter-dur) ease;
transition-delay: calc(var(--i) * var(--letter-step));
}
.gt-headline.is-lit .gt-letter {
opacity: 1;
transform: none;
filter: none;
}
/* Opacity exactly 1, no transform or filter during the handover: any of them makes
the span its own layer, which drops the glyph out of the h1's background clip. */
.gt-headline.is-clipped .gt-letter {
color: transparent;
-webkit-text-fill-color: transparent;
opacity: 1;
transform: none;
filter: none;
transition: color var(--settle-dur) ease, -webkit-text-fill-color var(--settle-dur) ease;
}
.gt-lede {
max-width: min(74ch, 940px);
margin-top: clamp(18px, 2.4vw, 34px);
font-size: clamp(15px, 1.22vw, 21px);
line-height: 1.62;
color: #8e8e8e;
opacity: 0;
transition: opacity var(--reveal-dur) ease;
}
.gt-lede.is-revealed { opacity: 1; }
/* Snap back to the start without transitions */
.gt-type.is-resetting,
.gt-type.is-resetting * { transition: none !important; }
@media (prefers-reduced-motion: reduce) {
.gt-letter { opacity: 1; transform: none; filter: none; transition: none; }
.gt-headline.is-clipped { animation: none; }
.gt-lede { opacity: 1; transition: none; }
.gt-headline.is-clipped .gt-letter { transition: none; }
}
</style>
</head>
<body>
<!-- Each letter is a .gt-letter span with its index in --i; keep words in .gt-word so they
don't break mid-word. The script below builds them from data-text, so just edit that. -->
<main class="gt-stage" id="stage">
<div class="gt-type">
<h1 class="gt-headline" id="headline" data-text="Motion, collected."></h1>
<p class="gt-lede" id="lede">A quiet library of motion for the web.</p>
</div>
</main>
<script>
(function () {
"use strict";
const START = 120; // ms before the first letter
const STEP = 85; // gap between letters (= --letter-step)
const LETTER = 560; // each letter's fade and rise (= --letter-dur)
const SETTLE = 700; // solid fill easing into the gradient (= --settle-dur)
const BEAT = 70; // short beat between phases
const PAUSE = 850; // pause before the subline
const REVEAL = 650; // subline fade (= --reveal-dur)
const HOLD_MS = 2500; // hold the end state
const OUT_MS = 400; // fade-out before the loop
const stage = document.getElementById("stage");
const type = stage.querySelector(".gt-type");
const headline = document.getElementById("headline");
const lede = document.getElementById("lede");
const reduced = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
// Split the headline into words and letters.
const text = headline.dataset.text;
headline.setAttribute("aria-label", text);
let i = 0;
text.split(" ").forEach((word, w) => {
if (w) headline.append(" ");
const wordEl = document.createElement("span");
wordEl.className = "gt-word";
wordEl.setAttribute("aria-hidden", "true");
for (const ch of word) {
const l = document.createElement("span");
l.className = "gt-letter";
l.style.setProperty("--i", i++);
l.textContent = ch;
wordEl.append(l);
}
headline.append(wordEl);
});
const LETTERS = i;
// Each phase begins once the one before it has finished, plus a short beat.
let at = START;
const CUE = { letters: at };
CUE.settle = at += (LETTERS - 1) * STEP + LETTER + BEAT;
at += SETTLE + BEAT;
CUE.reveal = at += PAUSE;
CUE.end = at += REVEAL;
let timers = [];
const later = (fn, ms) => timers.push(setTimeout(fn, ms));
function reset() {
timers.forEach(clearTimeout);
timers = [];
type.classList.add("is-resetting");
headline.classList.remove("is-lit", "is-clipped");
lede.classList.remove("is-revealed");
type.classList.remove("is-out");
void type.offsetWidth;
type.classList.remove("is-resetting");
}
function play() {
reset();
later(() => headline.classList.add("is-lit"), CUE.letters);
// Hand painting to the h1 and let the letters' own fill fade off it.
later(() => headline.classList.add("is-clipped"), CUE.settle);
later(() => lede.classList.add("is-revealed"), CUE.reveal);
later(() => type.classList.add("is-out"), CUE.end + HOLD_MS);
later(play, CUE.end + HOLD_MS + OUT_MS);
}
if (reduced) {
headline.classList.add("is-lit", "is-clipped");
lede.classList.add("is-revealed");
return;
}
stage.addEventListener("click", play);
// Start once the font is in, so the letters don't reflow mid-sequence.
const fontsReady = document.fonts && document.fonts.ready ? document.fonts.ready : Promise.resolve();
Promise.race([fontsReady, new Promise((r) => setTimeout(r, 1500))]).then(play);
})();
</script>
<script type="module" src="https://static.cloudflareinsights.com/beacon.min.js/vce701823e78c4301a35b7e07294d87301792088814092" integrity="sha512-UKNk8GbxkABxKcXYuLkO16EleLGUkcvJWtpSNmExE0Too3ZaFAxTscNWEc1OdAv5/BikHawapSCaiD2XCmp+Bw==" data-cf-beacon='{"version":"2024.11.0","token":"c54fa41b83884ade97f239ecb41aee46","r":1,"spa":2}' crossorigin="anonymous"></script>
</body>
</html>
원본 구현 프롬프트
# Gradient Text: letters rise in out of a blur, then their solid fill fades away to reveal a gradient
**Goal:** build a headline intro where each letter rises into place and comes into focus one after another in solid black, then the whole headline settles into a black-to-grey gradient with a soft band of light sweeping across it, and a subline fades in last.
## Visual behaviour
1. White page, a large centred headline (e.g. "Motion, collected.") with a short grey subline under it. At the start nothing is visible.
2. **Letters arrive** (starts 120 ms after load / after the font is ready). Each letter, in order:
- fades from opacity 0 to 1,
- rises from `translateY(0.35em)` to 0,
- un-blurs from `blur(8px)` to none.
Each letter takes 560 ms; letters start 85 ms apart. Opacity and blur use `ease`, the rise uses `cubic-bezier(0.22, 1, 0.36, 1)` (ease-out quint). During this phase every letter is painted in its own solid black.
3. **Handover to the gradient.** 70 ms after the last letter has finished, the headline element itself gets a gradient background clipped to its text (`background-clip: text`), black `#000` from 0% to 26%, then fading to grey `#8e8e8e` at 100%, left to right. At that instant the letters still paint their own solid black on top, so nothing visibly changes. Then each letter's own fill colour transitions to transparent over 700 ms (`ease`), which reveals the gradient underneath. The result: the end of the headline fades softly from black to grey.
4. **Light sweep.** Once the gradient has settled (start 700 ms after the handover), a soft diagonal band of light (a 100° linear gradient: transparent 42%, white at 45% opacity at 50%, transparent 58%) is layered on top of the text gradient, also clipped to the text. It sweeps across the letters from left to right, then rests out of sight, repeating forever: a 3.6 s cycle in which the sweep takes the first 65% (≈ 2.3 s) with `cubic-bezier(0.45, 0, 0.25, 1)` and the rest is a pause. Implement it as a background layer 300% wide whose `background-position` animates from `100% 0` to `0% 0`.
5. **Subline.** 920 ms after the gradient has settled (70 ms beat + 850 ms pause), the subline fades in over 650 ms (`ease`). It is grey `#8e8e8e`.
6. **Loop.** The finished state holds for 2.5 s, then the whole block fades out over 400 ms (`ease`), snaps back to the start with transitions off, and the sequence plays again. Clicking the headline area replays it immediately (clear all pending timers first so replays never stack).
## Timeline (17-letter headline)
| Time | Event |
|---|---|
| 0.12 s | first letter starts (then one every 85 ms) |
| 2.11 s | last letter done + 70 ms: gradient handover, letter fill fades off (700 ms) |
| 2.81 s | light sweep starts (3.6 s cycle, repeating) |
| 3.73 s | subline fades in (650 ms) |
| 4.38 s | everything at rest |
| 6.88 s | block fades out (400 ms) |
| 7.28 s | reset and replay |
Compute the cues from the letter count: handover = 120 + (letters − 1) × 85 + 560 + 70 ms.
## Exact values
| What | Value |
|---|---|
| Letter stagger | 85 ms |
| Letter duration | 560 ms |
| Letter rise | 0.35em → 0, `cubic-bezier(0.22, 1, 0.36, 1)` |
| Letter blur | 8px → 0, `ease` |
| Fill fade (handover) | 700 ms, `ease` |
| Gradient | `linear-gradient(90deg, #000 0%, #000 26%, #8e8e8e 100%)` |
| Light band | `linear-gradient(100deg, transparent 42%, rgba(255,255,255,.45) 50%, transparent 58%)`, size 300% × 100% |
| Sweep cycle | 3.6 s, sweep in 0–65%, `cubic-bezier(0.45, 0, 0.25, 1)`, infinite |
| Subline | 650 ms fade, grey `#8e8e8e` |
| Hold / fade-out | 2.5 s / 400 ms |
| Headline type | `clamp(44px, 8vw, 140px)`, regular weight, line-height 1.04, letter-spacing −0.035em |
| Subline type | `clamp(15px, 1.22vw, 21px)`, line-height 1.62, margin-top `clamp(18px, 2.4vw, 34px)` |
## Markup and structure
```html
<main class="stage">
<div class="type">
<h1 class="headline" aria-label="Motion, collected.">
<span class="word" aria-hidden="true"><span class="letter" style="--i:0">M</span>…</span>
<span class="word" aria-hidden="true">…</span>
</h1>
<p class="lede">A quiet library of motion for the web.</p>
</div>
</main>
```
- Split the text into letter spans (`display: inline-block`) with their index as `--i`, and set `transition-delay: calc(var(--i) * 85ms)`. Wrap each word in a `white-space: nowrap` span so words never break mid-word.
- The `h1` has `background-clip: text` and a transparent text colour from the start, but no background image until the handover.
- **Important:** during and after the handover each letter must have `opacity: 1` exactly, and no `transform` or `filter`. Any of those promotes the span to its own layer, which drops its glyph out of the parent's background-clip, and the gradient would vanish from that letter.
- Drive the phases with state classes on the headline (`is-lit` → letters in, `is-clipped` → gradient + fill fade-off), the subline (`is-revealed`) and the wrapper (`is-out` for the fade-out, `is-resetting` to kill transitions during the reset). Schedule them with timers.
- Start once `document.fonts.ready` resolves (or after 1.5 s at most), so the letters don't reflow mid-sequence.
## Libraries
None: CSS transitions, one CSS keyframe animation for the sweep, and a few lines of JS for the timers. Any animation library (GSAP, Motion) can drive the same phases, but the gradient handover has to stay a CSS colour transition on the letters.
## Responsive and reduced motion
- The headline scales fluidly with `clamp()` and stays centred; words wrap as whole words on narrow screens.
- `prefers-reduced-motion: reduce`: no sequence, no loop and no sweep. Show the final state straight away (gradient headline and subline visible).
## Acceptance criteria
- Letters appear one by one, 85 ms apart, each rising and sharpening out of a blur in solid black.
- The switch from solid black to the gradient shows no flash or jump: the end of the headline simply fades to grey over 0.7 s.
- A soft light band glides across the letters every 3.6 s and is otherwise out of sight.
- The subline fades in last; the whole block holds, fades out and replays about every 7.3 s.
- Clicking replays from the start without stacking animations.
- With reduced motion, the finished headline is shown statically.
보관·활용 메모
Cadence에서 제공하는 코드와 프롬프트, 미리보기 영상을 이 사이트에 보관했습니다. 코드는 다운로드한 원본으로 제공하며 실행 화면은 별도로 격리되어 있습니다. 코드의 외부 라이브러리·샘플 이미지 요청은 유지됩니다.
보관일: 2026-10-26 · 프로젝트 적용: 별도 검증 필요
원본 코드·프롬프트·제작자 정보를 함께 보관했습니다. 실제 납품 시 코드와 포함 소재의 이용 조건을 확인하세요.
코드 SHA-256: faf722c177bda2996f83d651ae779dbb1252d928b140ab7e98e611defc467cb0