본문으로 건너뛰기
Centralby 디자인 시스템

기초

모션

움직임은 무엇이 어디서 와서 어디로 갔는지 설명할 때만. 토큰, 거리 표, 패턴별 실시간 데모, 동작 줄이기 미리보기.

한 문장과 원칙

움직임은 “무엇이 어디서 왔고 어디로 갔는가”를 설명할 때만 쓴다. 설명할 것이 없으면 움직이지 않는다.

  1. 인과만 움직인다. 탭·키·스크롤이나 데이터 도착이 원인일 때만. 배경 흐름·호흡·반복·자동재생은 없다(Loader 회전만 예외).
  2. 멈춰도 안다. 모든 모션은 끝 상태만으로 의미가 성립한다. 그래서 동작 줄이기는 다른 디자인이 아니라 같은 끝 상태를 바로 보여주는 것이다.
  3. 출발점은 실제 관계에서. 시트는 아래에서, 팝오버는 트리거 쪽에서, push 상세는 오른쪽에서(RTL은 왼쪽).
  4. 짧게, 한 번에 하나. 한 화면에서 동시에 움직이는 그룹은 하나, 그룹 안 스태거는 최대 3개.
  5. 물처럼 조용하게. 최대 12px(시트·push 제외)를 감속 이징 하나로. 오버슈트·바운스·스프링 없음. scale도 쓰지 않는다.
  6. 브랜드는 한 번. 첫 “아하” 카드 도착만이 브랜드 모먼트다.

토큰

원천은 src/design/tokens/motion.css 하나다. 컴포넌트는 ms 값을 직접 쓰지 않는다(lint R8). 아래 표는 이 페이지의 CSS에서 읽으며, 토글을 켜면 data-motion="reduce" 영역 안에서 다시 읽는다.

모션 토큰
토큰값쓰는 곳
--cb-dur-fast…상태 피드백: hover·pressed·체크·스위치·선택 표시
--cb-dur-base…작은 오버레이·흐름 안 요소의 등장: Popover·Menu·Toast·칩·행 도착
--cb-dur-move…공간 이동: Sheet·Dialog·push·사이드바, 브랜드 모먼트
--cb-dur-fade…reduce에서도 남는 유일한 지속시간(불투명도 전용)
--cb-dur-spin…Loader 회전(유일한 linear·반복 예외)
--cb-ease…모든 전환에 같은 곡선. 빠르게 출발해 길게 감속
--cb-exit-factor…퇴장 = 등장 × 0.7
--cb-stagger…지연 = min(i, 2) × stagger. 1·2·3번째 0·40·80ms, 이후는 80ms
--cb-move-scale…모든 이동 거리에 곱한다. reduce면 0
--cb-ease-ink…잉크 획: 빨리 닿고 길게 감속(끝 접선 = --cb-ease)
--cb-ease-gather…연결 장면 전용 모으기: 느린 출발
--cb-stagger-ink…활자 간격(정렬·등장 총 ≤ 400ms). reduce면 0
--cb-dur-sweep…유리 위 빛 한 줄기가 한 번 지나가는 시간. reduce면 0(생략)

값은 지금 페이지에 적용된 CSS에서 읽습니다.

불투명도는 항상 max()로 쓴다. reduce에서 base가 0이 되어도 120ms 페이드가 남게 하기 위해서다. 이동 거리는 calc(Npx * var(--cb-move-scale))로 쓴다.

.x {
  --_fade-in: max(var(--cb-dur-base), var(--cb-dur-fade));            /* 200 → reduce 120 */
  --_fade-out: max(calc(var(--cb-dur-base) * var(--cb-exit-factor)),
                   calc(var(--cb-dur-fade) * var(--cb-exit-factor)));  /* 140 → reduce 84 */
  transition: opacity var(--_fade-in) var(--cb-ease),
              transform var(--cb-dur-base) var(--cb-ease);             /* reduce 0 */
  transform: translateY(calc(8px * var(--cb-move-scale)));
}

거리와 불투명도

