JS 없이도 UI가 반응한다: scroll-state 쿼리와 타입 있는 attr()
스크롤 가능 방향(위/아래)과 HTML 속성 값(색/숫자/키워드)을 CSS가 직접 읽고 검증하면, "작은 UI 디테일"을 JS 없이도 안정적으로 구현할 수 있습니다.
한 문장 결론: 스크롤 가능 방향(위/아래)과 HTML 속성 값(색/숫자/키워드)을 CSS가 직접 읽고 검증하면, “작은 UI 디테일”을 JS 없이도 안정적으로 구현할 수 있습니다. (MDN: Container scroll-state queries)
배경/문제
세로 스크롤 영역에서 “여기 더 내려갈 수 있어요” 같은 힌트를 주고 싶을 때가 많습니다. 대표적으로:
- 코드 블록/표/리스트처럼 세로로 스크롤되는 영역이 있는 UI
- 타이포그래피, 그리드 정렬처럼 레이아웃 디테일이 UX 품질을 좌우하는 화면
그런데 이 힌트(그림자)는 보통 JS로 스크롤 위치를 계산해서 토글합니다. 유지보수 포인트가 늘고, 스크롤 이벤트 성능도 신경 써야 하죠.
또 하나. HTML과 CSS 사이 값 전달도 자주 애매해집니다.
- 테마 색을 바꾸려고 inline style을 쓰거나
- 그리드 컬럼 수를 바꾸려고 class를 늘리거나
data-*값을 쓰고 싶어도 CSS에서 “문자열”로만 처리되는 제약이 있었습니다 (MDN: attr())
포인트는 단순합니다. CSS가 “상태(스크롤 가능 방향)”와 “입력값(HTML 속성)”을 직접 다루면 JS 의존도를 크게 줄일 수 있습니다. (MDN: Container scroll-state queries)
핵심 개념
1) scroll-state 컨테이너 쿼리로 “스크롤 가능 방향”을 질의하기
container-type: scroll-state를 설정하면, CSS에서 @container scroll-state(scrollable: top|bottom|y...) 형태로 해당 방향으로 스크롤이 가능한지를 질의할 수 있습니다. (MDN: Container scroll-state queries)
2) 타입이 있는 attr()로 “HTML 속성 값을 CSS 타입으로 파싱하기”
attr()는 HTML 속성 값을 CSS에서 꺼내 쓰는 함수입니다. 여기에 type(<color>), type(<integer>) 같은 타입을 붙이면 파싱 + 검증 + fallback까지 한 번에 처리할 수 있습니다. (Chrome for Developers: advanced attr())
→ 기대 결과/무엇이 달라졌는지: HTML은 “값”만 제공하고, CSS는 그 값을 “타입 안정적으로” 해석합니다. 스크롤 힌트도 브라우저 상태를 CSS가 직접 질의해 토글할 수 있습니다. (MDN: Container scroll-state queries)
해결 접근
- 스크롤 영역에 컨테이너 이름/타입을 부여합니다. (
container-type: scroll-state) - 위/아래 그림자 레이어를 만들고 기본은 숨김 처리합니다.
@container ... scrollable: top/bottom조건이 참일 때만 그림자를 켭니다. (MDN: Container scroll-state queries)- 테마/그리드 같은 값은 HTML 속성으로 전달하고, CSS에서
attr(... type(...), fallback)으로 받아 씁니다. (Chrome for Developers: advanced attr())
구현(코드)
아래 예시는 Next.js(App Router)에서 바로 붙여볼 수 있는 형태로 구성했습니다.
app/page.tsx
import styles from "./page.module.css";
export default function Page() {
return (
<main className={styles.page}>
<section className={styles.theme} data-bg="white" data-fg="deeppink">
<h1 className={styles.title}>Scroll Hint + Typed attr()</h1>
<p className={styles.desc}>
스크롤 영역은 위/아래 방향에 따라 그림자를 표시하고,
테마/그리드 값은 HTML 속성으로 전달합니다.
</p>
</section>
<section className={styles.grid} data-columns="3">
{Array.from({ length: 9 }).map((_, i) => (
<article key={i} className={styles.card}>
Card {i + 1}
</article>
))}
</section>
<section className={styles.scrollArea} aria-label="Scrollable content">
<div className={styles.shadowTop} aria-hidden="true" />
<div className={styles.shadowBottom} aria-hidden="true" />
<div className={styles.scrollContent}>
{Array.from({ length: 40 }).map((_, i) => (
<p key={i} className={styles.line}>
Line {i + 1} — 스크롤하면서 위/아래 그림자 변화를 확인합니다.
</p>
))}
</div>
</section>
</main>
);
}→ 기대 결과/무엇이 달라졌는지: 그림자/테마/그리드가 전부 “HTML 속성 + CSS”만으로 연결됩니다. JS로 스크롤 위치를 읽는 코드를 만들지 않아도 됩니다. (MDN: Container scroll-state queries)
app/page.module.css
.page {
padding: 24px;
display: grid;
gap: 20px;
}
/* 1) Typed attr()로 테마 연결 */
.theme {
padding: 16px;
border-radius: 12px;
/* 기본값(호환성/안전성) */
background: black;
color: white;
}
/* typed attr() 지원 환경에서만 업그레이드 */
@supports (color: attr(data-fg type(<color>), white)) {
.theme {
background: attr(data-bg type(<color>), black);
color: attr(data-fg type(<color>), white);
}
}
.title {
margin: 0 0 6px;
}
.desc {
margin: 0;
}
/* 2) Typed attr()로 그리드 컬럼 수 연결 */
.grid {
display: grid;
gap: 12px;
/* 기본값 */
grid-template-columns: repeat(2, minmax(0, 1fr));
}
@supports (width: attr(data-columns type(<integer>), 3)) {
.grid {
--_cols: attr(data-columns type(<integer>), 3);
grid-template-columns: repeat(var(--_cols), minmax(0, 1fr));
}
}
.card {
border: 1px solid rgba(0, 0, 0, 0.12);
border-radius: 12px;
padding: 14px;
}
/* 3) scroll-state 쿼리로 스크롤 방향 힌트(그림자) */
.scrollArea {
border-radius: 12px;
border: 1px solid rgba(0, 0, 0, 0.12);
height: 220px;
overflow: auto;
position: relative;
/* 컨테이너 쿼리 설정 */
container-name: scroller;
container-type: scroll-state;
}
/* 그림자 레이어: 레이아웃을 밀지 않도록 음수 마진으로 겹치기 */
.shadowTop,
.shadowBottom {
pointer-events: none;
position: sticky;
left: 0;
right: 0;
height: 14px;
opacity: 0;
transition: opacity 160ms ease;
z-index: 1;
}
.shadowTop {
top: 0;
margin-bottom: -14px;
background: linear-gradient(to bottom, rgba(0, 0, 0, 0.22), transparent);
}
.shadowBottom {
bottom: 0;
margin-top: -14px;
background: linear-gradient(to top, rgba(0, 0, 0, 0.22), transparent);
}
/* scroll-state 지원 환경에서만 방향별로 그림자 토글 */
@supports (container-type: scroll-state) {
@container scroller scroll-state(scrollable: top) {
.shadowTop {
opacity: 1;
}
}
@container scroller scroll-state(scrollable: bottom) {
.shadowBottom {
opacity: 1;
}
}
}
.scrollContent {
padding: 12px 14px;
}
.line {
margin: 0 0 10px;
}→ 기대 결과/무엇이 달라졌는지: (1) 테마/그리드 값은 HTML 속성에서 CSS로 “타입 안정적으로” 이동하고, (2) 스크롤 가능 방향은 CSS가 직접 질의해서 위/아래 그림자를 자동으로 토글합니다. (MDN: Container scroll-state queries)
(추가) “enum처럼” 키워드만 받게 만들기
아래처럼 type(start | center | end)로 허용 키워드만 받게 만들면, 잘못된 값은 fallback으로 안전하게 떨어집니다. (MDN: type())
[scroll-snap] {
scroll-snap-align: attr(scroll-snap type(start | center | end), start);
}→ 기대 결과/무엇이 달라졌는지: HTML이 scroll-snap="nothing"처럼 엉뚱한 값을 보내도, CSS는 유효한 값만 받아들이고 기본값으로 안전하게 복구합니다. (MDN: type())
검증 방법(체크리스트)
data-columns에 잘못된 값(예: "abc")을 넣어도 레이아웃이 깨지지 않고 fallback으로 유지된다. (W3C: CSS Values and Units Level 5)data-bg, data-fg에 유효하지 않은 색을 넣어도 기본 색(black/white)으로 동작한다. (Chrome for Developers: advanced attr())흔한 실수/FAQ
Q1. attr(data-bg color, black)처럼 “타입 이름만” 쓰면 되나요?
공식 문서 기준으로는 type(<color>)처럼 type()로 감싸는 형태가 핵심입니다. (문법/지원은 환경에 따라 달라질 수 있습니다.) (Chrome for Developers: advanced attr())
Q2. attr()로 URL을 만들어 background-image에 넣을 수 있나요?
attr()로 만들어진 값은 URL로 사용될 때 제한이 걸릴 수 있습니다. 스타일 목적이라면 색/숫자/키워드 같은 “안전한 타입” 위주로 설계하는 편이 좋습니다. (W3C: CSS Values and Units Level 5)
Q3. 이 기능이 없는 브라우저에서는 어떻게 하나요?
- 테마/그리드 값 전달: CSS 커스텀 프로퍼티(
-var) + inline style이 가장 단순한 대안입니다. (MDN: Using CSS custom properties) - “enum” 매핑:
[attr="value"]속성 선택자로 분기하면 호환성이 넓습니다. - 스크롤 그림자: IntersectionObserver/scroll 이벤트로
data-scroll-top같은 플래그를 토글하는 방식이 대안입니다. (MDN: IntersectionObserver)
요약(3~5줄)
scroll-state컨테이너 쿼리는 스크롤 가능 방향을 CSS가 직접 질의해 그림자를 토글할 수 있게 합니다. (MDN: Container scroll-state queries)- 타입이 있는
attr()는 HTML 속성 값을 색/숫자/키워드로 파싱하고, 실패 시 fallback으로 안전하게 복구합니다. (Chrome for Developers: advanced attr()) @supports로 감싸면 점진적 향상(Progressive Enhancement) 패턴으로 적용하기 쉽습니다. (MDN: Conditional rules)
결론
타이포그래피와 그리드 정렬처럼 “디테일이 곧 품질”인 UI에서는, JS로 상태를 추적하기보다 CSS가 상태/입력값을 직접 다루게 만드는 편이 유지보수에 유리합니다.
scroll-state 쿼리와 타입 있는 attr()는 HTML–CSS 연결을 한 단계 더 강하게 만들어 줍니다. (MDN: Container scroll-state queries)