Custom styled form elements are a common thing to see in a design. That’s because default form styles vary visually from browser to browser and OS to OS. It makes sense that we’d want these elements styled consistently. Styling them is pretty straightforward, with the exception of select
dropdowns which can be more complex. Recently, I ran into an unexpected problem when working on a site that needed a branded admin experience.
Styling Radio and Checkbox Buttons
There’s a simple method of styling radio buttons and checkboxes I’ve been using for a while. I first saw it from the people at Tuts+, and they provided this Pen demoing the technique. Briefly explained, we visually hide the input
for our radios/checkboxes and draw a new one using :before
and :after
pseudo elements on the label
element. CSS’ :checked
selector allows us to toggle our styles based on if the input
is checked or not. This technique relies on appropriately marked up input
s and label
s, for example:
<div class=”form-element”> <input type=”checkbox” id=”click-me”> <label for=”click-me”>Click Me</label> </div>
Clicking the label (containing the fake checkbox styling) will toggle the state of the real checkbox that’s visually hidden.
Drupal’s Admin Interface
One thing I learned while working with some of Drupal’s admin interfaces is that they only supply the input
, and not an accompanying label
. This seemed especially true in tabled interfaces, where you’d check off rows of content and perform some action on the selected items. Since we’re hiding an input
that doesn’t have a label
to attach the visuals to, we just end up with a blank space. There were several options we had for how to address this issue.
1. Drop the Custom Styles
The simplest is to just rely on browser defaults for checkboxes and radios. It’s not a great option, but it is an affordable one for tight budgets.
2. Create the Missing Labels
This ended up being my first approach to fixing this, and became more akin to a game of Whack-a-Mole than I anticipated. After going through various preprocess functions, alters, and render functions I was still encountering inputs that were missing labels. Some I was never able to fully track down where the markup was coming from. Manually finding and fixing every missing label might be a viable solution if your website or application has only a handful of places you need to update. However this is not the most scalable solution, and if your product grows this can quickly become a financial black hole.
3. Create the Missing Labels… with Javascript
Instead of trying to find every place that creates a checkbox or radio on the server side, we could use Javascript to target every checkbox or radio input that is not followed by a label. From there, we just create the label element and insert it after the selected inputs. This is how that might look using jQuery, though it can also be done with Vanilla JS.
This is great, as it solves the problem for every input in one fell swoop. One downside here is the Javascript dependency. Should your Javascript not run for any reason, you’re still left with the original problem of missing inputs. Another is page rendering. User’s might be left with a janky experience as Javascript inserts these elements into the DOM.
4. Drop the Custom Styles… for Older Browsers
In the end, this was the solution that won out. Using CSS Feature Queries and CSS’ appearance
property, we’re able to provide styled inputs for most modern browsers and then fall back to default styles in browsers that lack the support we need. This gives us our custom styles, without the scaling problem of #2, and the Javascript dependency of #3. The downside to this solution is that all versions of Internet Explorer and Firefox will use their browser defaults.
Firefox was a surprise to me, as the documentation says it supports appearance
. However in practice what I got was a less appealing version of the browser default styles. Also surprisingly was by checking for only -webkit-appearance
support, Edge still gets our custom styles applied. This all sat well with me for a working solution. Every team and project has it’s own constraints, so your mileage may vary.