거리 표
이름거리 (× move-scale)쓰는 곳
미세4pxPopover·Menu·Tooltip(트리거 쪽에서), 버튼 안 아이콘 교체
작음8px목록 행 도착, Toast(아래에서), NewItemsChip(위에서), 랜딩 위젯 놓임
브랜드12pxAhaCard(아래에서). 시스템의 최대 미세 거리
대화상자16px가운데 Sheet·Dialog(≥768, 아래에서)
시트100% (자기 높이)바닥 Sheet(phone). 시트 자체 불투명도는 1 고정
push100% (화면 폭)1024 미만 상세 push. 나가는 화면은 정지(패럴랙스 금지)
  • 이렇게 하지 않기불투명도를 중간값(0.5 등)에서 시작하지 않는다. 반투명한 중간 상태는 대비 계산을 벗어난다.
  • 이렇게 하지 않기색 전환은 컨트롤 크기 이하 요소에만. 화면 크기 면의 색 보간(테마 전환 포함)은 하지 않는다.

패턴 데모

각 데모에는 “동작 줄이기 미리보기”가 있다. 켜면 그 영역에 data-motion="reduce"가 붙어 토큰이 바뀌고, 같은 끝 상태가 바로 보인다.

실제 컴포넌트: 상태 피드백과 선택 표시

보기
불러오는 중
색 변화와 손잡이·선택 캡슐 이동은 fast(120ms). 동작 줄이기에서는 즉시 끝 상태. 초점 링은 애니메이션하지 않는다.

실제 컴포넌트: 흐름 안 등장 + 스태거 (행 도착)

  • 주간 계획 회의
  • 가이드 초안 검토
  • 출시 일정 조율
  • 고객 인터뷰 정리
opacity + 8px, base. 지연 0 · 40 · 80 · 80ms. 동작 줄이기에서는 이동 0, 120ms 페이드만 남는다.

실제 컴포넌트: 오버레이 (Popover·Menu)

등장: opacity + 트리거 쪽 4px, base. 퇴장: 이동 없이 opacity base × 0.7. 닫히면 초점이 트리거로 돌아가므로 패널이 트리거로 빨려 들어가지 않는다.

실제 컴포넌트: 바닥 시트 (phone)

translateY(100% → 0), move. 퇴장 move × 0.7. 끌어서 닫기 없음. 동작 줄이기에서는 이동 없이 페이드.

실제 컴포넌트: 토스트·칩

↑ 새 기록 3개
지금은 접어 둠. 실행 취소 가능
토스트는 아래에서 8px, 새 기록 칩은 위에서 8px, 둘 다 base. hover·초점 중에는 타이머만 멈추고 움직임은 없다.

실제 컴포넌트: 로딩: Skeleton은 항상 정지

쉬머·펄스 없음. “불러오는 중”은 빈 상태와 다른 모양과 1초 뒤 status 한 번으로 전달한다.

모션 함수

위젯은 흰 종이 위의 잉크로 움직인다. 정확하고 가볍다. src/design/motion/gen.ts의 아홉 함수(enter · reflow · roll · day · expand · collapse · lift · ripple · underline)는 --cb-ease-ink와 --cb-stagger-ink(24ms)를 쓴다. 위젯 배선은 useWidgetMotion(ref, "general"): 처음 보일 때 한 번 enter, 가리킴·초점에 lift, 44px “다시 보기” 버튼은 replay()를 부른다. 훅은 데이터 속성(data-gen-roll · data-gen-row · data-gen-source · data-gen-title · data-reveal)이다.

등장 + 들어 올림

gen.enter(w) · gen.lift(el, on)

다음 일정까지

14:00

분기 계획 리뷰 준비

예산 초안 공유

Google Calendar

처음 보일 때 한 번: 머리·문장 4px, 큰 값은 활자처럼 갈리고, 행이 24ms 간격으로 6px. 가리키거나 초점이 가면 2px 들림 + 그림자 + 출처 줄 잉크 밑줄. 줄이기: fade 120, 그림자만.

판면 재배치

