모션 디자인
흐름을 명확하게 하는 절제된 모션 — compositor 친화 속성, 지속 시간·이징 체계, 스프링(공간·효과) 물리, 관성·투영·러버밴드 상호작용, AI Thinking, 그리고 prefers-reduced-motion 대응을 정의합니다.
마지막 업데이트 2026-07-01
모션 원칙#
모션은 장식이 아니라 흐름을 설명하는 언어입니다. WDS의 모션은 네 가지 규칙을 따릅니다.
- 목적이 있을 때만 — 상태 변화·공간 관계·연속성을 전달할 때만 움직입니다. 이유 없는 애니메이션은 넣지 않습니다.
- compositor 친화 속성만 —
transform·opacity만 전환합니다.width·height·top·margin등 레이아웃을 흔드는 속성은 애니메이션하지 않습니다(리플로우·재깅 방지). - 빠르고 절제되게 — 대부분의 전환은 150–250ms입니다. 길고 화려한 모션은 작업을 방해합니다.
- 언제나 끌 수 있게 —
prefers-reduced-motion을 전면 존중합니다. 모션이 정보를 전달한다면, 모션을 끈 사용자에게는 정적 대체를 제공합니다.
인터랙션 물리#
WDS는 직접 조작에서 손을 뗀 뒤의 의도와 공간 관계가 끊기지 않도록 관성·투영·저항·정착 원리를 웹 컴포넌트에 맞게 적용합니다. 목표는 특정 플랫폼을 모방하는 것이 아니라, WIZ Factory 제품군에서 일관된 손맛과 예측 가능한 상태 전환을 제공하는 것입니다.
- 투영(projection) — release 위치만 보지 않고 속도로 예상 착지점을 계산합니다. BottomSheet는
projectMomentum으로 닫힘·디텐트 판정을 결정하고, ScrollRail·ChipGroup은 release velocity를animateMomentumScroll에 시드해 마찰 글라이드 뒤nearestChildOffset스냅으로 정착합니다. ScrollRail의 버튼·키보드 명령도 목표 위치를 같은 projection velocity로 환산해 동일한 스냅 엔진으로 보냅니다. - 러버밴드(rubber-band) — 경계 밖 드래그는 거리 그대로 늘리지 않고 저항 곡선으로 압축합니다. release 시에는
spring.spatial.fast로 원래 transform에 복귀해 끌림과 정착이 분리됩니다. BottomSheet는 하단으로 깊게 끌수록 visual offset을 압축하고, ScrollRail·ChipGroup은useDragScroll의 옵트인 overscroll로 같은 원리를 적용합니다. - 자석 스냅과 정착 — 가로 레일은 마찰 글라이드 뒤
spring.spatial계열 스냅으로 가장 가까운 항목에 정착합니다. ReorderList는 드래그 중 live gap으로 공간을 열고, release 후 FLIP으로 이전 위치와 새 위치를 연결해spring.spatial.fast로 정착합니다. - 충돌 회피(floating geometry) — 툴팁·차트 값 카드·메뉴처럼 떠 있는 표면은 앵커를 기준으로 먼저 배치한 뒤, 화면·스크롤 경계와 겹치면
flip → shift → clamp순서로 보정합니다. 말풍선은 잘리지 않게 안쪽으로 이동하되, 화살표와transform-origin은 가능한 한 원래 앵커를 가리킵니다. 선택·날짜·드롭다운 계열 표면은 최소 조작 높이를 보장하고 resize/scroll 때 다시 측정하며, 등장 scale은 보정된 origin에서 시작합니다. - 거리 기반 retarget — 선택 인디케이터는 목표가 바뀔 때 이동 거리
d = sqrt(dx² + dy²)를 계산해 정착 시간을 보정합니다. 작은 이동은 빠르게, 긴 이동은 상한 안에서 조금 더 길게 움직여 같은 컨트롤 안의 속도감이 균일하게 보입니다. - 연속성(continuity) — Tabs는 지원 브라우저에서
useViewTransition으로 같은 문서 전환을 연결하고, 미지원·reduce 환경에서는 즉시 상태를 바꿉니다. - 모션 감소 — JS가 구동하는 관성·러버밴드·View Transition도 reduced-motion을 확인합니다. reduce에서는 의도한 최종 상태를 즉시 보여 주는 것이 기본입니다.
지속 시간#
지속 시간은 세 단계 토큰으로 고정합니다 — 화면마다 다른 값을 쓰지 않습니다.
아래 플레이어로 duration·ease·spring 토큰을 직접 재생해 보세요 — specimen이 실제로 움직이고, 같은 진행도로 곡선 위 플레이헤드가 달립니다. “모션 감소”를 켜면 모션을 끈 사용자에게 제공하는 정적 대체(최종 상태 + 곡선 + 수치)를 그 자리에서 미리볼 수 있습니다.
| 토큰 | 용도 |
|---|---|
duration.fast | hover·focus·active 등 즉각 피드백, 작은 요소 토글 |
duration.normal | 패널·드롭다운·모달 등장/퇴장, 탭 전환 |
duration.slow | 큰 표면(시트·드로어·페이지) 전환, 강조 연출 |
이징은 다섯 가지를 씁니다. 기본 전환·짧은 등장에는 ease.out(빠르게 시작해 부드럽게 감속),
양방향 연속 전환에는 ease.standard를 씁니다. 강조 모션이 필요한 긴 거리·강한 표현에는
WDS 강조 이징 계열을 씁니다 — ease.emphasized는 양방향 강조 전환,
ease.emphasized-decelerate는 화면 안으로 들어오며 감속하는 등장,
ease.emphasized-accelerate는 화면 밖으로 나가며 가속하는 퇴장에 씁니다.
| 토큰 | 값 | 설명 |
|---|---|---|
상태 전환#
hover·focus·active는 duration.fast로 즉시 반응합니다. 색·그림자·약간의 transform만 바꿔
손맛을 주되, 레이아웃은 고정합니다. 아래 버튼에 마우스를 올리고 키보드로 포커스해 보세요.
포커스 링은 모션이 아니라 가시성이 핵심입니다 — 자세한 규칙은 접근성에서 다룹니다.
표면 전환#
떠 있는 표면은 등장 방향과 속도로 공간 관계를 전달합니다.
| 표면 | 모션 | 지속 |
|---|---|---|
| 드롭다운·선택 목록·날짜 팝업·툴팁·팝오버 | opacity + anchor-origin scale(0.98→1) + 4–8px translate, 드롭다운·선택 목록·날짜 팝업·팝오버 닫힘은 closing 상태 유지 후 같은 origin으로 fade/scale-out | spring.effect.fast |
| 모달·다이얼로그 | opacity + center-origin scale(0.96→1) + 4–8px translate, 닫힘은 closing 상태 유지 후 unmount | spring.spatial.default + exit spring.effect.default |
| SearchCommand | opacity + top-origin scale(0.97→1) + 8px translate, 닫힘은 closing 상태 유지 후 unmount | spring.spatial.fast + exit spring.effect.default |
| 바텀시트·드로어·사이드시트 | 가장자리에서 슬라이드 인 | duration.slow |
방향은 의미를 가집니다 — 바텀시트는 아래에서, 사이드시트는 옆에서 들어와 “어디서 왔는지”를
남깁니다. 오버레이(모달·드로어·바텀시트·팝오버) 등장에는 ease.emphasized-decelerate를 써서
화면 안으로 들어오며 감속하는 “안착감”을 줍니다. 닫힘은 같은 경로를 ease.emphasized-accelerate로
역재생하며, 빠르게 가속해 화면을 벗어납니다.
표면이 위치·크기로 움직일 때(모달의 살짝 scale, 시트의 슬라이드)는 cubic-bezier 대신
아래 스프링 절의 공간 spring 토큰(spring.spatial.*)을 써서 약간의 오버슈트로
“물리적으로 안착하는” 손맛을 줄 수 있습니다. 표면의 색·불투명도·blur 전환에는 오버슈트
없는 효과 spring(spring.effect.*)을 씁니다.
단, 툴팁·메뉴·선택 목록처럼 viewport edge에서 clamp되는 작은 floating surface의 anchor-origin
scale-in은 오버슈트가 경계 밖 clipping으로 보일 수 있으므로 spring.effect.fast를 씁니다. 움직임은
앵커에서 시작하되, 크기는 목표 bounds를 넘지 않는 critical damping으로 안착합니다.
스프링(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은 오버슈트 없음입니다. 값은 WDS의 업무 화면 밀도,
직접 조작 빈도, 색·투명도 가독성 기준으로 큐레이션했습니다. 정착 시간은 빌드 타임 결정값입니다 — 토큰 빌드가
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에서는 정적 라벨로 대체합니다.
컴포넌트 적용 현황#
아래 표는 현재 구현 기준입니다. WDS 인터랙션 물리 원리를 설명하되, 각 컴포넌트가 특정 플랫폼 컴포넌트와 완전한 동등성을 갖는다는 뜻은 아닙니다.
| 컴포넌트 | 상태 | 현재 적용 | 보류·주의 |
|---|---|---|---|
| ScrollRail | 적용됨 | useDragScroll + animateMomentumScroll로 마우스 드래그 관성, 러버밴드, release springback, 자석 스냅을 적용합니다. 이전/다음 버튼과 ArrowLeft/ArrowRight도 목표 카드 위치를 projection velocity로 변환해 같은 momentum/snap 엔진으로 정착합니다 | 터치·펜은 브라우저 네이티브 스크롤에 맡깁니다 |
| ChipGroup | 적용됨 | ScrollRail과 같은 momentum/rubber-band/snap 프리미티브를 씁니다 | 콘텐츠가 넘칠 때만 드래그 스크롤이 의미 있습니다 |
| BottomSheet | 적용됨 | projectMomentum 기반 release 판정으로 닫힘·half/full 디텐트를 결정하고, 드래그 추적 중에는 transition을 끄며 visual offset 러버밴드와 progress 기반 스크림 opacity를 적용합니다. open=false 전환에서는 closing 상태로 패널을 유지하고, release 위치·속도·남은 거리로 exit duration을 보정해 하단 exit 모션을 완료한 뒤 미마운트합니다 | 소비자가 컴포넌트 자체를 조건부 렌더링하면 exit 유지 계약을 우회합니다 |
| Tooltip | 보강됨 | floating 모드에서 공통 geometry solver가 앵커·버블·뷰포트 치수를 계산해 top/bottom/left/right 배치, edge shift, arrow clamp, anchor-origin scale-in을 처리합니다 | 기본 인플로우 모드는 기존 위치 규약을 유지합니다 |
| Chart tooltip | 보강됨 | 활성 포인트 바로 위에 값 카드를 띄우고, 가장자리에서는 공통 geometry solver로 좌우 shift와 상하 flip을 적용합니다 | 실제 rect 기반 시각 검증은 브라우저 테스트로 확장 예정입니다 |
| Select | 보강됨 | 데스크톱 listbox가 최소 조작 공간을 기준으로 top/bottom flip하고, viewport edge에서는 수평 shift, max-height, trigger-center transform-origin scale-in을 적용합니다. 선택·Escape·외부 클릭 닫힘은 closing 상태로 같은 origin의 fade/scale-out을 완료한 뒤 unmount합니다 | compact는 BottomSheet 표면을 유지합니다 |
| DatePicker | 보강됨 | docked popup이 Select와 같은 anchored surface 규칙으로 flip·shift·max-height와 trigger-center transform-origin scale-in을 계산합니다. 선택 완료·Escape·외부 클릭 닫힘은 closing 상태로 같은 origin의 fade/scale-out을 완료한 뒤 unmount합니다 | sheet/modal 변형은 기존 draft 확인 흐름을 유지합니다 |
| DropdownMenu | 보강됨 | 메인 메뉴가 trigger 주변 viewport 공간을 재측정해 top/bottom flip, 수평 shift, 내부 스크롤 max-height, anchor-origin scale-in을 적용하고, 닫힘은 closing 상태로 anchor-origin fade/scale-out을 완료한 뒤 unmount합니다. 서브메뉴는 부모 항목 기준으로 left/right flip과 edge shift 및 side-aware origin을 적용합니다 | 깊은 중첩 메뉴는 별도 검증 대상입니다 |
| ColorPicker | 보강됨 | floating 모드의 body portal fixed 패널이 viewport 안으로 top/left를 clamp하고, trigger-center transform-origin scale-in을 적용합니다 | 기본 인플로우 popup은 기존 위치 규약을 유지합니다 |
| Modal | 보강됨 | open=false 전환에서 즉시 언마운트하지 않고 closing 상태로 패널과 스크림을 유지합니다. 진입은 center-origin spatial spring, 닫힘은 effect duration 동안 opacity/scale을 정리한 뒤 focus·scroll·inert를 복원합니다 | 소비자가 컴포넌트 자체를 조건부 렌더링하면 exit 유지 계약을 우회합니다 |
| SearchCommand | 보강됨 | command palette가 top-origin spatial spring으로 viewport 안에서 열리고, Escape·닫기 버튼·스크림 닫힘은 closing 상태를 거친 뒤 focus·scroll·inert를 복원합니다 | 모바일 full-screen 표면도 같은 lifecycle을 공유합니다 |
| SegmentedButton | 보강됨 | single 선택 thumb가 이전 rect와 다음 rect의 거리로 retarget duration을 산출해 --wds-spring-spatial-fast-duration 위에 런타임 인라인 오버라이드로 얹어 속도를 보정합니다 | multi 모드는 thumb 없이 pressed 상태만 사용합니다 |
| BadgeToggle | 보강됨 | 선택 pill thumb가 같은 거리 기반 retarget 규칙을 사용합니다 | 줄바꿈된 필 그룹에서는 y축 이동도 같은 거리 계산에 포함됩니다 |
| Tabs | 적용됨 | useViewTransition으로 지원 브라우저에서 같은 문서 전환을 연결합니다 | 라우트 전환 전체로 확장하지 않았고 feature-detect 폴백을 유지합니다 |
| Popover | 보강됨 | 지원 브라우저에서는 showPopover({ source })로 top layer에 올리고 viewport 기준 fixed 좌표를 씁니다. 닫힘은 closing 상태로 top layer와 geometry를 유지해 anchor-origin fade/scale-out을 완료한 뒤 정리합니다. CSS Anchor Positioning 지원 시 고유 anchor name을 연결하고, 미지원 환경은 기존 clipping ancestor 기준 anchored geometry로 폴백합니다 | CSS Anchor Positioning 단독 full migration은 보류입니다 |
| AnimatedIcon | 보강됨 | CSS-only effect와 생성 아이콘의 `data-wds-icon-layer=“primary | secondary”` 훅으로 레이어별 타이밍을 씁니다 |
| AnimatedNumber | 보강됨 | easeOutCubic count-up, 방향 metadata(`up | down |
| ReorderList | 적용됨 | 포인터 드래그 중 active row를 raised surface로 lift하고, 이동 범위의 형제 row를 translate3d로 밀어 live insertion gap을 만듭니다. 긴 목록은 edge drag에서 가장 가까운 스크롤 컨테이너를 따라 스크롤하고, release 뒤 FLIP + spring.spatial.fast로 항목 위치를 정착합니다 | reduced-motion에서는 전환을 끄고 즉시 새 순서를 보여 줍니다 |
Reduced Motion 대응#
prefers-reduced-motion: reduce를 켠 사용자에게는 모션을 제거하되 정보는 유지합니다 —
애니메이션이 유일한 단서가 되어선 안 됩니다.
| 모션 | reduce 시 대체 |
|---|---|
| 등장/퇴장 전환 | 전환 제거, 즉시 표시/숨김 |
| 스프링 전환(공간·효과) | spring 전환 제거, 즉시 최종 상태로 표시(오버슈트 없음) |
| Spinner 회전 | 회전 정지 + “로딩 중” 텍스트 유지 |
| Progress indeterminate 왕복 | 왕복 정지 + 라벨 유지 |
| Skeleton shimmer | shimmer 제거, 정적 플레이스홀더 유지 |
| AI Thinking 펄스 | 펄스 제거, 상태 라벨 텍스트 유지 |
| 러버밴드·시트 정착 | 저항/스프링 전환 제거, release 최종 위치로 즉시 복귀 |
스프링을 포함한 CSS 전환은 WDS 전역 베이스(tokens/wds-base.css의 .wds-root 스코프)가
prefers-reduced-motion에서 transition·animation 지속 시간을 ~0으로 강제하므로 자동으로 최종 상태에
스냅됩니다 — 오버슈트가 새지 않습니다. 컴포넌트 CSS의 @media (prefers-reduced-motion: reduce)
블록은 JS 구동·비-transition 모션을 위한 규약으로 남기며, 신규 모션을 추가할 때 이 대체를 함께 정의합니다.