Advertisement

Merging Pill Filter Buttons

| by Vladimir | 2 min read | code by Fabio Ottaviani
Intermediate

Tech & Dependencies

HTML SCSS Babel

Features

  • Visual Merging
  • Adjacent Sibling Selector
  • Negative Margins
  • Fluid Scaling

Browser Support

Chrome 60+ Edge 79+ Firefox 50+ Safari 10+

Core

This Merging Pill Filter Button set introduces a fluid, organic feel to standard multi-select inputs. Instead of keeping active items isolated, this component visually fuses adjacent selected buttons into a single continuous shape. It’s an excellent choice for music apps, filtering systems, or any UI where categories are related and grouping them visually adds context.

Core Technique

The “fusion” effect is achieved entirely through CSS using the Adjacent Sibling Combinator (+). The logic checks if a button with the class .active is immediately followed by another .button.active.

If this condition is met, the code modifies the second button to make it look attached to the first:

  1. Negative Margin: It pulls the second button to the left using margin-left.
  2. Border Removal: It removes the left border and the left-side border-radius (border-radius: 0 ...).
  3. Separator: A pseudo-element (:before) scales up to create a thin divider line between the merged items.

Customization

The component relies heavily on a single CSS variable --fontSize for scaling. Changing this value resizes the buttons, padding, and border radius proportionally using calc().

body {
  /* Controls the scale of the entire component */
  --fontSize: 14px; 
}

.button {
  /* ... styles ... */
  
  &.active {
    background: #3a3d42;
    
    /* Logic for the second active button */
    & + .button.active {
      /* Removes the gap to merge them */
      margin-left: calc(var(--fontSize) * -2);
      
      /* Flattens the left side */
      border-radius: 0 20px 20px 0;
      border-left: none;
    }
  }
}

To change the interaction logic (e.g., allow only one active button), you would modify the JavaScript event listener.

buttons.forEach(b => {
  b.addEventListener('click', () => {
    // Current logic: Toggle (Multi-select)
    b.classList.toggle('active');
    
    /* 
       For Single-select (Radio behavior), use this instead:
       
       buttons.forEach(btn => btn.classList.remove('active'));
       b.classList.add('active');
    */
  })
})

Tips

1. Accessibility (A11y): While the buttons use semantic <button> tags, the visual state is controlled by a class. For screen readers, you must toggle the aria-pressed attribute alongside the class to indicate the state.

b.addEventListener('click', () => {
  b.classList.toggle('active');
  // Sync ARIA state
  const isActive = b.classList.contains('active');
  b.setAttribute('aria-pressed', isActive);
})

2. Wrapping Issues: This effect relies on the buttons being on a single line. The container uses white-space: nowrap to prevent wrapping. If the buttons wrap to a new line, the “merging” effect (removing the left border) will look broken on the first item of the new line. Keep them in a scrolling container if you have many options.

Advertisement