gen.reflow(getEls, mutate)

    정확한 FLIP(base, 잉크 이징). 새 순서 위에서부터 24ms 간격, 총 ≤ 400ms. 줄이기: 즉시.

    활자 갈림 + 물결

    gen.roll(el, text) · gen.ripple(line)

    14:00

    Google Calendar

    바뀐 글자만 칸 안에서 갈린다(방향은 늘 같아 증감을 말하지 않음). 갱신 때 밝은 짧은 획이 hairline을 한 번 지나간다(move × 2). 줄이기: 글자 + fade, 물결 없음.

    날짜 넘김 + 잔상

    gen.day(box, next, dir)

    옛 하루는 24px 비켜나며 .3으로 잠시 남았다가 사라지고, 새 하루는 반대편에서 12px로 들어온다. 줄이기: 교차 fade.

    카드 → 상세

    gen.expand(card, panel) · gen.collapse(card, panel, done)

    판이 카드 자리에서 미끄러져 오고, 제목은 카드 제목 자리에서 따로 날아온다(크기 그대로). 내용은 판이 자리 잡은 뒤. 닫기는 ×0.7 거꾸로, Esc, 초점은 카드로. 줄이기: fade.

    잉크 선 긋기

    gen.underline(el, delay)

    분기 계획 리뷰 준비

    선 자체가 클립 안에서 밀려 들어온다(move, 잉크 이징). scaleX·덮개 없음. 줄이기: 끝 상태.

    연결 장면은 ConnectScene의 cards-to-record(맨 위 카드가 기록, B→A 2안)와 ink-line(잉크 한 줄)이다. 이동 280ms 토큰을 넘는 유일한 문서화된 예외(계정당 한 번, 건너뛰기·Esc).

    브랜드 모먼트

    첫 가져오기에서 AhaCard가 처음 화면에 들어오는 순간, 계정당 한 번. 카드: opacity 0→1 + translateY 12→0, 280ms. 카드가 자리 잡은 뒤(280ms)에만 Dusk 빛 하나가 피어올라 1.6초 머문 뒤 196ms로 사라진다. 그 2.4초 동안 다른 것은 움직이지 않는다.

    실제 컴포넌트: 첫 “아하” 카드 (실제 AhaCard, phase="arriving")

    2주 전 캘린더에 있던 일

    수요일

    Q4 roadmap sync

    카탈로그의 다시 재생은 서버의 ahaPlayedAt을 쓰지 않는다. 실제로는 계정당 한 번.
    환경별
    환경카드빛
    기본위 타임라인위 타임라인
    동작 줄이기fade 120ms, 이동 없음정지 위치, fade 120 → 유지 → fade 84
    투명도 줄이기그대로없음
    강제 색상그대로없음

    동작 줄이기

    prefers-reduced-motion은 컴포넌트마다 분기하지 않고 토큰 단계에서 처리한다: fast·base·move·spin·stagger → 0, move-scale → 0, fade만 120ms. e2e는 reduce일 때 계산된 transform이 none이고 모든 duration이 120ms 이하인지 확인한다.

    동작 줄이기 요약
    항목동작 줄이기투명도 줄이기
    상태 피드백(색)즉시같음
    Switch·Segmented 손잡이즉시 위치유리 손잡이 → 불투명
    행 도착fade 120, 스태거 0같음
    Popover·Menu·Tooltipfade 120 / 84같음
    시트·대화상자fade 120 / 84, 이동 없음같음
    push 탐색즉시 교체 + fade 120같음
    사이드바 접기즉시유리 → 불투명
    Loader정지 아이콘 + 텍스트같음
    차트정지 상태로 바로같음
    랜딩 위젯 놓임·빛없음(정지)빛 없음

    성능과 금지

    • transform과 opacity만 애니메이션한다. height·width·top·margin·filter·backdrop-filter·box-shadow·clip-path·stroke-dashoffset은 하지 않는다.
    • 되돌릴 수 있는 상태는 transition, 일회성 등장만 @keyframes. 중간에 취소해도 점프가 없다.
    • will-change는 전환 직전에 붙이고 끝나면 뗀다. JS 애니메이션 라이브러리 없음(FLIP 계산만 JS).
    • 숨은 탭에서 도착한 등장 모션은 건너뛴다. 아하 카드만 보일 때까지 미룬다.
    • 이렇게 하지 않기스프링·오버슈트·바운스, scale 등장, 패럴랙스, 스크롤 연동, 끌어서 닫기, 숫자 세어 올리기, 쉬머, 테마 전환 색 보간, 탭 전환 슬라이드, 초점 링 애니메이션, 차트 그리기·성장, 로고 애니메이션, 두 번째 브랜드 모먼트.

    근거 문서: motion/motion-spec.md · contracts/README.md §5 · DIRECTION.md §1.5 · src/design/tokens/motion.css