Multi-Select

This pattern shows how to build a responsive, adaptive, and accessible, multiselect component for sort and filter user experiences.

Full article · Video on YouTube · Source on Github

HTML

<main>   <header>     <h1>Lighting</h1>     <small>Find your perfect light</small>   </header>   <aside>     <form>       <select multiple="true" title="Filter results by category">         <optgroup label="New">           <option value="last 30 days">Last 30 Days</option>           <option value="last 6 months">Last 6 Months</option>         </optgroup>         <optgroup label="Lamps">           <option value="table lamps">Table Lamps</option>           <option value="desk lamps">Desk Lamps</option>           <option value="floor lamps">Floor Lamps</option>         </optgroup>         <optgroup label="Ceiling">           <option value="chandeliers">Chandeliers</option>           <option value="pendant">Pendant</option>           <option value="flush">Flush</option>           <option value="fans">Fans</option>         </optgroup>         <optgroup label="By Room">           <option value="bedroom">Bedroom</option>           <option value="dining room">Dining Room</option>           <option value="kitchen">Kitchen</option>           <option value="living room">Living Room</option>           <option value="bathroom">Bathroom</option>           <option value="entryway">Entryway</option>           <option value="outdoor">Outdoor</option>         </optgroup>         <optgroup label="Kids">           <option value="lamps">Lamps</option>           <option value="night lights">Night Lights</option>           <option value="ceiling">Ceiling</option>         </optgroup>       </select>              <fieldset>         <legend>New</legend>         <div>           <input type="checkbox" id="last-30-days" name="new" value="last 30 days">           <label for="last-30-days">Last 30 Days</label>         </div>         <div>           <input type="checkbox" id="last-6-months" name="new" value="last 6 months">           <label for="last-6-months">Last 6 Months</label>         </div>       </fieldset>       <fieldset>         <legend>Lamps</legend>         <div>           <input type="checkbox" id="table-lamps" name="lamps" value="table lamps">           <label for="table-lamps">Table Lamps</label>         </div>         <div>           <input type="checkbox" id="desk-lamps" name="lamps" value="desk lamps">           <label for="desk-lamps">Desk Lamps</label>         </div>         <div>           <input type="checkbox" id="floor-lamps" name="lamps" value="floor lamps">           <label for="floor-lamps">Floor Lamps</label>         </div>       </fieldset>       <fieldset>         <legend>Ceiling</legend>         <div>           <input type="checkbox" id="chandeliers" name="ceiling" value="chandeliers">           <label for="chandeliers">Chandeliers</label>         </div>         <div>           <input type="checkbox" id="pendant" name="ceiling" value="pendant">           <label for="pendant">Pendant</label>         </div>         <div>           <input type="checkbox" id="flush" name="ceiling" value="flush">           <label for="flush">Flush</label>         </div>         <div>           <input type="checkbox" id="fans" name="ceiling" value="fans">           <label for="fans">Fans</label>         </div>       </fieldset>       <fieldset>         <legend>By Room</legend>         <div>           <input type="checkbox" id="bedroom" name="by room" value="bedroom">           <label for="bedroom">Bedroom</label>         </div>         <div>           <input type="checkbox" id="dining-room" name="by room" value="dining room">           <label for="dining-room">Dining Room</label>         </div>         <div>           <input type="checkbox" id="kitchen" name="by room" value="kitchen">           <label for="kitchen">Kitchen</label>         </div>         <div>           <input type="checkbox" id="living-room" name="by room" value="living room">           <label for="living-room">Living Room</label>         </div>         <div>           <input type="checkbox" id="bathroom" name="by room" value="bathroom">           <label for="bathroom">Bathroom</label>         </div>         <div>           <input type="checkbox" id="entryway" name="by room" value="entryway">           <label for="entryway">Entryway</label>         </div>         <div>           <input type="checkbox" id="outdoor" name="by room" value="outdoor">           <label for="outdoor">Outdoor</label>         </div>       </fieldset>       <fieldset>         <legend>Kids</legend>         <div>           <input type="checkbox" id="lamps" name="kids" value="lamps">           <label for="lamps">Lamps</label>         </div>         <div>           <input type="checkbox" id="night-lights" name="kids" value="night lights">           <label for="night-lights">Night Lights</label>         </div>         <div>           <input type="checkbox" id="ceiling" name="kids" value="ceiling">           <label for="ceiling">Ceiling</label>         </div>       </fieldset>     </form>     <div role="status" class="sr-only" id="applied-filters"></div>   </aside>   <article>     <span class="last-30-days table-lamps"></span>     <span class="last-6-months desk-lamps"></span>     <span class="floor-lamps"></span>     <span class="last-6-months chandeliers"></span>     <span class="pendant last-6-months"></span>     <span class="flush fans"></span>     <span class="fans pendant table-lamps"></span>     <span class="bedroom"></span>     <span class="dining-room last-30-days chandeliers"></span>     <span class="kitchen lamps"></span>     <span class="living-room"></span>     <span class="bathroom living-room chandeliers desk-lamps"></span>     <span class="bathroom table-lamps desk-lamps"></span>     <span class="entryway last-30-days"></span>     <span class="outdoor desk-lamps"></span>     <span class="lamps last-30-days"></span>     <span class="night-lights table-lamps"></span>     <span class="ceiling last-30-days"></span>     <span class="floor-lamps table-lamps"></span>     <span class="floor-lamps last-6-months"></span>     <span class="dining-room last-30-days chandeliers"></span>     <span class="kitchen lamps"></span>     <span class="living-room"></span>     <span class="bathroom living-room chandeliers desk-lamps"></span>   </article> </main>

