Разделенные кнопки

Этот шаблон показывает, как создать адаптивный и доступный компонент разделенной кнопки.

Полная статья · Видео на 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) })