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
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
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.)
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.
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 inputsAlso 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
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.