분할 버튼 구성요소 빌드

액세스 가능한 분할 버튼 구성요소를 빌드하는 방법에 관한 기본적인 개요입니다.

이 게시물에서는 분할 버튼을 만드는 방법에 대한 생각을 공유하고자 합니다 . 데모 사용해 보기

데모

동영상을 선호한다면 이 게시물의 YouTube 버전을 참조하세요.

개요

분할 버튼은 기본 버튼과 추가 버튼 목록을 숨기는 버튼입니다. 이 클래스는 필요할 때까지 자주 사용되지 않는 보조 작업을 중첩하면서 공통 작업을 노출하는 데 유용합니다. 분할 버튼은 바쁜 디자인을 최소화하는 데 중요한 역할을 할 수 있습니다. 고급 분할 버튼은 마지막 사용자 작업을 기억하여 이를 기본 위치로 승격시킬 수도 있습니다.

일반적인 분할 버튼은 이메일 애플리케이션에서 찾을 수 있습니다. 기본 작업은 전송이지만, 나중에 전송하거나 초안을 대신 저장할 수 있습니다.

이메일 애플리케이션에 표시된 분할 버튼의 예

공유된 작업 영역은 사용자가 주변을 둘러볼 필요가 없기 때문에 좋습니다. 필수적인 이메일 작업이 분할 버튼에 포함되어 있다는 것을 알고 있습니다.

부품

전체적인 조정 및 최종 사용자 경험에 대해 논의하기 전에 분할 버튼의 주요 부분을 자세히 살펴보겠습니다. 여기에서 VisBug의 접근성 검사 도구를 사용하여 구성요소의 매크로 보기를 표시하여 각 주요 부분의 HTML, 스타일, 접근성의 측면을 표시합니다.

분할 버튼을 구성하는 HTML 요소.

최상위 분할 버튼 컨테이너

가장 높은 수준의 구성요소는 기본 작업.gui-popup-button를 포함하는 gui-split-button 클래스가 있는 인라인 Flexbox입니다.

gui-split-button 클래스가 검사되어 이 클래스에 사용된 CSS 속성을 보여줍니다.

기본 작업 버튼

처음에 표시되고 포커스 가능한 <button>포커스, hover활성 상호작용이 .gui-split-button 내에 포함되도록 두 개의 일치하는 모서리 모양으로 컨테이너 내에 맞습니다.

버튼 요소의 CSS 규칙을 보여주는 검사기

팝업 전환 버튼

'팝업 버튼' 지원 요소는 보조 버튼 목록을 활성화하고 암시하기 위한 것입니다. <button>이 아니며 포커스할 수 없습니다. 하지만 .gui-popup의 배치 앵커이자 팝업을 표시하는 데 사용되는 :focus-within의 호스트입니다.

gui-popup-button 클래스의 CSS 규칙을 보여주는 검사기

팝업 카드

이는 앵커 .gui-popup-button의 플로팅 카드 하위 요소로, 절대 위치로 배치되고 버튼 목록을 의미론적으로 래핑합니다.

gui-popup 클래스의 CSS 규칙을 보여주는 검사기

보조 작업

기본 작업 버튼보다 글꼴 크기가 약간 작은 포커스 가능한 <button>에는 기본 버튼에 아이콘과 보간 스타일이 있습니다.

버튼 요소의 CSS 규칙을 보여주는 검사기

맞춤 속성

다음 변수는 색상 조화를 이루고 구성요소 전체에서 사용되는 값을 수정하는 중심 위치를 만드는 데 도움이 됩니다.

@custom-media --motionOK (prefers-reduced-motion: no-preference);
@custom-media --dark (prefers-color-scheme: dark);
@custom-media --light (prefers-color-scheme: light);

