적응형이며 접근성이 뛰어난 테마 전환 구성요소를 빌드하는 방법에 관한 기본 개요입니다.
이 게시물에서는 어두운 테마와 밝은 테마 전환 구성요소를 빌드하는 방법에 대한 생각을 공유하고자 합니다. 데모 사용해 보기
동영상을 선호하는 경우 이 게시물의 YouTube 버전을 참고하세요.
개요
웹사이트는 시스템 환경설정에 전적으로 의존하는 대신 색 구성표를 제어하는 설정을 제공할 수 있습니다. 즉, 사용자는 시스템 환경설정과 다른 모드로 탐색할 수 있습니다. 예를 들어 사용자의 시스템이 밝은 테마로 설정되어 있지만 사용자가 웹사이트가 어두운 테마로 표시되기를 원하는 경우
이 기능을 빌드할 때는 몇 가지 웹 엔지니어링 고려사항이 있습니다. 예를 들어 페이지 색상 플래시를 방지하려면 브라우저가 가능한 한 빨리 환경설정을 인식해야 하며 컨트롤은 먼저 시스템과 동기화한 다음 클라이언트 측 저장된 예외를 허용해야 합니다.
마크업
전환에는 <button>
를 사용해야 합니다. 그러면 클릭 이벤트 및 포커스 가능성과 같은 브라우저 제공 상호작용 이벤트와 기능을 활용할 수 있기 때문입니다.
버튼
버튼에는 CSS에서 사용할 클래스와 JavaScript에서 사용할 ID가 필요합니다.
또한 버튼 콘텐츠는 텍스트가 아닌 아이콘이므로 title 속성을 추가하여 버튼의 목적에 관한 정보를 제공합니다. 마지막으로 [aria-label]
를 추가하여 아이콘 버튼의 상태를 유지합니다. 그러면 스크린 리더가 시각 장애인에게 테마의 상태를 공유할 수 있습니다.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
>
…
</button>
aria-label
및 aria-live
정중함
스크린 리더에 aria-label
변경사항을 알려야 한다고 표시하려면 버튼에 aria-live="polite"
를 추가합니다.
<button
class="theme-toggle"
id="theme-toggle"
title="Toggles light & dark"
aria-label="auto"
aria-live="polite"
>
…
</button>
이 마크업을 추가하면 스크린 리더에 aria-live="assertive"
대신 변경된 사항을 사용자에게 정중하게 알리라는 신호를 보냅니다. 이 버튼의 경우 aria-label
의 상태에 따라 '밝음' 또는 '어두움'을 알립니다.
확장 가능한 벡터 그래픽 (SVG) 아이콘
SVG는 최소한의 마크업을 사용하여 확장 가능한 고품질 도형을 만드는 방법을 제공합니다. 버튼과 상호작용하면 벡터의 새로운 시각적 상태가 트리거될 수 있으므로 SVG는 아이콘에 적합합니다.
다음 SVG 마크업은 <button>
내에 배치됩니다.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
…
</svg>
aria-hidden
가 SVG 요소에 추가되었습니다. 따라서 스크린 리더는 이 요소가 프레젠테이션용으로 표시되어 있으므로 무시해야 함을 알 수 있습니다. 버튼 내부의 아이콘과 같은 시각적 장식에 적합합니다. 요소의 필수 viewBox
속성 외에도 이미지에 인라인 크기가 적용되어야 하는 것과 유사한 이유로 높이와 너비를 추가합니다.
태양
태양 그래픽은 SVG에 편리하게 도형이 있는 원과 선으로 구성됩니다. <circle>
은 cx
및 cy
속성을 표시 영역 크기 (24)의 절반인 12로 설정하고 크기를 설정하는 6
의 반경 (r
)을 12로 지정하여 가운데에 배치됩니다.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
</svg>
또한 마스크 속성은 다음에 만들 SVG 요소의 ID를 가리키고 마지막으로 currentColor
를 사용하여 페이지의 텍스트 색상과 일치하는 채우기 색상을 지정합니다.
태양 광선
다음으로, 햇빛 선이 원의 바로 아래 그룹 요소 <g>
그룹 내에 추가됩니다.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
<line x1="12" y1="1" x2="12" y2="3" />
<line x1="12" y1="21" x2="12" y2="23" />
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64" />
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78" />
<line x1="1" y1="12" x2="3" y2="12" />
<line x1="21" y1="12" x2="23" y2="12" />
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36" />
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22" />
</g>
</svg>
이번에는 fill 값이 currentColor
이 아닌 각 선의 stroke가 설정됩니다. 선과 원 도형을 사용해 햇살이 비치는 멋진 태양을 만들어 보세요.
달
밝음 (태양)과 어둠(달) 간에 원활한 전환의 환상을 만들기 위해 달은 SVG 마스크를 사용하여 태양 아이콘을 보강한 것입니다.
<svg class="sun-and-moon" aria-hidden="true" width="24" height="24" viewBox="0 0 24 24">
<circle class="sun" cx="12" cy="12" r="6" mask="url(#moon-mask)" fill="currentColor" />
<g class="sun-beams" stroke="currentColor">
…
</g>
<mask class="moon" id="moon-mask">
<rect x="0" y="0" width="100%" height="100%" fill="white" />
<circle cx="24" cy="10" r="6" fill="black" />
</mask>
</svg>
SVG를 사용한 마스크는 매우 강력하여 흰색과 검은색으로 다른 그래픽의 일부를 삭제하거나 포함할 수 있습니다. 원 모양을 마스크 영역 안팎으로 이동하기만 하면 태양 아이콘이 SVG 마스크가 적용된 달 <circle>
모양에 가려집니다.
CSS가 로드되지 않으면 어떻게 되나요?
CSS가 로드되지 않은 것처럼 SVG를 테스트하여 결과가 너무 크거나 레이아웃 문제를 일으키지 않는지 확인하는 것이 좋습니다. SVG의 인라인 높이 및 너비 속성과 currentColor
사용은 CSS가 로드되지 않을 때 브라우저에서 사용할 수 있는 최소한의 스타일 규칙을 제공합니다. 이렇게 하면 네트워크 난류에 대한 훌륭한 방어 스타일을 만들 수 있습니다.
레이아웃
테마 전환 구성요소는 노출 영역이 작으므로 레이아웃에 그리드나 플렉스박스가 필요하지 않습니다. 대신 SVG 위치 지정 및 CSS 변환이 사용됩니다.
스타일
스타일 .theme-toggle
개
<button>
요소는 아이콘 도형 및 스타일의 컨테이너입니다. 이 상위 컨텍스트는 SVG에 전달할 적응형 색상과 크기를 보유합니다.
첫 번째 작업은 버튼을 원형으로 만들고 기본 버튼 스타일을 삭제하는 것입니다.
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
}
다음으로 상호작용 스타일을 추가합니다. 마우스 사용자를 위한 커서 스타일을 추가합니다. 빠른 반응 터치 환경을 위해 touch-action: manipulation
를 추가합니다.
iOS에서 버튼에 적용하는 반투명 강조 표시를 삭제합니다. 마지막으로 포커스 상태 윤곽선이 요소 가장자리에서 약간 여유를 갖도록 합니다.
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
}
버튼 내부의 SVG에도 일부 스타일이 필요합니다. SVG는 버튼 크기에 맞게 조정해야 하며, 시각적으로 부드러워 보이도록 선 끝을 둥글게 처리해야 합니다.
.theme-toggle {
--size: 2rem;
background: none;
border: none;
padding: 0;
inline-size: var(--size);
block-size: var(--size);
aspect-ratio: 1;
border-radius: 50%;
cursor: pointer;
touch-action: manipulation;
-webkit-tap-highlight-color: transparent;
outline-offset: 5px;
& > svg {
inline-size: 100%;
block-size: 100%;
stroke-linecap: round;
}
}
hover
미디어 쿼리를 사용한 반응형 크기 조정
아이콘 버튼 크기는 2rem
로 약간 작습니다. 마우스 사용자에게는 문제가 없지만 손가락과 같은 대략적인 포인터에는 어려움이 있을 수 있습니다. 마우스 오버 미디어 쿼리를 사용하여 크기 증가를 지정하여 버튼이 여러 터치 크기 가이드라인을 충족하도록 합니다.
.theme-toggle {
--size: 2rem;
…
@media (hover: none) {
--size: 48px;
}
}
태양과 달 SVG 스타일
버튼은 테마 전환 구성요소의 상호작용 측면을 보유하고 내부의 SVG는 시각적 및 애니메이션 측면을 보유합니다. 여기에서 아이콘을 아름답게 만들고 생동감을 줄 수 있습니다.
밝은 테마
SVG 도형의 중심에서 크기 조절 및 회전 애니메이션이 실행되도록 하려면 transform-origin: center center
를 설정하세요. 버튼에서 제공하는 적응형 색상은 여기에서 도형에 사용됩니다. 달과 태양은 채우기에 제공된 버튼 var(--icon-fill)
및 var(--icon-fill-hover)
를 사용하고, 햇빛은 획에 변수를 사용합니다.
.sun-and-moon {
& > :is(.moon, .sun, .sun-beams) {
transform-origin: center center;
}
& > :is(.moon, .sun) {
fill: var(--icon-fill);
@nest .theme-toggle:is(:hover, :focus-visible) > & {
fill: var(--icon-fill-hover);
}
}
& > .sun-beams {
stroke: var(--icon-fill);
stroke-width: 2px;
@nest .theme-toggle:is(:hover, :focus-visible) & {
stroke: var(--icon-fill-hover);
}
}
}
어두운 테마
달 스타일은 햇빛을 삭제하고 태양 원을 확대하며 원 마스크를 이동해야 합니다.
.sun-and-moon {
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
}
& > .sun-beams {
opacity: 0;
}
& > .moon > circle {
transform: translateX(-7px);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
}
}
}
}
어두운 테마에는 색상 변경이나 전환이 없습니다. 상위 버튼 구성요소는 색상을 소유하며, 여기서 색상은 어두운 컨텍스트와 밝은 컨텍스트 내에서 이미 적응됩니다. 전환 정보는 사용자의 모션 환경설정 미디어 쿼리 뒤에 있어야 합니다.
애니메이션
이 시점에서 버튼은 작동하고 상태가 있지만 전환은 없어야 합니다. 다음 섹션에서는 방법과 전환 항목을 정의하는 방법을 설명합니다.
미디어 쿼리 공유 및 이중화 가져오기
사용자의 운영체제 모션 환경설정에 따라 전환과 애니메이션을 쉽게 적용할 수 있도록 PostCSS 플러그인 맞춤 미디어에서는 미디어 쿼리 변수의 초안 CSS 사양 구문을 사용할 수 있습니다.
@custom-media --motionOK (prefers-reduced-motion: no-preference);
/* usage example */
@media (--motionOK) {
.sun {
transition: transform .5s var(--ease-elastic-3);
}
}
고유하고 사용하기 쉬운 CSS 이징을 사용하려면 Open Props의 easings 부분을 가져옵니다.
@import "https://unpkg.com/open-props/easings.min.css";
/* usage example */
.sun {
transition: transform .5s var(--ease-elastic-3);
}
태양
태양의 전환은 달보다 더 장난스러우며, 탄력 있는 이중으로 이 효과를 얻습니다. 햇빛이 회전할 때 약간 튀어야 하고 태양의 중심이 크기가 조절될 때 약간 튀어야 합니다.
기본 (밝은 테마) 스타일은 전환을 정의하고 어두운 테마 스타일은 밝은 모드로의 전환에 대한 맞춤설정을 정의합니다.
.sun-and-moon {
@media (--motionOK) {
& > .sun {
transition: transform .5s var(--ease-elastic-3);
}
& > .sun-beams {
transition:
transform .5s var(--ease-elastic-4),
opacity .5s var(--ease-3)
;
}
@nest [data-theme="dark"] & {
& > .sun {
transform: scale(1.75);
transition-timing-function: var(--ease-3);
transition-duration: .25s;
}
& > .sun-beams {
transform: rotateZ(-25deg);
transition-duration: .15s;
}
}
}
}
Chrome DevTools의 애니메이션 패널에서 애니메이션 전환 타임라인을 확인할 수 있습니다. 전체 애니메이션, 요소, 이중 값 적용 타이밍의 길이를 검사할 수 있습니다.
달
달빛과 어두운 위치는 이미 설정되어 있습니다. --motionOK
미디어 쿼리 내에 전환 스타일을 추가하여 사용자의 모션 환경설정을 준수하면서 생동감을 더하세요.
이 전환을 원활하게 하려면 지연 시간과 기간이 중요합니다. 예를 들어 태양이 너무 일찍 지면 전환이 조율되거나 재미있지 않고 혼란스러워 보입니다.
.sun-and-moon {
@media (--motionOK) {
& .moon > circle {
transform: translateX(-7px);
transition: transform .25s var(--ease-out-5);
@supports (cx: 1px) {
transform: translateX(0);
cx: 17px;
transition: cx .25s var(--ease-out-5);
}
}
@nest [data-theme="dark"] & {
& > .moon > circle {
transition-delay: .25s;
transition-duration: .5s;
}
}
}
}
모션 감소 선호
대부분의 GUI 챌린지에서는 움직임을 줄이는 것을 선호하는 사용자를 위해 불투명도 교차 페이드와 같은 일부 애니메이션을 유지하려고 합니다. 그러나 이 구성요소는 즉각적인 상태 변경이 더 좋았습니다.
자바스크립트
이 구성요소에는 스크린 리더용 ARIA 정보를 관리하는 것부터 로컬 저장소에서 값을 가져오고 설정하는 것까지 JavaScript가 처리해야 할 작업이 많습니다.
페이지 로드 환경
페이지 로드 시 색상이 깜박이지 않아야 했습니다. 어두운 색 구성표를 사용하는 사용자가 이 구성요소에서 밝은 색상을 선호한다고 표시한 후 페이지를 새로고침하면 처음에는 페이지가 어둡게 표시되었다가 밝은 색상으로 플래시됩니다.
이를 방지하려면 HTML 속성 data-theme
를 최대한 빨리 설정하는 것을 목표로 소량의 차단 JavaScript를 실행해야 했습니다.
<script src="./theme-toggle.js"></script>
이를 위해 문서 <head>
의 일반 <script>
태그가 CSS 또는 <body>
마크업보다 먼저 로드됩니다. 브라우저가 이와 같이 마크되지 않은 스크립트를 만나면 코드를 실행하고 나머지 HTML보다 먼저 실행합니다. 이 차단 순간을 적절하게 사용하면 기본 CSS가 페이지를 페인트하기 전에 HTML 속성을 설정하여 플래시나 색상을 방지할 수 있습니다.
JavaScript는 먼저 로컬 저장소에서 사용자의 환경설정을 확인하고 저장소에 아무것도 없으면 시스템 환경설정을 확인하도록 대체합니다.
const storageKey = 'theme-preference'
const getColorPreference = () => {
if (localStorage.getItem(storageKey))
return localStorage.getItem(storageKey)
else
return window.matchMedia('(prefers-color-scheme: dark)').matches
? 'dark'
: 'light'
}
다음으로 로컬 저장소에 사용자의 환경설정을 설정하는 함수가 파싱됩니다.
const setPreference = () => {
localStorage.setItem(storageKey, theme.value)
reflectPreference()
}
그 뒤에는 환경설정을 사용하여 문서를 수정하는 함수가 나옵니다.
const reflectPreference = () => {
document.firstElementChild
.setAttribute('data-theme', theme.value)
document
.querySelector('#theme-toggle')
?.setAttribute('aria-label', theme.value)
}
이 시점에서 중요한 것은 HTML 문서 파싱 상태입니다. <head>
태그가 완전히 파싱되지 않았으므로 브라우저는 아직 '#theme-toggle' 버튼을 알지 못합니다. 하지만 브라우저에는 <html>
태그라고도 하는 document.firstElementChild
가 있습니다. 이 함수는 동기화를 유지하기 위해 두 태그를 모두 설정하려고 시도하지만, 첫 실행에서는 HTML 태그만 설정할 수 있습니다. querySelector
는 처음에는 아무것도 찾지 못하며 선택적 체이닝 연산자는 찾을 수 없고 setAttribute 함수를 호출하려고 시도할 때 문법 오류가 발생하지 않도록 합니다.
그런 다음 reflectPreference()
함수가 즉시 호출되어 HTML 문서에 data-theme
속성이 설정됩니다.
reflectPreference()
버튼에 여전히 속성이 필요하므로 페이지 로드 이벤트를 기다린 후 다음을 쿼리하고, 리스너를 추가하고, 속성을 설정할 수 있습니다.
window.onload = () => {
// set on load so screen readers can get the latest value on the button
reflectPreference()
// now this script can find and listen for clicks on the control
document
.querySelector('#theme-toggle')
.addEventListener('click', onClick)
}
전환 환경
버튼을 클릭하면 JavaScript 메모리와 문서에서 테마를 전환해야 합니다. 현재 테마 값을 검사하고 새 상태에 관해 결정을 내려야 합니다. 새 상태가 설정되면 저장하고 문서를 업데이트합니다.
const onClick = () => {
theme.value = theme.value === 'light'
? 'dark'
: 'light'
setPreference()
}
시스템과 동기화
이 테마 전환의 고유한 점은 시스템 환경설정이 변경될 때 동기화된다는 것입니다. 페이지와 이 구성요소가 표시된 상태에서 사용자가 시스템 환경설정을 변경하면 테마 스위치가 새 사용자 환경설정에 맞게 변경됩니다. 마치 사용자가 시스템 스위치를 전환하는 동시에 테마 스위치와 상호작용한 것처럼 말입니다.
JavaScript와 미디어 쿼리 변경사항을 수신 대기하는 matchMedia
이벤트를 사용하여 이를 실행합니다.
window
.matchMedia('(prefers-color-scheme: dark)')
.addEventListener('change', ({matches:isDark}) => {
theme.value = isDark ? 'dark' : 'light'
setPreference()
})
결론
이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂
접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.