htmlcssanimationdynamictoggle

CSS: Toggle switch with sliding background and labels of dynamic length


I'm working on a Vue.js project and I'm trying to figure out how to replicate this toggle button from TailGrids:

Toggle switch with two labels 'Preview' and 'Vue' with a sliding background

Sadly, this kind of switch isn't among the examples that TailGrids provides.

I was able to reproduce the general style of the toggle switch with Tailwind, that's rather easy. I was also able to find working code for similar switches.

* { font-family: sans-serif; }

.switchbox label {
  display: inline-block;
  cursor: pointer;
  position: relative;
  width: 70px;
  height: 20px;
  border: 1px solid #c1c1c1;
  border-radius: 3px;
}
.switchbox label span.on,
.switchbox label span.off {
  position: absolute;
  top: 0;
  width: 50%;
  line-height: 1.4;
  font-size: 12px;
  font-weight: 600;
  height: 16px;
  padding: 2px 0;
  text-align: center;
}
.switchbox span.on {  
   color: black;
  right: inherit;
  left: 0;
  border-bottom-right-radius: 2px;
  border-top-right-radius: 2px; 
}
.switchbox span.off {  
  right:0;
  color: black;

  border-bottom-right-radius: 2px;
  border-top-right-radius: 2px;
}
.switchbox input {
  display: none;
}
.switchbox input:checked + label:before {
  
  color: black;
}
.switchbox input:checked + label:after {
  
  color: #737373;
}

label{
    position:relative;    
}

.switchbox input + label:before {
    transition:0.3s;
    content:""; display:block;
    width:50%;
    height:20px;
    position:absolute;
    left:0;
    top:0;
  background: #a5a5a5; /* W3C */    
}

.switchbox input:checked + label:before {
    left:50%; 
}
<div class="switchbox">
    <input type="checkbox" id="switch" name="switch"  />
    <label for="switch" data-on="ON" data-off="OFF">
        <span class="on">ON</span>
        <span class="off">OFF</span>
    </label>
</div>

The problem is that these switches always make the coloured background 50% of the width of the switch. That doesn't work for my project, as one label has to be roughly double the size of the other.

I understand that this implementation animates the label:before pseudo-element and moves its absolute position 50% to the right when the input is checked. But I don't have any clue how I could make this work with two texts of different lengths.

Any help is highly appreciated. I've been researching and fiddling around for roughly 2 hours, but it seems that I'm simply missing the required css expertise.


Solution

  • You need to use a slider element for that, <span> would be a good choice for this. Then you should calculate the clicked items width using javascript of course, then you can adjust different widths for multiple items.

    You can play with the padding etc. to make it more look like the design you sent.

    JS + CSS + HTML

    const buttons = document.querySelectorAll(".toggle-btn");
    const slider = document.querySelector(".slider");
    
    buttons.forEach(btn => {
      btn.addEventListener("click", () => {
    
        // Calculate relative position
        const offset = btn.offsetLeft;
        const width = btn.offsetWidth;
    
        slider.style.width = `${width}px`;
        slider.style.left = `${offset}px`;
      });
    });
    
    // İlk yüklemede default ilk butona slider'ı yerleştir
    window.addEventListener("DOMContentLoaded", () => {
      const defaultBtn = document.querySelector(".toggle-btn[data-side='left']");
      slider.style.width = `${defaultBtn.offsetWidth}px`;
      slider.style.left = `${defaultBtn.offsetLeft}px`;
    });
    .toggle-wrapper {
      font-family: sans-serif;
      padding: 20px;
    }
    
    .toggle-switch {
      position: relative;
      display: inline-flex;
      border: 1px solid #c1c1c1;
      border-radius: 5px;
      overflow: hidden;
    }
    
    .toggle-btn {
      position: relative;
      background: none;
      border: none;
      padding: 8px 16px;
      font-weight: bold;
      cursor: pointer;
      z-index: 2;
      flex: 1;
    }
    
    .slider {
      position: absolute;
      top: 0;
      left: 0;
      height: 100%;
      background-color: #a5a5a5;
      border-radius: 5px;
      transition: all 0.3s ease;
      z-index: 1;
    }
    <div class="toggle-wrapper">
      <div class="toggle-switch">
        <button class="toggle-btn" data-side="left">Preview</button>
        <button class="toggle-btn" data-side="right">Vue</button>
        <span class="slider"></span>
      </div>
    </div>