.gui-split-button {
  --theme:             hsl(220 75% 50%);
  --theme-hover:  hsl(220 75% 45%);
  --theme-active:  hsl(220 75% 40%);
  --theme-text:      hsl(220 75% 25%);
  --theme-border: hsl(220 50% 75%);
  --ontheme:         hsl(220 90% 98%);
  --popupbg:         hsl(220 0% 100%);

  --border: 1px solid var(--theme-border);
  --radius: 6px;
  --in-speed: 50ms;
  --out-speed: 300ms;

  @media (--dark) {
    --theme:             hsl(220 50% 60%);
    --theme-hover:  hsl(220 50% 65%);
    --theme-active:  hsl(220 75% 70%);
    --theme-text:      hsl(220 10% 85%);
    --theme-border: hsl(220 20% 70%);
    --ontheme:         hsl(220 90% 5%);
    --popupbg:         hsl(220 10% 30%);
  }
}

레이아웃 및 색상

마크업

이 요소는 맞춤 클래스 이름이 있는 <div>로 시작합니다.

<div class="gui-split-button"></div>

기본 버튼과 .gui-popup-button 요소를 추가합니다.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions"></span>
</div>

ARIA 속성 aria-haspopuparia-expanded에 유의하세요. 이러한 신호는 스크린 리더가 분할 버튼 환경의 기능과 상태를 인식하는 데 중요합니다. title 속성은 모든 사용자에게 유용합니다.

<svg> 아이콘과 .gui-popup 컨테이너 요소를 추가합니다.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup"></ul>
  </span>
</div>

간단한 팝업 배치의 경우 .gui-popup는 확장 버튼의 하위 요소입니다. 이 전략에서 유일하게 포착되는 문제는 .gui-split-button 컨테이너가 overflow: hidden를 사용할 수 없다는 것입니다. 팝업이 시각적으로 표시되지 않도록 잘라내기 때문입니다.

<li><button> 콘텐츠로 채워진 <ul>는 스크린 리더에 '버튼 목록'으로 스스로를 알리며, 이는 제시되는 인터페이스입니다.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li>
        <button>Schedule for later</button>
      </li>
      <li>
        <button>Delete</button>
      </li>
      <li>
        <button>Save draft</button>
      </li>
    </ul>
  </span>
</div>

멋지고 색상을 사용할 수 있도록 https://heroicons.com의 보조 버튼에 아이콘을 추가했습니다. 아이콘은 기본 버튼과 보조 버튼 모두 선택사항입니다.

<div class="gui-split-button">
  <button>Send</button>
  <span class="gui-popup-button" aria-haspopup="true" aria-expanded="false" title="Open for more actions">
    <svg aria-hidden="true" viewBox="0 0 20 20">
      <path d="M5.293 7.293a1 1 0 011.414 0L10 10.586l3.293-3.293a1 1 0 111.414 1.414l-4 4a1 1 0 01-1.414 0l-4-4a1 1 0 010-1.414z" />
    </svg>
    <ul class="gui-popup">
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M8 7V3m8 4V3m-9 8h10M5 21h14a2 2 0 002-2V7a2 2 0 00-2-2H5a2 2 0 00-2 2v12a2 2 0 002 2z" />
        </svg>
        Schedule for later
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M19 7l-.867 12.142A2 2 0 0116.138 21H7.862a2 2 0 01-1.995-1.858L5 7m5 4v6m4-6v6m1-10V4a1 1 0 00-1-1h-4a1 1 0 00-1 1v3M4 7h16" />
        </svg>
        Delete
      </button></li>
      <li><button>
        <svg aria-hidden="true" viewBox="0 0 24 24">
          <path d="M5 5a2 2 0 012-2h10a2 2 0 012 2v16l-7-3.5L5 21V5z" />
        </svg>
        Save draft
      </button></li>
    </ul>
  </span>
</div>

스타일

HTML과 콘텐츠가 준비되면 스타일은 색상과 레이아웃을 제공할 준비가 되어 있습니다.

분할 버튼 컨테이너의 스타일 지정

inline-flex 디스플레이 유형은 다른 분할 버튼, 작업 또는 요소와 인라인으로 맞아야 하므로 이 래핑 구성요소에 적합합니다.

.gui-split-button {
  display: inline-flex;
  border-radius: var(--radius);
  background: var(--theme);
  color: var(--ontheme);
  fill: var(--ontheme);

  touch-action: manipulation;
  user-select: none;
  -webkit-tap-highlight-color: transparent;
}

분할 버튼

<button> 스타일 지정

