skip to Main Content

I’m making a selection bar for an app by using heavily styled radio buttons so I can make sure there can just be one element selected at a time and also using handy styling tricks so I had to code less. However, now I’m trying to animate them, using a slight ease whenever the selected button changes. I have changed the background color whenever an element is selected and I want it to move it linearly along the line until it reaches the new position of the element that should have the background color. Is that possible with just CSS and HTML? Or do I need to add a bit of JavaScript to make it work? And if yes, how exactly?

I tried many things, like adding the background color on different places, I tried different ways of animating, but nothing worked out the way I liked. I managed to get an animation where the form would collapse when I switched buttons, so it isn’t impossible to trigger an animation on input change.

Here’s my code (I’ve tried to make it slightly more generic so other people can benefit from this too:)

let options; // = "OPTION1" | "OPTION2" | "OPTION3" = "OPTION1";

function handleClick() {
  const option1 = document.getElementById('option1');
  const option2 = document.getElementById('option2');
  const option3 = document.getElementById('option3');

  if (option1.checked) {
      options = "OPTION1";
  } else if (option2.checked) {
      options = "OPTION2";
  } else if (option3.checked) {
      options = "OPTION3";
  }
  console.log(`debug > option selected: ${ options }`);
}
input[type="radio"] {
  display: none;
}

form {
  display: flex;
  align-items: center;
  justify-content: space-between;
  flex-wrap: nowrap;
  overflow: hidden;
  background-color: #242424;
  color: white; /* needed for debugging, otherwise you can't really visualize what happens */
  border-radius: 10px;
  padding-left: 5px;
  padding-right: 5px;
}

label {
  display: flex;
  align-items: center;
  cursor: pointer;
  max-width: 300px;
  white-space: nowrap;
  overflow: hidden; 
  text-overflow: ellipsis;
  border-radius: 10px;
  margin: 0; 
}

form > label:not(:first-child) {
  margin-left: 5px;
}

*, *::before, *::after {
  box-sizing: border-box;
}

input[type="radio"]:checked + span {
  background-color: #747bff;
  padding: 10px;
  border-radius: 10px;
  margin: 0;
}

label span {
  margin-left: 0;
}
<div class="options">
  <form>
    <label>
      <input type="radio" id="option1" name="options" onclick="handleClick(event)" checked>
      <span>Option 1</span>
    </label>
    <label>
      <input type="radio" id="option2" name="options" onclick="handleClick(event)">
      <span>Option 2</span>
    </label>
    <label>
      <input type="radio" id="option3" name="options" onclick="handleClick(event)">
      <span>Option 3</span>
    </label>
  </form>
</div>

PS. Some other thing I was encountering was that there was a slight gap between the form and the label if I’d select the first element. It would be nice if you guys could also help me with that.

4

