One of the most powerful and underutilized CSS selectors is the general sibling combinator:
~. In the coming tutorials I will go over different ways to use
~ to create components that are not only visually appealing, but also functional and useful. This tutorial will cover form elements; radio, checkbox, and regular inputs.
Mastering General Sibling Selectors: Custom Form Elements
We’ll be learning a lot: modern CSS selectors, the
will-change property, SVG’s
stroke properties, input states, and plenty more!
Before We Start..
A quick disclaimer: these CSS effects may or may not work in older browsers–I’ve tested them in the latest versions of Chrome, Firefox, and Safari.
I’ll be using Haml (an HTML compiler) and Sass (a CSS preprocessor) to speed up the coding process! The demos will use Haml while any inline code will use regular HTML.
I’ll also be using the amazing AutoPrefixer instead of vendor prefixes. If you use CodePen, be sure to go to your pen’s settings, click on CSS, and select AutoPrefixer.
We’ll start off with one of the most basic form elements: the radio input. There are two main visual states (checked and unchecked) as well as two in-between states (hover and click/active–click is similar in appearance to checked).
The first step is to set up our HTML. You’ll need a main outer container and an inner container with two children: a
div containing the input and visual elements and a
label for the input. I like to use a
fieldset for the outer container. Make sure you add an ID for the input that matches the
for attribute on the label.
<fieldset> <div class="container"> <div class="svg"> <input id="radio-svg" type="radio" name="option"/> <div></div> <svg viewBox="0 0 24 24"> <circle cx="12" cy="12" r="8"></circle> <circle cx="12" cy="12" r="8"></circle> </svg> </div> <label for="radio-svg">SVG</label> </div> </fieldset>
The next step is to hide the default input, add some basic visual styling, and hide the extra elements that will only show up when the radio input is selected. The idea is to make the input invisible, but position it on top of the visual elements, so that clicking on the radio input looks like clicking on the visual radio option. We can do this by setting
position: relative to
position: absolute to the input, then hiding the input.
Note: you can format this any way you choose. I chose a basic circle style that mimics the default radio, except that it’s flatter.
We’ll set some color variables using Sass; a couple of gray colors and a bright blue for the selected radio. We’ll also set a variable
$p for our default unit–12px is a nice number because it’s divisible by a wide range of different numbers (1, 2, 3, 4, 6).
I put the main variables into the embed directly, but additional styling may be found here or by clicking through to the CodePen page, clicking on Settings in the top right corner, and clicking on the CSS tab to view additional stylesheets.
We’ve made the first circle a light gray and the second one the bright blue, and then hidden the second one by setting
transform: scale(0). Later on, we’ll be animating the circle back in, so it’s important to set the scale now.
After setting all of that up, we need to decide on the visual styles for each state. For this example, I decided that on hover the light gray circle should turn a light blue; on click, the blue circle scales in and the gray background turns white, then remains that way for the checked state. The only way to remove the checked state from a radio is to click another radio, in which case the blue and white should simply fade away.
We’ll set the colors up first, then animate after all the states have colors. This is where the tilde
~ comes in handy. This general sibling selector (the sibling combinator) will select the second element as long as it’s preceded by the first element somewhere, and they share a common parent. We use
input:hover ~ div to select the visual element when the input is hovered over.
Try clicking the first radio, then the second one–you should clearly be able to see hover and active/checked states.
The final step is to set up the animations for each state. The key to animating all these different states is to set the unchecked transitions by default and set the checked transitions when the input is clicked. I’m using a new CSS property called
will-change to let the browser know what properties I’ll be animating.
You can also make this custom radio input without using an SVG. The setup is similar:
<fieldset> <div class="container"> <div class="svg"> <input id="radio-html" type="radio" name="option"/> <div></div> <div></div> <div></div> </div> <label for="radio-html">HTML</label> </div> </fieldset>
The CSS is almost exactly the same, except with slightly more styling for the two
div elements which have replaced the SVG circles. In this case, we use
nth-of-type(n) to select the different div elements so that we don’t have to give them a class in the HTML.
With the SVG version, there’s more markup but less styling; with the HTML version, it’s the other way around. The results look identical, so try whichever one suits your coding preferences!
The next form element we’ll be customizing is the checkbox input. Its states are similar to those of the radio input: two main visual states (checked and unchecked) and two in-between states (hover and click/active).
The setup looks a lot like the radio input, but uses lines to form the checkmark instead of circles.
<fieldset> <div class="container"> <div class="svg"> <input id="checkbox-svg" type="checkbox"> <div></div> <div></div> <svg viewBox="0 0 24 24"> <g> <line x1="4.5" x2="9.24" y1="12.5" y2="17.24"></line> <line x1="9.24" x2="19.76" y1="17.24" y2="6.73"></line> </g> <g> <line x1="4.5" x2="9.24" y1="12.5" y2="17.24"></line> <line x1="9.24" x2="19.76" y1="17.24" y2="6.73"></line> </g> </svg> </div> <label for="checkbox-svg">SVG</label> </div> </fieldset>
The lines in the first group will have a light gray color and won’t be animated; the lines in the second group will animate in when the input is clicked.
There’s also an additional
div element; we’ll use this to create a click effect where the bright blue expands into the background. In order to make the effect work, the div needs to be a blue circle with a greater width and height than the actual checkbox, and the outside container must have
overflow: hidden; set so that the circle’s edges don’t show up. The round div should have
transform: scale(0) set, similar to the radio.
Again, format this according to your preferences. I decided to round the edges of the checkmark as well as all the corners.
The next step is to prepare for the animations. The effects are similar to the radio, except that instead of a circle expanding, the checkmark draws itself in. For this animation, we’ll need to utilize
stroke-dashoffset on the SVG lines.
In order to animate
stroke-dashoffset, we’ll need the length of each line. I created a tool on CodePen to calculate the lengths, but if you use the checkmark I already created, the shorter line’s length is
6.708 and the longer is
14.873. We’ll use this value to set both
stroke-dasharray. This is only necessary for the first checkmark (the second set of lines shows by default in the unchecked state).
When the input is clicked, we’ll set the
0, which (with a transition) will look like the line is “drawn”. We also need to add in the other state changes from the custom radio, including the background changes on hover and the circle scale on click.
The last step is to add in all the transitions. Again, we’ll set the unchecked transitions by default and set the checked transitions on click. Similarly to the circle fading for the radio, we’ll have the checkmark fade out when unchecked.
You can also get the same effect with a few div elements instead of using an SVG; the markup is simpler, but not as clearly delineated. The first empty div is the expanding blue circle, the second is the default checkmark and the third is the checkmark that animates when clicked.
<fieldset> <div class="container"> <div class="line"> <input id="checkbox-html" type="checkbox"> <div></div> <div></div> <div></div> </div> <label for="checkbox-html">HTML</label> </div> </fieldset>
:after as each part of the checkmark, which simplifies the markup, otherwise you’d need four empty elements or more to create the two checkmarks. We have to position the lines manually and rotate them into place, but you can use a single transform to both rotate and draw them in.
You can also use a checkmark symbol instead of rotating div elements! It’s not quite the same as the other two visually, but it works just as well.
<fieldset> <div class="container"> <div class="symbol"> <input id="checkbox-sym" type="checkbox"> <div></div> <div>✓</div> <div>✓</div> </div> <label for="checkbox-sym">Symbol</label> </div> </fieldset>
Note: you only need the HTML symbol in the last two div elements, but in the demo below, I’ve included it in all three divs so that I can include it in the repeated loop.
Since we can’t draw the symbol in, the white version fades in and out on click. Check out all three versions down below!
The last part of this tutorial is the regular text input. I’ve taken inspiration from Google’s Material line inputs. These inputs have a few different states: default, active/focus (when the blinking cursor is visible), and a subtle hover.
The setup is even more minimal than the previous two input styles. We have a fieldset, the input, a label, and a div element for the border.
<fieldset> <input id="example" type="text"> <label for="example">Label</label> <div></div> </fieldset>
The next step is styling the input. We won’t be hiding the input this time since we need it to show the value. We’ll also be creating a label that moves up and down when we focus on the input. In order to make this work, we’ll need to position the label exactly on top of the input. The div border will have an
:after pseudo element that draws on top of the border when the input is clicked; we’ll put a
scale(0) on the pseudo element to prepare it for animation.
If you try clicking on the demo input above and start typing, you’ll notice that the text types out on top of the label. We’ll animate the label to shrink and translate up on click. You can use entirely transforms to prevent repaint, instead of using
font-size like I did, but I found that using it as well as
translateY gave me a much more precise-looking animation. We’ll also remove the scale on the div’s
:after to make the drawing animation.
We can either add
required as an empty attribute to our input in HTML or add
:required => true to our input attributes in Haml. Then we add
:valid to the
:focus properties in our Sass/CSS. This adds an additional visual state to our input, and since it’s a simple text input, the only valid state is when there is text entered.
Note: using a different kind of input such as an email input will cause this behavior to break, as email inputs have different validity requirements.
If you want to create your own inputs using these techniques, but you need more visual inspiration, check out the UI kits available with an ThemeKeeper Elements subscription:
There are dozens of different ways to utilize the general sibling selector that are visual, functional, or both. It provides a powerful way to customize components without having to use more than just CSS and HTML. We’ve covered three different kinds of form elements in this tutorial; in the next one, we’ll explore menus and navigation. Feel free to drop a comment below if you have any questions or feedback!