CSS

         main {   display: grid;   grid-template-columns: max-content 1fr;   gap: 5vmin;   align-items: flex-start;    & > header {     grid-column: 1 / -1;   }    @media (orientation: portrait) {     grid-template-columns: 1fr;   }    @media (--useSelect) {     & > article {       grid-row: 3;       grid-column: 1 / -1;     }   } }  article {   --size: min(300px, calc(25% - 2ch));   margin: -1ch;    & > span {     will-change: transform;     background: hsl(0 0% 50% / 25%);     border-radius: 10px;     inline-size: var(--size);     block-size: 15ch;     margin: 1ch;      @media (orientation: portrait) {       --size: calc(50% - 2ch);     }      @supports (aspect-ratio: 1) {       block-size: auto;       aspect-ratio: 1;     }   } }  header {   display: grid;   gap: 1ch; }  aside {   counter-reset: filters;    & :checked {     counter-increment: filters;    }    & #applied-filters::before {     content: counter(filters) " filters ";   } }  fieldset:first-of-type {   margin-block-start: -5px; }  [role="status"] {   @media (--useSelect) {     display: none;   } }  .sr-only {   inline-size: 0;   block-size: 0;   overflow: hidden; }         

JS

         import 'https://unpkg.com/[email protected]/dist/isotope.pkgd.min.js'  const IsotopeGrid = new Isotope( 'article', {   itemSelector: 'span',   layoutMode: 'fitRows',   percentPosition: true })    const filterGrid = query => {   const { matches:motionOK } = window.matchMedia(     '(prefers-reduced-motion: no-preference)'   )      IsotopeGrid.arrange({     filter: query,     stagger: 25,     transitionDuration: motionOK ? '0.4s' : 0,   }) }  // takes a  const prepareSelectOptions = element =>   Array.from(element.selectedOptions).reduce((data, opt) => {     data.push([opt.parentElement.label.toLowerCase(), opt.value])     return data   }, [])  //  document.querySelector('select').addEventListener('input', e => {   let selectData = prepareSelectOptions(e.target)   console.warn('Multiselect', selectData)    // DEMO   // isotope query assembly from checkbox selections   let query = selectData.reduce((query, val) => {     query.push('.' + val[1].split(' ').join('-'))     return query   }, []).join(',')    filterGrid(query)    // update for assistive technology   let statusRoleElement = document.querySelector('#applied-filters')   let filterResults = IsotopeGrid.getFilteredItemElements().length    statusRoleElement.style.counterSet = selectData.length   statusRoleElement.textContent = " giving " + filterResults + " results" })  document   .querySelector('aside form')   .addEventListener('input', e => {     if (e.target.nodeName === 'SELECT') return            const formData = new FormData(document.querySelector('form'))     console.warn('Checkboxes', Array.from(formData.entries()))      // DEMO     // isotope query assembly from checkbox selections     let query = Array.from(formData.values()).reduce((query, val) => {       query.push('.' + val.split(' ').join('-'))       return query     }, []).join(',')      filterGrid(query)      document.querySelector('#applied-filters').textContent = " giving " + IsotopeGrid.getFilteredItemElements().length + " results"   })