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 can do this in pure CSS
    Just by moving a background on each text...

    1. First version

    body
      {
      background  : lavender;
      font-family : Geneva, sans-serif;
      font-size   : 16px;
      padding     : 1em;
      }
    label.switchbox
      {
      border        : .4em solid white;
      border-radius : .5em;
      display       : flex;
      width         : fit-content;
      overflow      : hidden;
      cursor        : pointer;
    
      & > input[type="checkbox"]
        {
        display: none;
        }
      & > span
        {
        display    : inline-block;
        position   : relative;
        color      : black;
        padding    : .3em .7em;
        overflow   : hidden;
        }
      & > span::after
        {
        content    : "";
        position   : absolute;
        top        : 0;
        left       : -100%;
        display    : block;
        width      : 200%;
        height     : 100%;
        z-index    : -1;
        transition : all 0.3s ease;
        }
      & > span:first-of-type::after
        {
        background : linear-gradient(to left, #a5a5a5 50%, white 50%);
        }
      & > span:last-of-type::after
        {
        background : linear-gradient(to left, white 50%, #a5a5a5 50%);
        }
      }
    label.switchbox:has(input[type="checkbox"]:checked) > span::after
      {
      left: 0;
      }
    <label class="switchbox">
      <input type="checkbox">
      <span> Preview </span>
      <span> Vue </span>
    </label>


    Other way in pure CSS;
    (with no span::after)

    1. last version (better one, imho)

    body
      {
      background  : lavender;
      font-family : Geneva, sans-serif;
      font-size   : 16px;
      padding     : 1em;
      }
    label.switchbox
      {
      border        : .4em solid white;
      border-radius : .5em;
      display       : flex;
      width         : fit-content;
      overflow      : hidden;
      cursor        : pointer;
      --txtOn       : white;
      --txtOff      : black;
      --bgiOn       : #253342;  /* off black */
      --bgiOff      : white;
    
      & > input[type="checkbox"]
        {
        display: none;
        }
      & > span
        {
        font-weight       : bold;
        padding           : .3em .7em;
        background-repeat : no-repeat;
        background-size   : 0% 100%;
        transition        : all .4s ease-in-out;
        }
      & > span:nth-of-type(1)
        {
        color             : var(--txtOn);
        background-color  : var(--bgiOn);
        background-image  : linear-gradient(var(--bgiOff),var(--bgiOff));
        }
      & > span:nth-of-type(2)
        {
        color             : var(--txtOff);
        background-color  : var(--bgiOff);
        background-image  : linear-gradient(var(--bgiOn),var(--bgiOn));
        }
      &:has(input[type="checkbox"]:checked)
        {
        & > span                { background-size : 100% 100%;     }
        & > span:nth-of-type(1) { color           : var(--txtOff); }
        & > span:nth-of-type(2) { color           : var(--txtOn);  }
        }
      }
    <label class="switchbox">
      <input type="checkbox">
      <span> Preview </span>
      <span> Vue </span>
    </label>