Answers


  1. Here is an article to help you out for animation of radio buttons and checkbox
    https://codepen.io/im_vikasdeep/pen/oNxwjZJ

    You can replace the checkbox to radio

    * {
        box-shadow: none;
    }
    
    body {
        font-family: 'Poppins', sans-serif;
        margin: 0;
        width: 100%;
        height: 100vh;
        background-color: #d1dad3;
        display: flex;
        justify-content: center;
        align-items: center;
        font-size: 17px;
    }
    
    .container {
        max-width: 1000px;
        width: 100%;
        display: flex;
        flex-wrap: wrap;
        justify-content: space-around;
    }
    
    .switch-holder {
        display: flex;
        padding: 10px 20px;
        border-radius: 10px;
        margin-bottom: 30px;
        box-shadow: -8px -8px 15px rgba(255,255,255,.7),
                    10px 10px 10px rgba(0,0,0, .3),
                    inset 8px 8px 15px rgba(255,255,255,.7),
                    inset 10px 10px 10px rgba(0,0,0, .3);
        justify-content: space-between;
        align-items: center;
    }
    
    .switch-label {
        width: 150px;
    }
    
    .switch-label i {
        margin-right: 5px;
    }
    
    .switch-toggle {
        height: 40px;
    }
    
    .switch-toggle input[type="checkbox"] {
        position: absolute;
        opacity: 0;
        z-index: -2;
    }
    
    .switch-toggle input[type="checkbox"] + label {
        position: relative;
        display: inline-block;
        width: 100px;
        height: 40px;
        border-radius: 20px;
        margin: 0;
        cursor: pointer;
        box-shadow: inset -8px -8px 15px rgba(255,255,255,.6),
                    inset 10px 10px 10px rgba(0,0,0, .25);
        
    }
    
    .switch-toggle input[type="checkbox"] + label::before {
        position: absolute;
        content: 'OFF';
        font-size: 13px;
        text-align: center;
        line-height: 25px;
        top: 8px;
        left: 8px;
        width: 45px;
        height: 25px;
        border-radius: 20px;
        background-color: #d1dad3;
        box-shadow: -3px -3px 5px rgba(255,255,255,.5),
                    3px 3px 5px rgba(0,0,0, .25);
        transition: .3s ease-in-out;
    }
    
    .switch-toggle input[type="checkbox"]:checked + label::before {
        left: 50%;
        content: 'ON';
        color: #fff;
        background-color: #0000ff;
        box-shadow: -3px -3px 5px rgba(255,255,255,.5),
                    3px 3px 5px #0000ff;
    }
    <div class="container">
            
            <div class="switch-holder">
                <div class="switch-label">
                    <i class="fab fa-bluetooth-b"></i><span>Bluetooth</span>
                </div>
                <div class="switch-toggle">
                    <input type="checkbox" id="bluetooth">
                    <label for="bluetooth"></label>
                </div>
            </div>
    
            <div class="switch-holder">
                <div class="switch-label">
                    <i class="fas fa-wifi"></i><span>wi-fi</span>
                </div>
                <div class="switch-toggle">
                    <input type="checkbox" id="wifi">
                    <label for="wifi"></label>
                </div>
            </div>
    
            <div class="switch-holder">
                <div class="switch-label">
                    <i class="fas fa-map-marker-alt"></i></i><span>Location</span>
                </div>
                <div class="switch-toggle">
                    <input type="checkbox" id="location">
                    <label for="location"></label>
                </div>
            </div>
    
        </div>
    Login or Signup to reply.
  2. You need to move around an element between specific positions; this may not be possible with just CSS. With Svelte it is easy to approximate this using crossfade: You conditionally add the background via an {#if} and let the transition move it via transforms.

    (This is imperfect due to one element fading out and another fading in, but depending on styling and timings this may be hard to notice.)

    <script>
      import { crossfade } from "svelte/transition";
      import { cubicOut } from "svelte/easing";
    
      const options = ["Option 1", "Option 2", "Option 3"];
      let selected = $state(options[0]);
    
      const [send, receive] = crossfade({
        duration: 500,
        easing: cubicOut,
      });
    </script>
    
    <div class="options">
      {#each options as option}
        <div class="option">
          {#if option == selected}
            <div
              class="selected-background"
              in:receive={{ key: "selection" }}
              out:send={{ key: "selection" }}
            >
            </div>
          {/if}
          <label>
            <input
              type="radio"
              name="options"
              value={option}
              bind:group={selected}
            >
            <span>{option}</span>
          </label>
        </div>
      {/each}
    </div>
    
    <style>
      input[type="radio"] {
        display: none;
      }
    
      .options {
        display: flex;
        align-items: center;
        justify-content: space-between;
        background-color: #242424;
        color: white;
        border-radius: 10px;
        padding: 5px;
      }
    
      label {
        display: flex;
        align-items: center;
        cursor: pointer;
        white-space: nowrap;
        border-radius: 10px;
        margin: 0;
        isolation: isolate; /* to keep background behind labels */
      }
      label span {
        padding: 10px;
        margin-left: 0;
      }
    
      /* stacking of background and text */
      .option {
        display: grid;
        place-items: stretch;
      }
      .option > * {
        grid-area: 1 / 1;
      }
    
      .selected-background {
        background-color: #747bff;
        border-radius: 10px;
      }
    </style>
    

    Playground

    You could also e.g. absolutely position an element that always exists and move that around. This would fix the crossfade issues but requires more manual calculation.

    Login or Signup to reply.
  3. With some pseudo-element magic and the subsequent sibling selector, you can create a version of that interface like this. This is just a very quick demo of the technique, but it should be enough to get you moving on your way.

    Any time you want something to animate and move around like that as a purely visual effect, you should look at whether it’s possible to separate that visual indicator from the interactive/text-based nodes so that it can be controlled independently of them.

    The labels have been removed from the radio inputs for this because of needing to use the ~ selector – that requires the indicator to have the same parent as the radio inputs

    input[type="radio"] {
        display: none;
    }
    
    form {
        display: flex;
        align-items: center;
        justify-content: space-between;
        flex-wrap: nowrap;
        overflow: hidden;
        background-color: #242424;
        color: white; /* needed for debugging, otherwise you can't really visualize what happens */
        border-radius: 10px;
        padding-left: 5px;
        padding-right: 5px;
        position: relative;
    }
    
    #indicator, input[type="radio"] {
        display: flex;
        align-items: center;
        cursor: pointer;
        width: 120px;
        height: 40px;
        white-space: nowrap;
        overflow: hidden; 
        text-overflow: ellipsis;
        border-radius: 10px;
        margin: 0; 
    }
    
    input[type="radio"] {
      width: 120px;
      height: 40px;
      cursor: pointer;
      appearance: none;
      z-index: 1;
    }
    
    input[type="radio"]::after {
      content: attr(data-label);
      color: white;
      margin: auto;
    }
    
    input[type="radio"]:checked::after {
      color: orange;
    }
    
    
    #indicator {
      background-color: #747bff;
      padding: 10px;
      border-radius: 10px;
      margin: 0;
      position: absolute;
      left: 0;
      top: 0;
      z-index: 0;
      transition: left 0.3s;
    }
    
    #indicator::before {
      content: attr(data-label);
      display: block;
      margin: auto;
    }
    
    #option1:checked ~ #indicator {
      left: 0;
    }
    
    #option2:checked ~ #indicator {
      left: calc(50% - 60px);
    }
    
    #option3:checked ~ #indicator {
      left: calc(100% - 120px);
    }
    
    
    *, *::before, *::after {
        box-sizing: border-box;
    }
    <div class="options" style="margin-top: 100px">
        <form>
          <input type="radio" id="option1" name="options" data-label="Option 1" checked on:click={handleClick}>
          <input type="radio" id="option2" name="options" data-label="Option 2" on:click={handleClick}>
          <input type="radio" id="option3" name="options" data-label="Option 3" on:click={handleClick}>
          <div id="indicator"></div>
        </form>
    </div>

    Also on a side note, pretty much anything is possible with CSS and HTML – for reference here’s a dungeon crawler built entirely without javascript: https://codepen.io/jcoulterdesign/pen/NOMeEb

    Login or Signup to reply.
  4. What the OP is looking for can be achieved through CSS Generated Content in combination with some additional :has() pseudo-class rules.

    The following approach mainly places all radio-controls at an -3 z-index level.

    Since there is a box, generated ::before the main component, and due to being placed at an -2 z-index level together with its white background, it will cover/hide all radio-controls .

    There will be another box, generated ::after the main component, with a width of roughly 33% of the main component, placed at an -1 z-index level, such that it can slide behind each of the radio-control’s label, yet in front of the generated box that hides the radio-controls.

    The approache’s biggest advantages are … it remains entirely accessible (e.g. keyboard navigation) while its UI is fully customizable.

    [data-is="multiswitch"] {
    
      &.rating {
        width: 30%;
        /* max-width: 340px; */
      }
      position: relative;
      margin: 24px;
      padding: 2px;
      border: 1px solid #aaa;
      border-radius: 12px;
      background: none;
      overflow: hidden;
    
      &::after,
      &::before {
        content: "";
        display: block;
        position: absolute;
        z-index: -2;
        width: 100%;
        height: 100%;
        left: 0;
        top: 0;
        background: white;
      }
      &::before {
        z-index: -3;
        left: 2px;
        top: 2px;
        width: 33.33%;
        height: calc(100% - 4px);
        border-radius: 12px;
        background: #747bff;
        transition-property: left;
        transition-duration: .5s;
      }
      label {
        float: left;
        width: 33.33%;
        border-radius: 12px;
        padding: 2px 0 4px 0;
        text-align: center;
        text-indent: -1.5em;
    
        [type="radio"] {
          position: relative;
          z-index: -3;
        }
        :has([type="radio"]:focus)& {
          outline: 2px dashed #c0f;
        }
        :has([type="radio"]:checked)& {
          outline: 2px dashed #c0f;
        }
        &:hover {
          outline: 2px dashed #0cf;
        }
      }
      &:has([type="radio"]:checked)::before {
        z-index: -1;
      }
      &:has([data-id="opt-1"]:checked)::before {
        left: 1px;
      }
      &:has([data-id="opt-2"]:checked)::before {
        left: 33.33%;
      }
      &:has([data-id="opt-3"]:checked)::before {
        left: calc(66.66% - 1px);
      }
    }
    body { margin: 0; }
    <form data-is="multiswitch">
      <label>
          <input type="radio" data-id="opt-1" name="options">
          <span>Option 1</span>
      </label>
      <label>
          <input type="radio" data-id="opt-2" name="options">
          <span>Option 2</span>
      </label>
      <label>
          <input type="radio" data-id="opt-3" name="options">
          <span>Option 3</span>
      </label>
    </form>
    
    <form data-is="multiswitch" class="rating">
      <label>
          <input type="radio" data-id="opt-1" name="other-options">
          <span>AAA</span>
      </label>
      <label>
          <input type="radio" data-id="opt-2" name="other-options" checked>
          <span>AA</span>
      </label>
      <label>
          <input type="radio" data-id="opt-3" name="other-options">
          <span>B</span>
      </label>
    </form>
    Login or Signup to reply.
Please signup or login to give your own answer.
Back To Top
Search