버튼은 얼마나 많은 코드가 필요한지 위장하는 데 매우 유용합니다. 브라우저 기본 스타일을 실행취소하거나 교체해야 할 수 있지만 일부 상속을 적용하고 상호작용 상태를 추가하며 다양한 사용자 환경설정과 입력 유형에 맞게 조정해야 합니다. 버튼 스타일은 빠르게 추가됩니다.

이러한 버튼은 상위 요소와 배경을 공유한다는 점에서 일반 버튼과 다릅니다. 일반적으로 버튼은 배경과 텍스트 색상을 소유합니다. 하지만 공유하며 상호작용 시 각자의 배경만 적용합니다.

.gui-split-button button {
  cursor: pointer;
  appearance: none;
  background: none;
  border: none;

  display: inline-flex;
  align-items: center;
  gap: 1ch;
  white-space: nowrap;

  font-family: inherit;
  font-size: inherit;
  font-weight: 500;

  padding-block: 1.25ch;
  padding-inline: 2.5ch;

  color: var(--ontheme);
  outline-color: var(--theme);
  outline-offset: -5px;
}

몇 가지 CSS 유사 클래스 및 상태에 일치하는 맞춤 속성을 사용하여 상호작용 상태를 추가합니다.

.gui-split-button button {
  …

  &:is(:hover, :focus-visible) {
    background: var(--theme-hover);
    color: var(--ontheme);

    & > svg {
      stroke: currentColor;
      fill: none;
    }
  }

  &:active {
    background: var(--theme-active);
  }
}

기본 버튼에 디자인 효과를 완성하려면 몇 가지 특별한 스타일이 필요합니다.

.gui-split-button > button {
  border-end-start-radius: var(--radius);
  border-start-start-radius: var(--radius);

  & > svg {
    fill: none;
    stroke: var(--ontheme);
  }
}

마지막으로 밝은 테마 버튼과 아이콘에 그림자가 추가되어 멋진 효과를 줍니다.

.gui-split-button {
  @media (--light) {
    & > button,
    & button:is(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--theme-active);
    }
    & > .gui-popup-button > svg,
    & button:is(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--theme-active));
    }
  }
}

세세한 상호작용과 작은 세부 사항에도 주의를 기울이는 버튼을 잘 만들 수 있습니다.

:focus-visible 관련 참고사항

버튼 스타일이 :focus 대신 :focus-visible를 사용하는 방식을 확인하세요. :focus는 액세스 가능한 사용자 인터페이스를 만드는 데 중요한 터치이지만 한 가지 단점이 있습니다. 사용자가 인터페이스를 봐야 하는지 여부에 대해 지능적이지 않고 모든 포커스에 적용됩니다.

아래 동영상은 이러한 마이크로 상호작용을 세분화하여 :focus-visible가 어떻게 지능적인 대안인지 보여줍니다.

팝업 버튼의 스타일 지정

아이콘을 중앙에 배치하고 팝업 버튼 목록을 고정하는 4ch Flexbox 기본 버튼과 마찬가지로 마우스를 가져가거나 상호작용하고 확장될 때까지 투명합니다.

팝업을 트리거하는 데 사용되는 분할 버튼의 화살표 부분입니다.

.gui-popup-button {
  inline-size: 4ch;
  cursor: pointer;
  position: relative;
  display: inline-flex;
  align-items: center;
  justify-content: center;
  border-inline-start: var(--border);
  border-start-end-radius: var(--radius);
  border-end-end-radius: var(--radius);
}

CSS 중첩:is() 기능 선택기를 사용하여 마우스 오버, 포커스, 활성 상태의 레이어를 만듭니다.

.gui-popup-button {
  …

  &:is(:hover,:focus-within) {
    background: var(--theme-hover);
  }

  /* fixes iOS trying to be helpful */
  &:focus {
    outline: none;
  }

  &:active {
    background: var(--theme-active);
  }
}

이러한 스타일은 팝업을 표시하고 숨기기 위한 기본 후크입니다. .gui-popup-button의 하위 요소 중 하나에 focus가 있으면 아이콘과 팝업에서 opacity, 위치, pointer-events를 설정합니다.

