Этот шаблон показывает, как создать адаптивный и доступный компонент разделенной кнопки.
Полная статья · Видео на YouTube · Источник на Github
HTML
<div class="gui-split-button"> <button>View Cart</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="M3 3h2l.4 2M7 13h10l4-8H5.4M7 13L5.4 5M7 13l-2.293 2.293c-.63.63-.184 1.707.707 1.707H17m0 0a2 2 0 100 4 2 2 0 000-4zm-8 2a2 2 0 11-4 0 2 2 0 014 0z" /> </svg> Checkout </button></li> <li><button> <svg aria-hidden="true" viewBox="0 0 24 24"> <path d="M16 11V7a4 4 0 00-8 0v4M5 9h14l1 12H4L5 9z" /> </svg> Quick Pay </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 for later </button></li> </ul> </span> </div> <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> <div class="gui-split-button"> <button>Squash</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> Create a merge commit </button></li> <li><button> Rebase </button></li> </ul> </span> </div>
CSS
.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: 500ms; --out-speed: 100ms; 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; @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%); } & 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; &:is(:hover, :focus-visible) { background: var(--theme-hover); color: var(--ontheme); & > svg { stroke: currentColor; fill: none; } } &:active { background: var(--theme-active); } } & > button { border-radius: var(--radius) 0 0 var(--radius); @supports (border-start-start-radius: 1px) { border-end-start-radius: var(--radius); border-start-start-radius: var(--radius); } } @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)); } } & svg { inline-size: 2ch; box-sizing: content-box; stroke-linecap: round; stroke-linejoin: round; stroke-width: 2px; } } .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-radius: 0 var(--radius) var(--radius) 0; @supports (border-start-start-radius: 1px) { border-inline-start: var(--border); border-start-end-radius: var(--radius); border-end-end-radius: var(--radius); } &:is(:hover,:focus-within) { background: var(--theme-hover); } /* fixes iOS trying to be helpful */ &:focus { outline: none; } &:active { background: var(--theme-active); } &: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; } } @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 { --shadow: 220 70% 15%; --shadow-strength: 1%; opacity: 0; pointer-events: none; position: absolute; inset-block-end: 80%; inset-inline-start: -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%)) ; /* fixes iOS trying to be helpful */ &:focus {outline: none} @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)); } } @media (width <= 400px) { inset-inline-start: -200%; } & svg { fill: var(--popupbg); stroke: var(--theme); @media (prefers-color-scheme: dark) { stroke: var(--theme-border); } } & button { color: var(--theme-text); width: 100%; } }
JS
import $ from 'blingblingjs' import {rovingIndex} from 'roving-ux' const splitButtons = $('.gui-split-button') const popupButtons = $('.gui-popup-button') // popup activating roving index for it's buttons popupButtons.forEach(element => rovingIndex({ element, target: 'button', })) // support escape key popupButtons.on('keyup', e => { if (e.code === 'Escape') e.target.blur() }) popupButtons.on('focusin', e => { e.currentTarget.setAttribute('aria-expanded', true) }) popupButtons.on('focusout', e => { e.currentTarget.setAttribute('aria-expanded', false) }) // respond to any button interaction splitButtons.on('click', event => { if (event.target.nodeName !== 'BUTTON') return console.info(event.target.innerText) })