Navigation

7. 접근성과 움직임

키보드 포커스를 눈으로 확인할 수 있고, 움직임에 민감한 사용자도 이용할 수 있어야 합니다. 포커스 표시는 없애지 않고 형태로 구분하며, 애니메이션은 전역 이름을 겹치지 않게 두고 사용자 설정을 따릅니다.

7.1 포커스 표시는 항상 눈에 보이게 남깁니다

포커스 위치를 형태로 구분해 키보드 사용자가 현재 위치를 알 수 있습니다.

포커스 표시를 유지하고 outline: none을 쓸 때는 대체 스타일을 함께 제공합니다. 표시는 수정자 안에만 두지 않고 **기본 블록의 :focus-visible**로 선언합니다.

확인 항목기준
표시 시점:focus보다 :focus-visible을 씁니다
표시 형태색만 바꾸지 않고 outline, box-shadow 링, border 두께처럼 형태도 바꿉니다. 색각 이상에서도 구분할 수 있어야 합니다
컨벤션 기본값인접 배경과 대비가 3:1 이상인 2 CSS px 이상의 외곽선을 씁니다
앱 상태--focused 같은 수정자로 대체하지 않습니다. 앱의 입력 방식 추적이 브라우저 판단이나 사용자 설정과 어긋날 수 있습니다
강제 색상 모드forced-colors: active에서는 box-shadow가 없어집니다. 투명한 outline을 함께 두거나 조건 안에서 시스템 색 외곽선을 제공합니다
사용자 색상 설정forced-color-adjust: none으로 끄지 않습니다

:focus-visible을 적용할지는 브라우저가 입력 방식과 사용자 설정으로 판단하므로 고정 규칙으로 가정하지 않습니다. WCAG 2.2 SC 1.4.11(AA)은 인접 색 대비를, SC 2.4.13(AAA)은 2 CSS px 둘레의 최소 면적과 3:1 대비를 요구합니다.

포커스 링을 제거하고 대체를 두지 않습니다.

.ui_button__root {
	border: 1px solid #d9d9d9;

	&:focus {
		outline: none;
	}
}

:focus-visible에 형태가 바뀌는 표시를 기본 블록에 둡니다.

.ui_button__root {
	border: 1px solid #d9d9d9;

	&:focus-visible {
		outline: 2px solid #1677ff;
		outline-offset: 2px;
	}
}

색만 바꾸고 수정자 안에만 둡니다.

.ui_input__field--invalid {
	&:focus-visible {
		outline: none;
		color: #1677ff;
	}
}

그림자 링에 투명한 외곽선을 함께 두어 강제 색상 모드에서도 표시를 남깁니다.

.ui_input__field {
	border: 1px solid #d9d9d9;

	&:focus-visible {
		outline: 2px solid transparent;
		outline-offset: 2px;
		border-color: #1677ff;
		box-shadow: 0 0 0 3px #1677ff;
	}
}

함께 보기

7.2 @keyframes 이름에 소유자를 붙이고 prefers-reduced-motion을 따릅니다

전역 애니메이션 이름의 충돌을 막고 움직임에 민감한 사용자의 설정을 따릅니다.

@keyframes에는 소유자 이름을 붙이고 움직임 감소 설정은 전역 스타일시트 한 곳에서 처리합니다.

이름과 토큰

일반 CSS의 @keyframes는 파일로 격리되지 않으며, 같은 이름이 있으면 문서 순서상 뒤의 정의가 적용됩니다.

이름과 토큰작성 기준
@keyframes 이름<범위>_<식별자>__<동작>으로 씁니다. 예: pg_products__fadeIn
지속 시간과 이징한 파일에서 한 번만 써도 토큰으로 둡니다. 예: var(--app-motion-duration-fast)

지속 시간과 이징 토큰은 규칙 5.2의 예외입니다.

도구 설정과 속성

도구 설정과 속성 선택기준
이름 검사stylelint-config-standard의 kebab-case 기본값을 keyframes-name-pattern으로 바꿉니다
애니메이션 속성transform과 opacity를 씁니다. width나 top은 매 프레임 레이아웃을 다시 계산합니다

움직임 감소 처리

움직임 감소 요청이 오면 기본으로 전역에서 animation과 transition을 함께 차단합니다.

전역 처리기준과 예외
차단 범위위치 이동만 골라 끄지 않습니다. 색이나 투명도 전환이 필요하면 전역 블록에서 해당 클래스를 예외로 적습니다
컴포넌트 파일움직임을 되살리지 않습니다. !important는 전역 스타일시트에서만 허용합니다
지속 시간과 지연지속 시간은 0.01ms, 지연은 0s로 줄여 이전 상태에서 기다리지 않게 합니다
완료 처리전환 지속 시간과 지연이 모두 0s이면 transitionend가 발생하지 않습니다

0.01ms도 transitionend 발생을 보장하지 않으므로 취소되거나 제거된 요소의 완료를 이벤트에만 맡기지 않습니다.

전역 이름을 겹치게 쓰고 시간을 직접 적습니다.

@keyframes fadeIn {
	from {
		opacity: 0;
	}
}

.pg_products__panel {
	animation: fadeIn 200ms ease-out;
}

소유자를 붙인 이름과 토큰을 씁니다.

@keyframes pg_products__fadeIn {
	from {
		opacity: 0;
	}
}

.pg_products__panel {
	animation: pg_products__fadeIn var(--app-motion-duration-fast) var(--app-motion-easing-out);
}

컴포넌트 파일마다 따로 끄고 지속 시간을 0으로 둡니다.

src/page/products/pg-products.csscss
@media (prefers-reduced-motion: reduce) {
	.pg_products__panel {
		animation-duration: 0s;
	}
}

전역 스타일시트에서 한 번 처리합니다.

src/style/motion.csscss
@media (prefers-reduced-motion: reduce) {
	*,
	*::before,
	*::after {
		animation-duration: 0.01ms !important;
		animation-delay: 0s !important;
		animation-iteration-count: 1 !important;
		transition-duration: 0.01ms !important;
		transition-delay: 0s !important;
		scroll-behavior: auto !important;
	}
}

함께 보기