← 글 목록

insights

색·모션·폼·스티키가 한 화면에서 충돌할 때

테마 칩이 늦게 튀고, 토스트 pulse가 바뀌고, 폼은 placeholder에 속고, sticky는 키보드에 짓눌렸다 — 네 갈래를 다시 맞추는 밤 메모.

1. contrast-color() — 대비는 paint 전에 결정된다

출처: Building Self-Correcting Color Systems with contrast-color()

처음에 든 생각은 단순했다. "배경이 어두우면 글자를 밝게, 밝으면 어둡게. contrast-color()면 끝나지 않나?" Astro 태그 칩이랑 TQ 쿠폰 상태 칩에 바로 붙이면 테마 토글도 자동으로 맞춰질 줄 알았다.

헷갈린 지점은 테마 전환 애니메이션이었다. 배경은 부드럽게 넘어가는데, 칩 위 글자만 거의 끝에서 흑↔백으로 탁 바뀌었다. "보간이 안 되나?"가 아니라, 애초에 보간할 중간값이 없다. Level 5의 contrast-color()는 지금 black/white만 돌려준다. 그리고 그 스냅 지점은 HSL 50%가 아니라 상대휘도(relative luminance) 약 18% 근처다. 사람 눈이 밝기를 느끼는 방식과 CSS의 계산이 거기서 만난다. 중간 회색을 기대하면 안 된다.

정적 칩·태그에서는 contrast-color()가 잘 맞는다. 반면 테마 transition 중에는 텍스트 색을 따로 보간해야 한다. color-mix(in oklab, …)로 시작/끝 텍스트 색을 섞거나, 라이트·다크 라벨 레이어를 opacity로 크로스페이드하면 스냅이 줄어든다. 대비 "자기 교정"과 "부드러운 전환"은 서로 다른 문제다.

contrast-color snap vs color-mix/crossfade 타임라인

2. Keyframes 토큰 — 전역 이름이 조용히 서로를 죽인다

출처: Keyframes Tokens: Standardizing Animation Across Projects

쿠폰 등록 성공 토스트의 pulse가 어느 날부터 "덜 귀엽고 더 공격적"으로 바뀌었다. 토스트 CSS는 안 건드렸다. 범인은 Astro 쪽 view transition 실험에서 가져온 @keyframes pulse였다. 같은 이름, 마지막에 로드된 정의가 전역으로 이긴다. @keyframes는 파일 스코프가 아니다.

왜 이렇게 헷갈렸냐면, 컴포넌트 CSS는 모듈처럼 느껴지는데 키프레임 이름만 전역 네임스페이스라는 비대칭 때문이다. 토스트·Astro transition·어딘가의 lib이 서로 fade / pulse / slide-in을 쓰면, 증상은 "애니메이션이 이상해짐"으로만 나타난다. 에러는 안 난다.

이 비대칭을 다루는 핵심은 키프레임 이름을 전역 네임스페이스로 보는 것이다. kf- 접두사(kf-pulse-soft, kf-toast-in)를 이름에 포함하고, 타이밍·이징을 --kf-* 변수로 분리하면 이름 충돌을 추적하기 쉬워진다. prefers-reduced-motion에서는 pulse를 끄고 entrance를 최종값으로 점프시키는 정책도 토큰의 일부가 된다. Astro transitions와 쿠폰 toast가 서로 다른 이름을 쓰면서도 같은 리듬을 공유할 수 있는 구조다.

keyframes 이름 충돌 → kf- 토큰 네임스페이스

3. 폼 인지부하 4원칙 — placeholder가 "친절"로 위장할 때

출처: 4 Principles to Reduce Cognitive Load in Forms (Structure · Transparency · Clarity · Support)

TQ 쿠폰 등록·결제 폼을 다시 보면서 "필드가 많은데 왜 더 바빠 보이지?"를 물었다. 답은 필드 개수보다 힌트가 잘못된 자리에 있었다. placeholder만 달아 두면 세 가지가 동시에 깨진다. 입력하는 순간 힌트가 사라지고, 대비가 낮아 읽기 어렵고, 이미 채워진 것처럼 보인다. 사용자는 빈 칸을 건너뛰거나, 조기 validation 에러에 맞닥뜨린다.

NN/g 네 원칙을 우리 말로 다시 붙이면 이렇다. Structure — 단일 열, 관련 필드 그룹. Transparency — 선택 항목은 "(선택)"으로 밝히기. Clarity — 라벨과 도움말은 필드 밖. Support — 에러는 해당 필드 옆에, 입력 중이 아니라 제출 시점(또는 필드 blur 이후)에. placeholder를 라벨 대용으로 쓰지 않는 것이 핵심이다.

쿠폰 코드 필드에서 "대소문자 구분 없음 · 하이픈 포함 가능"을 라벨 아래 작은 도움말로 분리하면, 입력 중에도 규칙이 남고 "이미 채워진 칸"이라는 착각이 줄어든다. 결제 쪽 만료일·CVC에도 같은 원칙이 적용된다.

placeholder 함정 vs 필드 밖 label+help

4. 모바일 Sticky — 키보드가 화면의 60%를 가져갈 때

출처: Sticky Menus & UX Guidelines

결제 화면 QA에서 "카드번호가 안 보여요" 피드백이 왔다. 필드가 사라진 게 아니라, 가상 키보드 + 하단 sticky CTA가 남은 뷰포트를 거의 삼킨 것이었다. 폼과 키보드만으로도 대략 화면의 60%다. 그 위에 sticky bar를 고정하면 입력칸이 "거의 없는" 상태가 된다. sticky가 편의를 주려다 과제를 불가능하게 만드는 순간이다.

가이드에서 확인되는 기준은 다음과 같다. sticky 항목은 ≤5개이고, 헤더는 전부 고정하기보다 부분적으로 persistent하게 둔다. slide-in은 300–400ms 정도다. 앵커·포커스 스크롤에서는 scroll-padding으로 sticky 높이만큼 여백을 확보해야 한다. 입력 포커스(키보드 열림) 동안 sticky CTA가 계속 화면을 차지하면, 폼과 키보드가 남은 뷰포트를 잠식한다. TQ 결제·쿠폰 화면도 이 조건에서 보면 같은 충돌을 확인할 수 있다.

키보드+sticky 충돌 vs 아코디언·scroll-padding

네 요소가 동시에 작동할 때

네 꼭지는 한 제품 플로우(쿠폰 → 결제 → 토스트 → 테마 칩) 안에서 서로 겹친다. 색 대비의 스냅은 테마 전환의 리듬과 부딪히고, 전역 키프레임 충돌은 토스트와 화면 전환의 리듬을 바꾼다. 폼의 placeholder는 정보가 사라지는 순간을 만들며, 모바일에서는 키보드와 sticky CTA가 입력 가능한 면적을 함께 줄인다.

각 문제는 별개의 규칙처럼 보이지만 사용자가 만나는 것은 한 화면이다. 색이 튀고, 모션이 덮이고, 폼이 헷갈리고, sticky가 시야를 누르는 현상은 개별 결함보다 동시에 겹칠 때 신뢰를 더 크게 깎는다. 이번 조사의 공통점은 장식의 문제가 아니라 상태 변화, 정보 위치, 가용 뷰포트를 함께 읽어야 한다는 데 있다.