모션 디자인
흐름을 명확하게 하는 절제된 모션 — compositor 친화 속성, 지속 시간·이징 체계, 스프링(공간·효과) 물리, 로딩·전환 패턴, AI Thinking, 그리고 prefers-reduced-motion 대응을 정의합니다.
마지막 업데이트 2026-06-15
모션 원칙#
모션은 장식이 아니라 흐름을 설명하는 언어입니다. WDS의 모션은 네 가지 규칙을 따릅니다.
- 목적이 있을 때만 — 상태 변화·공간 관계·연속성을 전달할 때만 움직입니다. 이유 없는 애니메이션은 넣지 않습니다.
- compositor 친화 속성만 —
transform·opacity만 전환합니다.width·height·top·margin등 레이아웃을 흔드는 속성은 애니메이션하지 않습니다(리플로우·재깅 방지). - 빠르고 절제되게 — 대부분의 전환은 150–250ms입니다. 길고 화려한 모션은 작업을 방해합니다.
- 언제나 끌 수 있게 —
prefers-reduced-motion을 전면 존중합니다. 모션이 정보를 전달한다면, 모션을 끈 사용자에게는 정적 대체를 제공합니다.
지속 시간#
지속 시간은 세 단계 토큰으로 고정합니다 — 화면마다 다른 값을 쓰지 않습니다.
아래 플레이어로 duration·ease·spring 토큰을 직접 재생해 보세요 — specimen이 실제로 움직이고, 같은 진행도로 곡선 위 플레이헤드가 달립니다. "모션 감소"를 켜면 모션을 끈 사용자에게 제공하는 정적 대체(최종 상태 + 곡선 + 수치)를 그 자리에서 미리볼 수 있습니다.
| 토큰 | 용도 |
|---|---|
duration.fast | hover·focus·active 등 즉각 피드백, 작은 요소 토글 |
duration.normal | 패널·드롭다운·모달 등장/퇴장, 탭 전환 |
duration.slow | 큰 표면(시트·드로어·페이지) 전환, 강조 연출 |
이징은 다섯 가지를 씁니다. 기본 전환·짧은 등장에는 ease.out(빠르게 시작해 부드럽게 감속),
양방향 연속 전환에는 ease.standard를 씁니다. 강조 모션이 필요한 긴 거리·강한 표현에는
M3 emphasized 계열을 씁니다 — ease.emphasized는 양방향 강조 전환,
ease.emphasized-decelerate는 화면 안으로 들어오며 감속하는 등장,
ease.emphasized-accelerate는 화면 밖으로 나가며 가속하는 퇴장에 씁니다.
| 토큰 | 값 | 설명 |
|---|---|---|
| — | ||
| — | ||
| — | ||
| — | ||
| — | ||
| M3 emphasized — 화면 전환·강조 모션의 기본 (CSS 근사) | ||
| M3 emphasized decelerate — 표면 등장(enter). 오버레이 패널 진입에 사용 | ||
| M3 emphasized accelerate — 표면 퇴장(exit) |
상태 전환#
hover·focus·active는 duration.fast로 즉시 반응합니다. 색·그림자·약간의 transform만 바꿔
손맛을 주되, 레이아웃은 고정합니다. 아래 버튼에 마우스를 올리고 키보드로 포커스해 보세요.
포커스 링은 모션이 아니라 가시성이 핵심입니다 — 자세한 규칙은 접근성에서 다룹니다.
표면 전환#
떠 있는 표면은 등장 방향과 속도로 공간 관계를 전달합니다.
| 표면 | 모션 | 지속 |
|---|---|---|
| 드롭다운·툴팁·팝오버 | opacity + 4–8px translateY | duration.fast |
| 모달·다이얼로그 | opacity + 살짝 scale(0.98→1) | duration.normal |
| 바텀시트·드로어·사이드시트 | 가장자리에서 슬라이드 인 | duration.slow |
방향은 의미를 가집니다 — 바텀시트는 아래에서, 사이드시트는 옆에서 들어와 "어디서 왔는지"를
남깁니다. 오버레이(모달·드로어·바텀시트·팝오버) 등장에는 ease.emphasized-decelerate를 써서
화면 안으로 들어오며 감속하는 "안착감"을 줍니다. 닫힘은 같은 경로를 ease.emphasized-accelerate로
역재생하며, 빠르게 가속해 화면을 벗어납니다.
표면이 위치·크기로 움직일 때(모달의 살짝 scale, 시트의 슬라이드)는 cubic-bezier 대신
아래 스프링 절의 공간 spring 토큰(spring.spatial.*)을 써서 약간의 오버슈트로
"물리적으로 안착하는" 손맛을 줄 수 있습니다. 표면의 색·불투명도·blur 전환에는 오버슈트
없는 효과 spring(spring.effect.*)을 씁니다.
스프링(Spring)#
cubic-bezier 이징이 "정해진 곡선을 시간에 맞춰 따라가는" 모션이라면, 스프링은 질량·강성·감쇠 라는 물리에서 곡선이 자연스럽게 도출되는 모션입니다. 절제 원칙은 그대로입니다 — 스프링은 화려한 연출이 아니라, 표면이 "물리적으로 안착한다"는 손맛을 더하는 강조 모션에만 씁니다.
WDS 스프링은 두 갈래로 나뉩니다. 무엇이 움직이는가에 따라 다른 물리를 씁니다.
- 공간(spatial) — 위치·크기·레이아웃이 움직이는 모션. 약간 과소감쇠(damping ratio 0.8)라 목표를 살짝 지나쳤다가 돌아오는 미세한 오버슈트가 있습니다. 이 오버슈트가 생동감(personality)입니다.
- 효과(effect) — 색·불투명도·blur가 변하는 모션. 임계감쇠(damping ratio 1.0)라 오버슈트 없이 가장 빠르게 안착합니다. 색이 출렁이면 잔상처럼 보여 가독을 해치므로 오버슈트를 두지 않습니다.
각 스프링은 빌드 타임에 CSS linear() 이징 함수와 정착 시간(duration) 두 변수로 출력됩니다.
질량-스프링-감쇠 방정식의 닫힌 해를 32점 샘플링해 linear() 점열을 만들고, 변위가 목표의 0.5%
이내로 안착하는 시각을 duration으로 잡습니다. 웹은 var(--wds-spring-*) 이징과
var(--wds-spring-*-duration)을 transition에 함께 쓰고, Flutter는 동일 토큰에서 생성된
WizSprings의 SpringDescription을 씁니다 — 웹과 Flutter가 같은 물리에서 나옵니다.
| 토큰 | 종류 | 강성·감쇠비 | 정착(ms) | 용처 |
|---|---|---|---|---|
spring.spatial.default | 공간 | k380 · ζ0.8 | 372ms | 위치·크기·레이아웃 전환 기본 |
spring.spatial.fast | 공간 | k800 · ζ0.8 | 256ms | 작은 요소·즉각 반응 |
spring.spatial.slow | 공간 | k200 · ζ0.8 | 512ms | 큰 표면·강조 등장 전용 — 작은 컨트롤·고빈도 전환엔 쓰지 말 것 |
spring.effect.default | 효과 | k1600 · ζ1.0 | 188ms | 색·불투명도·blur 전환 기본 |
spring.effect.fast | 효과 | k3800 · ζ1.0 | 124ms | 즉각 피드백의 색·불투명도 |
강성(k)이 높을수록 빠르게 안착합니다. 감쇠비(ζ)는 오버슈트의 유무를 정합니다 — 공간 스프링의
0.8은 절제된 오버슈트, 효과 스프링의 1.0은 오버슈트 없음입니다. 값은 Material 3 Expressive의
spring 모션 토큰을 기준으로 큐레이션했습니다. 정착 시간은 빌드 타임 결정값입니다 — 토큰 빌드가
linear()·--wds-spring-*-duration에 같은 ms 값을 박으므로 연쇄(choreography) 타이밍에 그대로 쓸 수 있습니다.
/* 공간 spring — 카드가 살짝 오버슈트하며 안착 (transform만 — 레이아웃 무흔들) */
.card {
transition:
transform var(--wds-spring-spatial-default-duration) var(--wds-spring-spatial-default);
}
/* 효과 spring — 색·불투명도는 오버슈트 없이 안착 */
.surface {
transition:
opacity var(--wds-spring-effect-default-duration) var(--wds-spring-effect-default);
}
스프링도 레이아웃 속성은 애니메이션하지 않습니다(transform·opacity·clip-path만). 그리고
prefers-reduced-motion에서는 다른 모션과 동일하게 전환을 제거하고 즉시 표시합니다(아래 표 참조).
Flutter에서는 SpringDescription이 물리 상수일 뿐이므로, 스프링 구동 애니메이션은
MediaQuery.disableAnimationsOf(context)로 감싸 reduce 시 최종 값으로 즉시 스냅해야 합니다
(웹 전역 가드와 같은 정신 — 기존 wiz_modal·wiz_toast 패턴 계승).
로딩#
대기 시간의 성격에 따라 표현을 고릅니다.
- Spinner — 길이를 모르는 짧은 대기. 행동 옆이나 영역 중앙에 둡니다.
- Skeleton — 곧 들어올 콘텐츠의 형태를 미리 보여 줄 때(레이아웃 점프 방지).
- Progress — 진행률을 아는 작업(업로드 등)은 결정 모드, 모르면 indeterminate.
AI Thinking#
AI 상태는 모션으로 "지금 무엇을 하는지"를 전달합니다 — 멈춘 화면은 멈춘 시스템처럼 보입니다.
- Thinking — 추론 중. 은은한 펄스/도트로 "살아 있음"을 표시하되 진행률을 약속하지 않습니다.
- Streaming — 응답 생성 중. 텍스트가 흐르듯 나타나고 커서가 깜빡입니다.
- Tool Calling — 외부 도구 호출. 도구 라벨과 함께 indeterminate 표시.
상태 정의와 표현의 정본은 AI 가이드라인의 AI State Model 9종입니다.
모든 AI Thinking 모션도 prefers-reduced-motion에서는 정적 라벨로 대체합니다.
Reduced Motion 대응#
prefers-reduced-motion: reduce를 켠 사용자에게는 모션을 제거하되 정보는 유지합니다 —
애니메이션이 유일한 단서가 되어선 안 됩니다.
| 모션 | reduce 시 대체 |
|---|---|
| 등장/퇴장 전환 | 전환 제거, 즉시 표시/숨김 |
| 스프링 전환(공간·효과) | spring 전환 제거, 즉시 최종 상태로 표시(오버슈트 없음) |
| Spinner 회전 | 회전 정지 + "로딩 중" 텍스트 유지 |
| Progress indeterminate 왕복 | 왕복 정지 + 라벨 유지 |
| Skeleton shimmer | shimmer 제거, 정적 플레이스홀더 유지 |
| AI Thinking 펄스 | 펄스 제거, 상태 라벨 텍스트 유지 |
스프링을 포함한 CSS 전환은 WDS 전역 베이스(tokens/wds-base.css의 .wds-root 스코프)가
prefers-reduced-motion에서 transition·animation 지속 시간을 ~0으로 강제하므로 자동으로 최종 상태에
스냅됩니다 — 오버슈트가 새지 않습니다. 컴포넌트 CSS의 @media (prefers-reduced-motion: reduce)
블록은 JS 구동·비-transition 모션을 위한 규약으로 남기며, 신규 모션을 추가할 때 이 대체를 함께 정의합니다.