분할 버튼 구성요소 빌드

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

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

데모

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

개요

분할 버튼은 기본 버튼과 추가 버튼 목록을 숨기는 버튼입니다. 덜 자주 사용되는 보조 작업을 필요할 때까지 중첩하면서 일반적인 작업을 노출하는 데 유용합니다. 분할 버튼은 복잡한 디자인을 최소화하는 데 중요한 역할을 할 수 있습니다. 고급 분할 버튼은 마지막 사용자 작업을 기억하고 기본 위치로 승격할 수도 있습니다.

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

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

공유 작업 영역은 사용자가 둘러볼 필요가 없으므로 유용합니다. 필수 이메일 작업이 분할 버튼에 포함되어 있음을 알고 있습니다.

부품

전체 조정 및 최종 사용자 환경을 논의하기 전에 분할 버튼의 필수 부분을 살펴보겠습니다. 여기서는 VisBug의 접근성 검사 도구를 사용하여 구성요소의 매크로 보기를 표시하고 각 주요 부분의 HTML, 스타일 및 접근성을 표시합니다.

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

최상위 분할 버튼 컨테이너

최상위 구성요소는 기본 작업.gui-popup-button가 포함된 gui-split-button 클래스가 있는 인라인 Flexbox입니다.

gui-split-button 클래스를 검사하고 이 클래스에 사용된 CSS 속성을 표시합니다.

기본 작업 버튼

처음에는 표시되고 포커스를 받을 수 있는 <button>가 컨테이너 내에 들어맞으며 포커스, 마우스 오버, 활성 상호작용에 맞는 두 개의 모서리 도형이 일치하여 .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를 사용할 수 없다는 점입니다. 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 플렉스박스입니다. 기본 버튼과 마찬가지로 마우스를 가져가거나 상호작용할 때까지는 투명하며 채우기 위해 늘어납니다.

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

.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의 줄임말 대신 padding-blockpadding-inline 쌍을 사용하여 논리적 측면에 패딩을 적용하는 이점을 얻습니다. - border-end-start-radius친구는 문서 방향을 기반으로 모서리를 둥글게 처리합니다. - width가 아닌 inline-size는 크기가 실제 크기와 연결되지 않도록 합니다. - border-inline-start는 시작 부분에 테두리를 추가합니다. 테두리는 스크립트 방향에 따라 오른쪽 또는 왼쪽에 있습니다.

자바스크립트

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

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

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

위의 라이브러리를 가져오고 요소를 선택하여 변수에 저장했으므로 환경을 업그레이드하는 데 몇 가지 함수만 남았습니다.

이동 색인

키보드나 화면 리더가 .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)
})

결론

이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂

접근방식을 다각화하고 웹에서 빌드하는 방법을 모두 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.

커뮤니티 리믹스