03스크롤 위치에 따라 가운데에서 양쪽으로 그려지는 구분선
스크롤 영역 바닥에서 선이 올라온 거리를 DRAW_RANGE_PX 로 나눈 비율이 선 길이가 되어, 구분선이 가운데에서 양쪽으로 그려지고 되돌아가면 다시 줄어든다.
쓰임스크롤에 맞춰 선 그리기
DRAW_RANGE_PXDRAW_MIN_PCT

<main class="stage">
<div class="dv is-demo" id="dvRoot">
<div class="dv__scroller" id="dvScroller" tabindex="0" aria-label="스크롤 영역">
<div class="dv__block">위 단락</div>
<div class="dv__block dv__block--tall">위 단락</div>
<div class="dv__line" id="dvLine" role="separator"></div>
<div class="dv__block">아래 단락</div>
<div class="dv__block dv__block--tall">아래 단락</div>
</div>
<p class="dv__meta"><span class="dv__tag">DRAW</span> <b id="dvPct">0</b>%</p>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
// 핵심: 스크롤로 올라온 거리(DRAW_RANGE_PX 기준)를 clip-path inset 으로 바꿔 선이 가운데에서 양쪽으로 열린다.
$line-h: 10px;
$line-color: $subject-mint;
@include stage($stage-ink);
.dv {
width: min(100%, 360px);
display: grid;
gap: $sp-3;
}
.dv__scroller {
box-sizing: border-box;
height: 128px;
overflow-y: auto;
scrollbar-width: none;
display: grid;
gap: $sp-3;
align-content: start;
padding: $sp-3;
border-radius: $r-card;
background: $tick-panel;
}
.dv__scroller::-webkit-scrollbar { display: none; }
.dv__scroller:focus-visible { outline: 3px solid $stage-paper; outline-offset: 3px; }
.dv__block {
box-sizing: border-box;
height: 40px;
padding: 0 $sp-4;
display: flex;
align-items: center;
border-radius: $r-control;
background: rgba($subject-lilac, .22);
color: $stage-paper;
font-size: 14px;
font-weight: 700;
line-height: 20px;
}
.dv__block--tall { height: 72px; }
.dv__line {
flex: none;
height: $line-h;
background: $line-color;
clip-path: inset(0 50% 0 50% round 6px);
}
.dv__meta {
margin: 0;
display: flex;
align-items: baseline;
gap: $sp-2;
font-size: 12px;
line-height: 16px;
color: rgba($stage-paper, .78);
}
.dv__tag { font-weight: 700; letter-spacing: .08em; }
.dv__meta b { min-width: 3ch; text-align: right; font-variant-numeric: tabular-nums; color: $stage-paper; }
@media (prefers-reduced-motion: reduce) {
.dv__line { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #17141a;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.dv {
width: min(100%, 360px);
display: grid;
gap: 12px;
}
.dv__scroller {
box-sizing: border-box;
height: 128px;
overflow-y: auto;
scrollbar-width: none;
display: grid;
gap: 12px;
align-content: start;
padding: 12px;
border-radius: 16px;
background: #231f28;
}
.dv__scroller::-webkit-scrollbar {
display: none;
}
.dv__scroller:focus-visible {
outline: 3px solid #ffffff;
outline-offset: 3px;
}
.dv__block {
box-sizing: border-box;
height: 40px;
padding: 0 16px;
display: flex;
align-items: center;
border-radius: 12px;
background: rgba(185, 160, 255, 0.22);
color: #ffffff;
font-size: 14px;
font-weight: 700;
line-height: 20px;
}
.dv__block--tall {
height: 72px;
}
.dv__line {
flex: none;
height: 10px;
background: #4cd4a6;
clip-path: inset(0 50% 0 50% round 6px);
}
.dv__meta {
margin: 0;
display: flex;
align-items: baseline;
gap: 8px;
font-size: 12px;
line-height: 16px;
color: rgba(255, 255, 255, 0.78);
}
.dv__tag {
font-weight: 700;
letter-spacing: 0.08em;
}
.dv__meta b {
min-width: 3ch;
text-align: right;
font-variant-numeric: tabular-nums;
color: #ffffff;
}
@media (prefers-reduced-motion: reduce) {
.dv__line {
transition: none;
}
}(function () {
// 기법: scroll 이벤트마다 선의 위치를 getBoundingClientRect 로 재서 진행도를 구하고, 그 값만큼 clip-path 를 양쪽에서 연다.
// IntersectionObserver 는 쓰지 않는다. 진행도 = 선이 영역 바닥을 지나 올라온 거리 / DRAW_RANGE_PX 이다.
var DRAW_RANGE_PX = 80;
var DRAW_MIN_PCT = 0;
var CYCLE_MS = 2000;
var root = document.getElementById('dvRoot');
var scroller = document.getElementById('dvScroller');
var line = document.getElementById('dvLine');
var pctEl = document.getElementById('dvPct');
var reduce = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
function update() {
var box = scroller.getBoundingClientRect();
var lineBox = line.getBoundingClientRect();
var risen = box.bottom - lineBox.top;
var p = Math.min(1, Math.max(0, risen / DRAW_RANGE_PX));
var pct = DRAW_MIN_PCT + (100 - DRAW_MIN_PCT) * p;
var edge = (100 - pct) / 2;
line.style.clipPath = 'inset(0 ' + edge.toFixed(2) + '% 0 ' + edge.toFixed(2) + '% round 6px)';
pctEl.textContent = String(Math.round(pct));
}
function stopDemo() { root.classList.remove('is-demo'); }
scroller.addEventListener('scroll', update);
scroller.addEventListener('wheel', stopDemo, { passive: true });
scroller.addEventListener('pointerdown', stopDemo);
scroller.addEventListener('keydown', stopDemo);
var t0 = null;
function tick(ts) {
if (t0 === null) t0 = ts;
if (root.classList.contains('is-demo')) {
var max = scroller.scrollHeight - scroller.clientHeight;
var phase = ((ts - t0) % CYCLE_MS) / CYCLE_MS;
var tri = phase < 0.5 ? phase * 2 : 2 - phase * 2;
scroller.scrollTop = max * tri;
}
requestAnimationFrame(tick);
}
if (reduce) {
stopDemo();
scroller.scrollTop = (scroller.scrollHeight - scroller.clientHeight) / 2;
update();
} else {
update();
requestAnimationFrame(tick);
}
})();// ScrollDrawDivider — vanilla 판(03-divl-scroll-draw-from-center) 의 React 포트.
// scroll 이벤트마다 선의 위치를 getBoundingClientRect 로 재서 진행도를 구하고 clip-path 를 양쪽에서 연다.
import { useEffect, useRef } from "react";
import styles from "./ScrollDrawDivider.module.scss";
type Box<T> = { current: T };
const DRAW_RANGE_PX = 80;
const DRAW_MIN_PCT = 0;
const CYCLE_MS = 2000;
export default function ScrollDrawDivider() {
const scrollerRef = useRef<HTMLDivElement | null>(null) as Box<HTMLDivElement | null>;
const lineRef = useRef<HTMLDivElement | null>(null) as Box<HTMLDivElement | null>;
const pctRef = useRef<HTMLElement | null>(null) as Box<HTMLElement | null>;
useEffect(() => {
const scroller = scrollerRef.current;
const line = lineRef.current;
const pctEl = pctRef.current;
if (!scroller || !line || !pctEl) return;
const reduce = window.matchMedia("(prefers-reduced-motion: reduce)").matches;
let demo = !reduce;
let t0: number | null = null;
let raf = 0;
const update = (): void => {
const box = scroller.getBoundingClientRect();
const lineBox = line.getBoundingClientRect();
const risen = box.bottom - lineBox.top;
const p = Math.min(1, Math.max(0, risen / DRAW_RANGE_PX));
const pct = DRAW_MIN_PCT + (100 - DRAW_MIN_PCT) * p;
const edge = (100 - pct) / 2;
line.style.clipPath = "inset(0 " + edge.toFixed(2) + "% 0 " + edge.toFixed(2) + "% round 6px)";
pctEl.textContent = String(Math.round(pct));
};
const stopDemo = (): void => { demo = false; };
scroller.addEventListener("scroll", update);
scroller.addEventListener("wheel", stopDemo, { passive: true });
scroller.addEventListener("pointerdown", stopDemo);
scroller.addEventListener("keydown", stopDemo);
const tick = (ts: number): void => {
if (t0 === null) t0 = ts;
if (demo) {
const max = scroller.scrollHeight - scroller.clientHeight;
const phase = ((ts - t0) % CYCLE_MS) / CYCLE_MS;
const tri = phase < 0.5 ? phase * 2 : 2 - phase * 2;
scroller.scrollTop = max * tri;
}
raf = requestAnimationFrame(tick);
};
if (reduce) {
scroller.scrollTop = (scroller.scrollHeight - scroller.clientHeight) / 2;
update();
} else {
update();
raf = requestAnimationFrame(tick);
}
return () => {
cancelAnimationFrame(raf);
scroller.removeEventListener("scroll", update);
scroller.removeEventListener("wheel", stopDemo);
scroller.removeEventListener("pointerdown", stopDemo);
scroller.removeEventListener("keydown", stopDemo);
};
}, []);
return (
<div className={styles.dv}>
<div className={styles.scroller} ref={scrollerRef} tabIndex={0} aria-label="스크롤 영역">
<div className={styles.block}>위 단락</div>
<div className={styles.blockTall}>위 단락</div>
<div className={styles.line} ref={lineRef} role="separator" />
<div className={styles.block}>아래 단락</div>
<div className={styles.blockTall}>아래 단락</div>
</div>
<p className={styles.meta}>
<span className={styles.tag}>DRAW</span> <b ref={pctRef}>0</b>%
</p>
</div>
);
}03 스크롤 위치에 따라 가운데에서 양쪽으로 그려지는 구분선 — 측정 누적면적 29.984% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · css 구분선 9가지 — 양 끝이 사라지고 글자 폭에 맞춰 선이 나뉘는 구분선 장면
이 데모가 속한 글
css 구분선 9가지 — 양 끝이 사라지고 글자 폭에 맞춰 선이 나뉘는 구분선 장면css 구분선은 선 한 줄이면 될 것 같지만, 양 끝이 뚝 잘려 어색하고 가운데 글자 폭에 선 길이가 안 맞고 섹션 경계가 직선이라 밋밋하다 — 01번·02번·04번이 하나씩 푼다. 이 편의 아홉 구분선은 선의 모양 이 글의 데모 전부 보기


