액세스 가능한 분할 버튼 구성요소를 빌드하는 방법에 관한 기본 개요
이 게시물에서는 분할 버튼을 빌드하는 방법에 대한 생각을 공유하고자 합니다. 데모 사용해 보기
동영상을 선호한다면 이 게시물의 YouTube 버전을 참고하세요.
개요
분할 버튼은 기본 버튼과 추가 버튼 목록을 숨기는 버튼입니다. 덜 자주 사용되는 보조 작업을 필요할 때까지 중첩하면서 일반적인 작업을 노출하는 데 유용합니다. 분할 버튼은 복잡한 디자인을 최소화하는 데 중요한 역할을 할 수 있습니다. 고급 분할 버튼은 마지막 사용자 작업을 기억하고 기본 위치로 승격할 수도 있습니다.
일반적인 분할 버튼은 이메일 애플리케이션에서 찾을 수 있습니다. 기본 작업은 전송이지만 나중에 보내거나 초안을 저장할 수도 있습니다.
공유 작업 영역은 사용자가 둘러볼 필요가 없으므로 유용합니다. 필수 이메일 작업이 분할 버튼에 포함되어 있음을 알고 있습니다.
부품
전체 조정 및 최종 사용자 환경을 논의하기 전에 분할 버튼의 필수 부분을 살펴보겠습니다. 여기서는 VisBug의 접근성 검사 도구를 사용하여 구성요소의 매크로 보기를 표시하고 각 주요 부분의 HTML, 스타일 및 접근성을 표시합니다.
최상위 분할 버튼 컨테이너
최상위 구성요소는 기본 작업 및 .gui-popup-button
가 포함된 gui-split-button
클래스가 있는 인라인 Flexbox입니다.
기본 작업 버튼
처음에는 표시되고 포커스를 받을 수 있는 <button>
가 컨테이너 내에 들어맞으며 포커스, 마우스 오버, 활성 상호작용에 맞는 두 개의 모서리 도형이 일치하여 .gui-split-button
내에 포함된 것처럼 보입니다.
팝업 전환 버튼
'팝업 버튼' 지원 요소는 보조 버튼 목록을 활성화하고 암시하기 위한 것입니다. <button>
가 아니며 포커스를 설정할 수 없습니다. 하지만 .gui-popup
의 위치 지정 앵커이자 팝업을 표시하는 데 사용되는 :focus-within
의 호스트입니다.
팝업 카드
이는 앵커 .gui-popup-button
의 플로팅 카드 하위 요소로, 절대적으로 배치되고 버튼 목록을 시맨틱 방식으로 래핑합니다.
보조 작업
기본 작업 버튼보다 글꼴 크기가 약간 작은 포커스 가능한 <button>
에는 아이콘과 기본 버튼에 대한 보완 스타일이 있습니다.
맞춤 속성
다음 변수는 색상 조화를 만들고 구성요소 전체에서 사용되는 값을 수정할 수 있는 중앙 장소를 만드는 데 도움이 됩니다.
@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-haspopup
및 aria-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-block
및 padding-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
의 첫 번째(또는 가장 최근에 포커스가 있던) 버튼으로 포커스를 전달해야 합니다. 라이브러리는 element
및 target
매개변수를 사용하여 이 작업을 실행하는 데 도움이 됩니다.
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)
})
결론
이제 제가 어떻게 했는지 알았으니 어떻게 하시겠어요? 🙂
접근방식을 다각화하고 웹에서 빌드하는 방법을 모두 알아보겠습니다. 데모를 만들어 트윗해 주시면 아래의 커뮤니티 리믹스 섹션에 추가해 드리겠습니다.