애니메이션 선택 도구

맞춤설정 가능한 select API는 <select> 요소와 드롭다운 선택 도구에 애니메이션을 적용하는 선언적 CSS 기반 방식을 제공합니다. appearance: base-select를 @starting-style 및 allow-discrete 전환 동작과 같은 최신 CSS 애니메이션 기법과 결합하면 무거운 JavaScript 라이브러리에 의존하지 않고도 최상위 레이어 요소에 유연하고 프리미엄 UI 전환을 만들 수 있습니다.

이전에는 네이티브 선택 드롭다운의 UI가 액세스 가능한 표시 영역 제약 조건 외부에서 렌더링되었기 때문에 애니메이션을 적용할 수 없었습니다. appearance: base-select를 사용하면 다른 페이지 요소와 마찬가지로 선택 도구의 스타일을 지정하고 애니메이션을 적용할 수 있습니다.

구현 방법

애니메이션 선택 선택 도구를 구현하려면 다음 단계를 따르세요.

  1. 맞춤설정 선택: <select> 요소와 ::picker(select) 가상 요소 모두에 appearance: base-select를 적용합니다.
  2. 자동 크기 조절 전환 사용 설정 (선택사항): 브라우저가 height: auto 및 height: 0와 같은 개별 측정항목 값 간에 전환할 수 있도록 interpolate-size: allow-keywords (일반적으로 :root)를 정의합니다.
  3. 최상위 레이어 컨테이너 애니메이션: ::picker(select)에 표준 시작/종료 스타일을 적용합니다. display: none와 display: block 간에 이동할 때 불투명도 전환이 작동하도록 하려면 transition-behavior: allow-discrete (인라인으로 transition: display 0.3s allow-discrete로 작성되는 경우가 많음)를 사용해야 합니다.
  4. @starting-style를 사용하여 열기 상태에 연결: @starting-style를 사용하여 전환이 시작되기 전에 브라우저가 계산해야 하는 기준 스타일을 정의합니다. 예를 들어 페이드 인하려면 @starting-style 블록 내에서 불투명도를 0로 설정합니다.
  5. 아이콘 회전: :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.
  }
});