.gui-popup-button {
  …

  &:focus-within {
    & > svg {
      transition-duration: var(--in-speed);
      transform: rotateZ(.5turn);
    }
    & > .gui-popup {
      transition-duration: var(--in-speed);
      opacity: 1;
      transform: translateY(0);
      pointer-events: auto;
    }
  }
}

인 및 아웃 스타일이 완성되면 마지막 단계는 사용자의 모션 환경설정에 따라 조건부로 변환을 전환하는 것입니다.

.gui-popup-button {
  …

  @media (--motionOK) {
    & > svg {
      transition: transform var(--out-speed) ease;
    }
    & > .gui-popup {
      transform: translateY(5px);

      transition:
        opacity var(--out-speed) ease,
        transform var(--out-speed) ease;
    }
  }
}

코드를 예리하게 살펴보면 모션 감소를 선호하는 사용자의 불투명도가 여전히 전환되는 것을 확인할 수 있습니다.

팝업 스타일 지정

.gui-popup 요소는 맞춤 속성과 상대 단위를 사용하는 플로팅 카드 버튼 목록으로, 비교적 크기가 작아지고 기본 버튼과의 상호작용 방식으로 일치되며 브랜드에 색상을 사용합니다. 아이콘은 대비가 덜 하고 더 얇으며 그림자에 브랜드 파란색이 약간 있습니다. 버튼과 마찬가지로 강력한 UI 및 UX는 이러한 작은 디테일이 축적된 결과입니다.

플로팅 카드 요소입니다.

.gui-popup {
  --shadow: 220 70% 15%;
  --shadow-strength: 1%;

  opacity: 0;
  pointer-events: none;

  position: absolute;
  bottom: 80%;
  left: -1.5ch;

  list-style-type: none;
  background: var(--popupbg);
  color: var(--theme-text);
  padding-inline: 0;
  padding-block: .5ch;
  border-radius: var(--radius);
  overflow: hidden;
  display: flex;
  flex-direction: column;
  font-size: .9em;
  transition: opacity var(--out-speed) ease;

  box-shadow:
    0 -2px 5px 0 hsl(var(--shadow) / calc(var(--shadow-strength) + 5%)),
    0 1px 1px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 10%)),
    0 2px 2px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 12%)),
    0 5px 5px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 13%)),
    0 9px 9px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 14%)),
    0 16px 16px -2px hsl(var(--shadow) / calc(var(--shadow-strength) + 20%))
  ;
}

아이콘과 버튼에는 각 어두운 테마와 밝은 테마의 카드 내에서 멋지게 스타일을 지정할 수 있도록 브랜드 색상이 지정됩니다.

결제, 빠른 결제, 나중을 위해 저장하기 위한 링크 및 아이콘

.gui-popup {
  …

  & svg {
    fill: var(--popupbg);
    stroke: var(--theme);

    @media (prefers-color-scheme: dark) {
      stroke: var(--theme-border);
    }
  }

  & button {
    color: var(--theme-text);
    width: 100%;
  }
}

어두운 테마 팝업에는 텍스트와 아이콘 그림자가 추가되며 약간 더 강한 상자 그림자가 있습니다.

어두운 테마의 팝업입니다.

.gui-popup {
  …

  @media (--dark) {
    --shadow-strength: 5%;
    --shadow: 220 3% 2%;

    & button:not(:focus-visible, :hover) {
      text-shadow: 0 1px 0 var(--ontheme);
    }

    & button:not(:focus-visible, :hover) > svg {
      filter: drop-shadow(0 1px 0 var(--ontheme));
    }
  }
}

일반 <svg> 아이콘 스타일

모든 아이콘은 ch 단위를 inline-size로 사용하여 사용되는 버튼 font-size에 상대적으로 크기가 조정됩니다. 아이콘의 윤곽선을 부드럽고 매끄럽게 하기 위한 스타일도 각각 제공됩니다.

.gui-split-button svg {
  inline-size: 2ch;
  box-sizing: content-box;
  stroke-linecap: round;
  stroke-linejoin: round;
  stroke-width: 2px;
}

오른쪽에서 왼쪽 레이아웃

