맞춤설정 가능한 select API는 <select> 요소와 드롭다운 선택 도구에 애니메이션을 적용하는 선언적 CSS 기반 방식을 제공합니다. appearance: base-select를 @starting-style 및 allow-discrete 전환 동작과 같은 최신 CSS 애니메이션 기법과 결합하면 무거운 JavaScript 라이브러리에 의존하지 않고도 최상위 레이어 요소에 유연하고 프리미엄 UI 전환을 만들 수 있습니다.
이전에는 네이티브 선택 드롭다운의 UI가 액세스 가능한 표시 영역 제약 조건 외부에서 렌더링되었기 때문에 애니메이션을 적용할 수 없었습니다. appearance: base-select를 사용하면 다른 페이지 요소와 마찬가지로 선택 도구의 스타일을 지정하고 애니메이션을 적용할 수 있습니다.
구현 방법
애니메이션 선택 선택 도구를 구현하려면 다음 단계를 따르세요.
- 맞춤설정 선택:
<select>요소와::picker(select)가상 요소 모두에appearance: base-select를 적용합니다. - 자동 크기 조절 전환 사용 설정 (선택사항): 브라우저가
height: auto및height: 0와 같은 개별 측정항목 값 간에 전환할 수 있도록interpolate-size: allow-keywords(일반적으로:root)를 정의합니다. - 최상위 레이어 컨테이너 애니메이션:
::picker(select)에 표준 시작/종료 스타일을 적용합니다.display: none와display: block간에 이동할 때 불투명도 전환이 작동하도록 하려면transition-behavior: allow-discrete(인라인으로transition: display 0.3s allow-discrete로 작성되는 경우가 많음)를 사용해야 합니다. @starting-style를 사용하여 열기 상태에 연결:@starting-style를 사용하여 전환이 시작되기 전에 브라우저가 계산해야 하는 기준 스타일을 정의합니다. 예를 들어 페이드 인하려면@starting-style블록 내에서 불투명도를0로 설정합니다.- 아이콘 회전:
:open::picker-icon와 같은 가상 요소 포커스 또는 활성 선택기를 사용하여 화살표 표시기에 전환 (예: 회전 또는 변환)을 적용합니다.
예시 코드: 부드러운 선택 크기 조절 및 페이드
다음 예는 선택 도구 컨테이너의 표준 페이지 애니메이션으로 스타일이 지정된 맞춤 선택을 보여줍니다.
<!-- Always use a <label> linked via 'for' to the select for accessibility -->
<label for="theme-select">Visual Theme</label>
<select id="theme-select" class="animated-select" name="theme">
<!-- The <button> inside <select> becomes the visible trigger when appearance: base-select is used -->
<button>
<!-- <selectedcontent> automatically displays the content of the chosen <option> -->
<selectedcontent></selectedcontent>
</button>
<option value="system">
<!-- MANDATORY: Decorative inline SVGs MUST set aria-hidden="true" to prevent redundant screen reader announcement -->
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<rect x="2" y="3" width="20" height="14" rx="2" ry="2"></rect>
<line x1="8" y1="21" x2="16" y2="21"></line>
<line x1="12" y1="17" x2="12" y2="21"></line>
</svg>
System Default
</option>
<option value="light">
<svg aria-hidden="true" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
<circle cx="12" cy="12" r="5"></circle>
<line x1="12" y1="1" x2="12" y2="3"></line>
<line x1="12" y1="21" x2="12" y2="23"></line>
<line x1="4.22" y1="4.22" x2="5.64" y2="5.64"></line>
<line x1="18.36" y1="18.36" x2="19.78" y2="19.78"></line>
<line x1="1" y1="12" x2="3" y2="12"></line>
<line x1="21" y1="12" x2="23" y2="12"></line>
<line x1="4.22" y1="19.78" x2="5.64" y2="18.36"></line>
<line x1="18.36" y1="5.64" x2="19.78" y2="4.22"></line>
</svg>
Light UI
</option>
</select>
/* Opt-in to customizable select */
.animated-select,
.animated-select::picker(select) {
appearance: base-select;
}
/* Enable auto-keyword transitions (usually set globally at :root) */
:root {
interpolate-size: allow-keywords;
}
/* Style the visible trigger and icon rotation */
.animated-select {
display: flex;
align-items: center;
justify-content: space-between;
width: 100%;
padding: 0.875rem 1rem;
font-size: 1rem;
border-radius: 8px;
cursor: pointer;
transition: border-color 0.2s ease;
}
.animated-select::picker-icon {
transition: transform 0.3s cubic-bezier(0.16, 1, 0.3, 1);
}
.animated-select:open::picker-icon {
transform: rotate(180deg);
}
/*
* The Picker Container
* Uses top-layer animations with `allow-discrete` visibility hooks
*/
.animated-select::picker(select) {
background: white;
border-radius: 12px;
box-shadow: 0 10px 25px -3px rgba(0,0,0,0.1);
padding: 0.5rem;
margin-top: 0.25rem;
width: anchor-size(width);
overflow: hidden;
/* The crucial transition setting for popover animations */
transition:
display 0.4s allow-discrete,
overlay 0.4s allow-discrete,
opacity 0.4s cubic-bezier(0.16, 1, 0.3, 1),
height 0.4s cubic-bezier(0.16, 1, 0.3, 1);
opacity: 0;
height: 0;
}
/* Open State */
.animated-select:open::picker(select) {
opacity: 1;
height: auto;
}
/* @starting-style to hook the transition on initial popover open */
@starting-style {
.animated-select:open::picker(select) {
opacity: 0;
height: 0;
}
}
/* Support for SVG inside Options and Selected Content */
.animated-select option svg,
.animated-select selectedcontent svg {
flex-shrink: 0; /* Prevent icons from shrinking */
width: 1.25rem;
height: 1.25rem;
}
/* MANDATORY: Provide multiple indicators (e.g. bold font and distinct background) for the checked state to avoid color-only state communication */
.animated-select option:checked {
font-weight: 700;
background-color: #f1f5f9;
}
/* Ensure copy-paste safety for users with motion sensitivities */
@media (prefers-reduced-motion: reduce) {
.animated-select::picker(select),
.animated-select::picker-icon {
transition: none !important;
}
}
전략적 구현 및 권장사항
- 요소가
display: none에서 표시 상태로 전환될 때 정확하게 애니메이션을 트리거해야 하는 경우@starting-style를 사용합니다. - 임시 스크롤 잠금을 사용하지 마세요. 'base-select'로 관리되는 최상위 레이어 요소는 자연스러운 배경 닫기 동작을 허용해야 합니다.
- 움직임 감소 환경설정을 확인해야 합니다. 멀미의 영향을 받는 사용자가 액세스할 수 있는 환경을 보장하려면 항상 애니메이션 제약 조건을
prefers-reduced-motion미디어 쿼리로 래핑하세요. - 레이아웃 동작을 테스트합니다.
appearance: base-select를 설정하면 가장 긴 옵션 너비를 기준으로 선택 항목의 크기를 조정하는 기본 브라우저 동작이 삭제됩니다. 레이아웃 변경을 방지하려면 고정 너비를 설정하거나 flex/grid 제약 조건을 사용해야 할 수 있습니다. <select>에name속성과 연결된<label>가 있는지 확인합니다. 이렇게 하면 맞춤 UI를 사용하더라도 스크린 리더가 구성요소에 액세스할 수 있고 표준 양식 제출이 올바르게 작동합니다.- 옵션에 표준
value속성을 사용하여 양식 제출이 이전과 동일하게 작동하도록 합니다.
대체 전략
맞춤설정 가능한 <select>의 대체 및 브라우저 지원
아직 appearance: base-select를 지원하지 않는 브라우저의 경우 <select> 요소가 표준 운영체제 드롭다운으로 원활하게 다운그레이드됩니다.
- 텍스트가 아닌 콘텐츠 무시: 이전 브라우저는
<option>태그 내의 HTML 태그 (예:<svg>또는<div>)를 제거하고 텍스트 노드만 렌더링합니다.<option>의 텍스트 콘텐츠가 그 자체로 읽기 쉽고 의미가 있는지 확인합니다. - HTML 구조 처리: 표준 파서는
<select>내의<button>및<selectedcontent>태그를 무시하거나 잘못된 것으로 처리할 수 있습니다. 표준 텍스트를 읽을 수 있는 대체로 간주하는 경우 점진적 개선을 위해 무거운 JavaScript 폴리필이 반드시 필요한 것은 아닙니다.
document.addEventListener("DOMContentLoaded", () => {
// Check if browser supports base-select value
if (!CSS.supports("appearance", "base-select")) {
// Custom select overrides are not supported natively.
}
});