CSS animation-timeline — 스크롤 오버플로우를 감지해 Border-Radius 동적 조절하기
스크롤바가 나타나면 어색해지는 문제
코드 블록이나 스크롤 컨테이너에 큰 border-radius를 적용했을 때, 내용이 짧으면 예쁘게 보인다. 하지만 내용이 길어져서 스크롤바가 생기면 어색해진다.
- 스크롤바가 오른쪽 곡선 경계에서 튀어나오는 것처럼 보임
- 곡률이 크면 클수록 더 어색함
- 콘텐츠 길이는 동적이라 미리 알 수 없음
JavaScript로 오버플로우를 감지해서 클래스를 토글하는 방법이 일반적이다. 그런데 순수 CSS로도 가능하다.
[💡 잠깐! 이 용어는?]
animation-timeline: CSS 애니메이션의 진행을 시간이 아닌 다른 기준(스크롤 위치 등)으로 제어하는 속성. scroll() 함수와 함께 쓰면 스크롤 위치에 따라 애니메이션이 진행된다.
핵심 아이디어: scroll() 타임라인
CSS animation-timeline: scroll() 은 스크롤 위치를 애니메이션 타임라인으로 사용한다. 스크롤이 없으면 타임라인이 0에서 멈추고, 스크롤이 생기면 진행한다.
이 원리를 오버플로우 감지에 활용한다.
오버플로우 없음 → 스크롤 없음 → 타임라인 = 0 → 큰 border-radius
오버플로우 있음 → 스크롤 있음 → 타임라인 > 0 → 작은 border-radius
비유하면 물이 가득 차야 흐르는 수조와 같다. 물(콘텐츠)이 조금 있으면 흐르지 않고(스크롤 없음), 가득 차면 넘쳐서 흐른다(스크롤 생김). 그 흐름을 감지한다.
구현
1단계: 오버플로우 감지 애니메이션 설정
.code-block {
overflow: auto;
border-radius: 1.5rem; /* 기본값: 큰 곡률 */
/* X축과 Y축 스크롤을 별도로 감지 */
animation:
detect-overflow-x steps(1) both,
detect-overflow-y steps(1) both;
animation-timeline:
scroll(x self), /* X축 스크롤 */
scroll(y self); /* Y축 스크롤 */
/* 스크롤 범위: 1px만 스크롤 가능해도 감지 */
animation-range: 0px 1px;
}scroll(x self) 는 해당 요소 자신의 X축 스크롤을 타임라인으로 쓴다는 의미다. 스크롤이 1px이라도 생기면 애니메이션이 진행된다.
2단계: 커스텀 프로퍼티로 상태 설정
@keyframes detect-overflow-x {
from { --overflows-x: 0; }
to { --overflows-x: 1; }
}
@keyframes detect-overflow-y {
from { --overflows-y: 0; }
to { --overflows-y: 1; }
}
/* 정수형으로 등록해야 계산에서 사용 가능 */
@property --overflows-x {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@property --overflows-y {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}[💡 잠깐! 이 용어는?]
@property: CSS custom property(변수)의 타입을 명시하는 규칙. 타입을 지정하면 calc() 계산이나 트랜지션 적용이 가능해진다. syntax: '<integer>'로 선언하면 숫자 연산이 가능하다.
오버플로우가 없으면 --overflows-x: 0, 있으면 --overflows-x: 1. 간단한 0/1 플래그다.
3단계: border-radius를 오버플로우에 따라 조절
스크롤바는 오른쪽(Y축) 또는 하단(X축)에 생긴다. 각 모서리에 맞게 곡률을 조절한다.
.code-block {
--r: 1.5rem; /* 기본 곡률 */
--s: 1.2rem; /* 스크롤바가 있을 때 줄이는 양 */
border-top-left-radius: var(--r);
border-top-right-radius: calc(
var(--r) - var(--overflows-y, 0) * var(--s)
);
border-bottom-right-radius: calc(
var(--r) - max(var(--overflows-x, 0), var(--overflows-y, 0)) * var(--s)
);
border-bottom-left-radius: calc(
var(--r) - var(--overflows-x, 0) * var(--s)
);
}| 모서리 | 영향받는 스크롤바 | 계산식 |
|---|---|---|
| top-left | 없음 | --r 고정 |
| top-right | Y축 (오른쪽) | --r - overflows-y * --s |
| bottom-right | X+Y 모두 | --r - max(X, Y) * --s |
| bottom-left | X축 (하단) | --r - overflows-x * --s |
전체 코드
@property --overflows-x {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@property --overflows-y {
syntax: '<integer>';
inherits: false;
initial-value: 0;
}
@keyframes detect-overflow-x {
from { --overflows-x: 0; }
to { --overflows-x: 1; }
}
@keyframes detect-overflow-y {
from { --overflows-y: 0; }
to { --overflows-y: 1; }
}
.code-block {
--r: 1.5rem;
--s: 1.2rem;
overflow: auto;
padding: 1.5rem;
background: #1e1e2e;
border-top-left-radius: var(--r);
border-top-right-radius: calc(var(--r) - var(--overflows-y) * var(--s));
border-bottom-right-radius: calc(var(--r) - max(var(--overflows-x), var(--overflows-y)) * var(--s));
border-bottom-left-radius: calc(var(--r) - var(--overflows-x) * var(--s));
animation:
detect-overflow-x steps(1) both,
detect-overflow-y steps(1) both;
animation-timeline:
scroll(x self),
scroll(y self);
animation-range: 0px 1px;
}동작 확인
콘텐츠 짧음 → 스크롤바 없음
[╔══════════════╗]
[║ ║]
[╚══════════════╝] ← 모서리 전부 둥글게
수직 스크롤바 생김
[╔══════════╗ ]
[║ ║ ↕ ║]
[╚══════════╝ ] ← 오른쪽 모서리 각짐
수평 스크롤바 생김
[╔══════════════╗]
[║ ║]
[╚═══════════↔══╝] ← 하단 모서리 각짐
브라우저 지원
| 브라우저 | 지원 여부 |
|---|---|
| Chrome 115+ | ✅ |
| Safari 17.4+ | ✅ |
| Firefox | ❌ (scroll() 미지원) |
| Edge 115+ | ✅ |
파이어폭스는 아직 scroll() 타임라인을 지원하지 않는다. 미지원 브라우저에서는 기본 border-radius가 고정 적용된다. 폴백으로 작은 곡률을 쓰거나, JavaScript로 보완하는 방식을 고려해야 한다.
.code-block {
/* 폴백: 모든 브라우저에서 안전한 작은 곡률 */
border-radius: 0.5rem;
}
@supports (animation-timeline: scroll()) {
.code-block {
/* 스마트 border-radius 적용 */
border-radius: unset;
border-top-left-radius: var(--r);
/* ... 나머지 설정 */
}
}어디에 쓸 수 있나
이 기법이 잘 맞는 케이스:
- 코드 블록 — 긴 코드 줄이 있을 때 수평 스크롤 발생, 코너 어색함 해결
- 댓글/텍스트 영역 — 내용 길이에 따라 스크롤 생길 수 있는 컨테이너
- 테이블 래퍼 — 좁은 화면에서 수평 스크롤이 필요한 테이블
어울리지 않는 케이스:
- 스크롤이 거의 없는 고정 크기 컨테이너 (과도한 설정)
- 파이어폭스 지원이 반드시 필요한 서비스 (현재는 미지원)
정리
- CSS
animation-timeline: scroll(self)로 특정 요소의 스크롤 유무를 감지할 수 있다. @keyframes와@property를 조합해 오버플로우 상태를0/1정수 변수로 추적한다.calc()로 각 모서리의border-radius를 스크롤바 위치에 맞게 자동 조절한다.- JavaScript 없는 순수 CSS 해결책이지만, 현재 Firefox에서는 작동하지 않는다.
CSS의 animation-timeline이 단순한 스크롤 애니메이션을 넘어서 상태 감지에도 활용될 수 있다는 점이 흥미롭다. 이 트릭이 파이어폭스에서도 지원되면 훨씬 넓은 곳에 쓸 수 있을 것이다.
참고:
- Pqina — Updating CSS Border Radius When A Container Is Overflowing: https://pqina.nl/blog/update-container-border-radius-scrollbar-overflow/
- MDN — animation-timeline: https://developer.mozilla.org/en-US/docs/Web/CSS/animation-timeline
- MDN — @property: https://developer.mozilla.org/en-US/docs/Web/CSS/@property
관심 있을 만한 포스트
12컬럼 그리드 없이 반응형 레이아웃 — CSS Grid와 Flexbox 비교
Bootstrap의 12컬럼 시스템을 CSS Grid와 Flexbox만으로 대체하는 현대적 레이아웃 패턴을 비교한다.
CSS linear() Easing — 자바스크립트 없이 스프링 애니메이션 만들기
CSS linear() 함수로 바운스와 탄성을 가진 스프링 애니메이션을 순수 CSS만으로 구현하는 방법을 정리한다.
SVG 아이콘 — 코드 배포 없이 프로덕트 팀이 직접 관리하는 법
CSS mask-image와 S3를 조합해 개발자 개입 없이 아이콘을 교체하는 패턴을 소개한다.
CSS 한 줄로 업그레이드 — 지금 바로 적용할 수 있는 12가지 모던 CSS 속성
aspect-ratio부터 scrollbar-gutter까지, 한 줄 추가로 스타일시트를 현대화하는 12가지 CSS 속성을 정리한다.
모던 CSS 컴포넌트 아키텍처 — 네이티브 기능만으로 설계하는 컴포넌트 시스템
CSS Nesting, Cascade Layers, Container Queries, :has() 등 네이티브 CSS 기능으로 컴포넌트 기반 아키텍처를 구축하는 방법을 정리한다.
CSS @property — 커스텀 속성에 타입을 부여하는 방법
CSS @property at-rule로 커스텀 속성에 타입 정의, 상속 제어, 폴백을 추가해 렌더링 안정성과 애니메이션 가능성을 확보하는 방법을 다룬다.
CSS :near() — 마우스가 '가까이' 오면 반응하는 새로운 의사 클래스
CSS Working Group에 제안된 :near() 의사 클래스는 포인터 근접성을 감지해 호버 전에 UI를 활성화하는 새로운 상호작용 패턴을 연다.
CSS만으로 커스텀 셀렉트 박스 — JavaScript 150줄이 사라지는 순간
Chrome 135에 도입된 appearance: base-select와 sibling-index()로 JavaScript 없이 완전한 커스텀 드롭다운을 구현하는 방법을 분석한다.