논리적 속성은 모든 복잡한 작업을 실행합니다. 사용된 논리 속성 목록은 다음과 같습니다. display: inline-flex는 인라인 flex 요소를 만듭니다. padding-blockpadding-inlinepadding 약식 대신 쌍으로 사용하면 논리 변을 패딩하는 이점을 얻을 수 있습니다. - border-end-start-radiusfriends는 문서 방향에 따라 모서리를 둥글게 처리합니다. - width 대신 inline-size를 사용하면 크기가 실제 크기에 연결되지 않습니다. - border-inline-start는 시작 부분에 테두리를 추가합니다. 테두리는 스크립트 방향에 따라 오른쪽이나 왼쪽에 있을 수 있습니다.

JavaScript

다음 JavaScript는 모두 접근성을 향상하기 위한 것입니다. 두 개의 도우미 라이브러리를 사용하여 작업을 좀 더 쉽게 할 수 있습니다. BlingBlingJS는 간결한 DOM 쿼리와 간편한 이벤트 리스너 설정에 사용되며 roving-ux는 팝업에서 액세스 가능한 키보드 및 게임패드 상호작용을 용이하게 하는 데 도움이 됩니다.

import $ from 'blingblingjs'
import {rovingIndex} from 'roving-ux'

const splitButtons = $('.gui-split-button')
const popupButtons = $('.gui-popup-button')

위의 라이브러리를 가져오고 요소를 선택하여 변수에 저장하면 몇 가지 함수만 있으면 환경을 업그레이드할 수 있습니다.

Roving 색인

키보드 또는 스크린 리더가 .gui-popup-button에 포커스를 맞추면 .gui-popup의 첫 번째 (또는 가장 최근에 포커스가 맞춰진) 버튼으로 포커스를 전달하려고 합니다. 라이브러리는 elementtarget 매개변수를 사용하여 이 작업을 실행하는 데 도움이 됩니다.

popupButtons.forEach(element =>
  rovingIndex({
    element,
    target: 'button',
  }))

이제 요소가 타겟 <button> 하위 요소에 포커스를 전달하고 표준 화살표 키 탐색을 사용하여 옵션을 탐색할 수 있습니다.

aria-expanded 전환 중

팝업이 표시되고 숨겨지는 것을 시각적으로 확인할 수 있지만 스크린 리더에는 시각적 신호 이상의 것이 필요합니다. 여기서 JavaScript를 사용하여 스크린 리더의 적절한 속성을 전환하여 CSS 기반 :focus-within 상호작용을 보완합니다.

popupButtons.on('focusin', e => {
  e.currentTarget.setAttribute('aria-expanded', true)
})

popupButtons.on('focusout', e => {
  e.currentTarget.setAttribute('aria-expanded', false)
})

Escape 키 사용 설정

사용자의 포커스가 의도적으로 트랩으로 이동되었으므로 이탈할 방법을 제공해야 합니다. 가장 일반적인 방법은 Escape 키 사용을 허용하는 것입니다. 이렇게 하려면 팝업 버튼의 키 누름을 관찰합니다. 하위 요소의 모든 키보드 이벤트가 이 상위 요소 위로 올라올 것이기 때문입니다.

popupButtons.on('keyup', e => {
  if (e.code === 'Escape')
    e.target.blur()
})

팝업 버튼에 Escape 키가 눌리면 blur()를 사용하여 포커스를 자체에서 삭제합니다.

분할 버튼 클릭수

마지막으로 사용자가 클릭하거나 탭하거나 키보드가 작동하는 경우 애플리케이션은 적절한 작업을 실행해야 합니다. 여기서 이벤트 버블링이 다시 사용되지만 이번에는 .gui-split-button 컨테이너에서 하위 팝업 또는 기본 작업에서 버튼 클릭을 포착합니다.

splitButtons.on('click', event => {
  if (event.target.nodeName !== 'BUTTON') return
  console.info(event.target.innerText)
})

결론

이제 제가 어떻게 했는지 알았으니 어떻게 되세요?‽ 🙂

접근 방식을 다양화하고 웹에서 빌드하는 모든 방법을 알아보겠습니다. 데모를 만들고 링크를 트윗해 주세요. 그러면 아래의 커뮤니티 리믹스 섹션에 추가하겠습니다.

커뮤니티 리믹스