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.
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 #f00!important;
}
:has([type="radio"]:checked)& {
outline: 2px dashed #00f;
}
&:hover {
outline: 2px dashed #8c00ff!important;
}
}
&: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>
A second iteration takes the approach further, relying in addition on just the :last-child
and :nth-child()
CSS pseudo-classes in combination with the :is()
CSS pseudo-class in order to establish generic rules for switches that can span ranges in between 2 and 5 radio-controls/options; thus the former solution's attribute dependencies are not longer necessary.
[data-is="multiswitch"] {
&:has([name="rating"]) {
width: 50%;
}
&:has([name="switch"]) {
width: 70%;
}
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: 1px;
top: 2px;
width: 33.33%;
height: calc(100% - 4px);
border-radius: 12px;
background: #747bff;
transition-property: left;
transition-duration: .3s;
}
label {
float: left;
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 #f00!important;
}
:has([type="radio"]:checked)& {
outline: 2px dashed #00f;
}
&:hover {
outline: 2px dashed #8c00ff!important;
}
}
&:has([type="radio"]:checked)::before {
z-index: -1;
}
&:has(:last-child:is(:nth-child(2))) {
label,
&::before {
width: 50%;
}
&:has(:last-child [type="radio"]:checked)::before {
left: calc(50% - 1px);
}
}
&:has(:last-child:is(:nth-child(3))) {
label,
&::before {
width: 33.33%;
}
&:has(:nth-child(2) [type="radio"]:checked)::before {
left: 33.33%;
}
&:has(:last-child [type="radio"]:checked)::before {
left: calc(66.66% - 1px);
}
}
&:has(:last-child:is(:nth-child(4))) {
label,
&::before {
width: 25%;
}
&:has(:nth-child(2) [type="radio"]:checked)::before {
left: 25%;
}
&:has(:nth-child(3) [type="radio"]:checked)::before {
left: 50%;
}
&:has(:last-child [type="radio"]:checked)::before {
left: calc(75% - 1px);
}
}
&:has(:last-child:is(:nth-child(5))) {
label,
&::before {
width: 20%;
}
&:has(:nth-child(2) [type="radio"]:checked)::before {
left: 20%;
}
&:has(:nth-child(3) [type="radio"]:checked)::before {
left: 40%;
}
&:has(:nth-child(4) [type="radio"]:checked)::before {
left: 60%;
}
&:has(:last-child [type="radio"]:checked)::before {
left: calc(80% - 1px);
}
}
}
body { margin: 0; }
<form data-is="multiswitch">
<label>
<input type="radio" value="1" name="options">
<span>Option 1</span>
</label>
<label>
<input type="radio" value="2" name="options">
<span>Option 2</span>
</label>
<label>
<input type="radio" value="3" name="options">
<span>Option 3</span>
</label>
</form>
<form data-is="multiswitch">
<label>
<input type="radio" value="aaa" name="rating">
<span>AAA</span>
</label>
<label>
<input type="radio" value="aa" name="rating" checked>
<span>AA</span>
</label>
<label>
<input type="radio" value="ab" name="rating">
<span>AB</span>
</label>
<label>
<input type="radio" value="b" name="rating">
<span>B</span>
</label>
<label>
<input type="radio" value="c" name="rating">
<span>C</span>
</label>
</form>
<form data-is="multiswitch">
<label>
<input type="radio" value="0" name="switch">
<span>Nej</span>
</label>
<label>
<input type="radio" value="1" name="switch" checked>
<span>Yey</span>
</label>
</form>