모션·인터랙션 / Cadence
Scroll Text Reveal
스크롤 · 텍스트 모션. 미리보기와 원본 코드·프롬프트를 이곳에서 확인하세요.
미리보기 안에서 스크롤하거나 마우스를 움직여 모션을 확인하세요.
HTML · CSS · JavaScript 전체 코드
<!doctype html>
<!--
Scroll Text Reveal
A passage of text lights up character by character, from light grey to black in
reading order, as it scrolls up the screen. The lit point follows the scroll position,
so scrolling back dims the text again.
Dependencies: none (plain JavaScript and CSS).
Tweak:
- START: 2/3 → the reveal begins when the text's top reaches 2/3 down the viewport
- END: 1/3 → the text is fully lit when its bottom reaches 1/3 down the viewport
- .str-char color: rgb(0 0 0 / 0.16) dim, #000 lit; transition 120ms linear
- .str padding: room above and below so the reveal can run from start to end
From The Direct site (own project), the statement reveal of "The Direct 1.0".
-->
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Scroll Text Reveal</title>
<style>
*, *::before, *::after { box-sizing: border-box; }
body {
margin: 0;
background: #fff;
color: #000;
font-family: system-ui, -apple-system, "Segoe UI", Roboto, Helvetica, Arial, sans-serif;
}
/* White space above and below so the reveal can run fully. */
.str {
padding: 75vh clamp(16px, 2.5vw, 32px) 80vh;
}
.str-text {
max-width: 900px;
margin: 0 auto;
font-size: clamp(22px, 2vw + 2px, 30px);
line-height: 1.7;
}
.str-text p { margin: 0; }
.str-text p + p { margin-top: 1.1em; }
/* Characters start light grey and switch to black in reading order. */
.str-char {
color: rgb(0 0 0 / 0.16);
transition: color 120ms linear;
}
.str-char.is-lit { color: #000; }
.visually-hidden {
position: absolute;
width: 1px;
height: 1px;
overflow: hidden;
clip: rect(0 0 0 0);
clip-path: inset(50%);
white-space: nowrap;
}
@media (prefers-reduced-motion: reduce) {
.str-char { transition: none; }
}
</style>
</head>
<body>
<section class="str">
<div class="str-text">
<p>
A collection of motion for the web, gathered slowly and rebuilt with care.
Scroll effects, text reveals and quiet transitions, each one ready to open, study and take home.
</p>
<p>
Good motion is rarely loud. It is the half second a photograph takes to settle,
the way a line of type steps forward as you reach it, the pause before one page gives way to the next.
It shows you where to look, and then it lets you go.
</p>
<p>
Every piece here begins as something seen and admired, then is stripped back to what makes it move.
Borrow the timing, change the easing, make it your own.
Nothing here asks for attention; it simply moves at its own pace.
</p>
</div>
</section>
<script>
(() => {
const START = 2 / 3; // × viewport height: reveal starts when the text top is here
const END = 1 / 3; // × viewport height: fully revealed when the text bottom is here
const block = document.querySelector(".str-text");
if (!block) return;
// Rebuild each paragraph as one span per character, in one reading-order list
// across the whole passage. A visually hidden copy is what screen readers read.
const chars = [];
block.querySelectorAll("p").forEach((paragraph) => {
const text = paragraph.textContent.replace(/\s+/g, " ").trim();
const visual = document.createElement("span");
visual.setAttribute("aria-hidden", "true");
[...text].forEach((character) => {
const span = document.createElement("span");
span.className = "str-char";
span.textContent = character;
visual.append(span);
chars.push(span);
});
const readable = document.createElement("span");
readable.className = "visually-hidden";
readable.textContent = text;
paragraph.replaceChildren(readable, visual);
});
// Reduced motion: show the text fully lit.
if (window.matchMedia("(prefers-reduced-motion: reduce)").matches) {
chars.forEach((span) => span.classList.add("is-lit"));
return;
}
let lit = 0;
const update = () => {
const start = window.innerHeight * START;
const end = window.innerHeight * END;
const { top, height } = block.getBoundingClientRect();
// 0 when the top is at the start line, 1 when the bottom is at the end line
const progress = Math.min(Math.max((start - top) / (start - end + height), 0), 1);
const target = Math.round(progress * chars.length);
// Only touch the characters that change state
while (lit < target) chars[lit++].classList.add("is-lit");
while (lit > target) chars[--lit].classList.remove("is-lit");
};
let queued = false;
const onScroll = () => {
if (queued) return;
queued = true;
requestAnimationFrame(() => {
queued = false;
update();
});
};
addEventListener("scroll", onScroll, { passive: true });
addEventListener("resize", onScroll);
update();
})();
</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>
원본 구현 프롬프트
# Scroll Text Reveal: a passage that lights up character by character as you scroll
**Goal:** build a block of text that starts light grey and turns black one character at a time, in reading order, tied to the scroll position, so the lit point moves through the passage as it climbs the screen and dims again when you scroll back.
## Visual behaviour
1. A passage of two or three paragraphs sits on a white page, centred, with plenty of empty space above and below it. At load every character is light grey (black at 16% opacity).
2. As the page scrolls down and the passage rises, characters turn solid black **in reading order across the whole passage** (paragraph 1, then 2, then 3, as one continuous list).
3. The number of lit characters is a direct function of where the text sits in the viewport:
- **0% lit** when the top of the text block is at **2/3 of the viewport height** (the bottom-third line).
- **100% lit** when the **bottom** of the text block reaches **1/3 of the viewport height** (the top-third line).
- In between, progress is linear: `progress = clamp((startLine − blockTop) / (startLine − endLine + blockHeight), 0, 1)`, where `startLine = 2/3 × innerHeight`, `endLine = 1/3 × innerHeight` and `blockTop` / `blockHeight` come from `getBoundingClientRect()`.
- Lit characters = `round(progress × totalCharacters)`.
- Because the range includes the block's own height, the boundary between black and grey always stays on screen, moving from the bottom third to the top third.
4. Scrolling up reverses it exactly: characters go back to grey from the end.
5. Each character's colour change has a short `120ms linear` colour transition, so fast scrolling reads as a soft wipe rather than a hard flicker.
## Exact values
| What | Value |
|---|---|
| Dim colour | `rgb(0 0 0 / 0.16)` |
| Lit colour | `#000` |
| Colour transition | `color 120ms linear` |
| Start line | text top at 2/3 of the viewport height |
| End line | text bottom at 1/3 of the viewport height |
| Mapping | linear, no easing, rounded to whole characters |
| Font size | `clamp(22px, 2vw + 2px, 30px)`, line height 1.7, regular weight |
| Text width | max 900px, centred |
| Paragraph spacing | 1.1em |
| Space around the passage | about 75vh above and 80vh below |
## Markup and structure
```html
<section class="str">
<div class="str-text">
<p>First paragraph…</p>
<p>Second paragraph…</p>
<p>Third paragraph…</p>
</div>
</section>
```
- On load, for each paragraph: collapse whitespace (`textContent.replace(/\s+/g, " ").trim()`), then replace its contents with
- a visually hidden `<span>` holding the plain text (what screen readers read), and
- an `aria-hidden="true"` `<span>` containing one `<span class="str-char">` per character (spaces included).
- Push every character span into one array in document order.
- On `scroll` (passive) and `resize`, schedule one update per animation frame. The update computes the target count and only adds or removes the `is-lit` class on characters between the previous count and the new one (don't loop over all characters every frame).
- Run the update once on load.
## Libraries
- None needed: plain JavaScript and CSS.
- If you use GSAP, a ScrollTrigger with `start: "top 66.66%"`, `end: "bottom 33.33%"` and an `onUpdate` that maps `self.progress` to the character count gives the same result. SplitText (`type: "chars"`) can do the splitting.
## Responsive and reduced motion
- The mapping uses viewport fractions, so it works at any size. The fluid font size keeps the passage readable on phones.
- `prefers-reduced-motion: reduce`: all characters lit from the start, no scroll listener, no colour transition.
## Acceptance criteria
- At load, with the passage below the bottom third of the screen, all text is light grey.
- Scrolling down lights characters strictly in reading order, flowing from one paragraph into the next.
- The passage is completely black when its last line reaches the top third of the screen.
- Scrolling up dims characters from the end, exactly mirroring the way down.
- The boundary between lit and unlit text is always visible on screen while the reveal is in progress.
- Screen readers read the paragraphs as normal text, not character by character.
- With reduced motion enabled, the text is fully black and static.
보관·활용 메모
Cadence에서 제공하는 코드와 프롬프트, 미리보기 영상을 이 사이트에 보관했습니다. 코드는 다운로드한 원본으로 제공하며 실행 화면은 별도로 격리되어 있습니다. 코드의 외부 라이브러리·샘플 이미지 요청은 유지됩니다.
보관일: 2026-10-26 · 프로젝트 적용: 별도 검증 필요
원본 코드·프롬프트·제작자 정보를 함께 보관했습니다. 실제 납품 시 코드와 포함 소재의 이용 조건을 확인하세요.
코드 SHA-256: d2f07d8ccf77b68768b30a43636edc25e17907edd6a2d5b5a6445c088bf1db5f