Just about every web form you might come across uses the <select>
element in one way or another. It’s used as an easy way to allow a user to select an item from a list of options. There are impactful benefits to using the <select>
element like accessibility and browser native functionality, but when it comes to style options, are you giving anything up?
More and more, I’ve been getting requests to style our select
s and their children <option>
s in ways that we really can’t with just HTML and CSS. But fear not, I’ve found a solution that allows us to maintain accessibility and native functionality, while giving us the ability to provide a bit more visual interest to the page.
The ask
A common use-case for <select>
is a country selector which might be used for phone numbers or addresses. Let’s say that our design team has requested that–instead of just displaying the name of the country selected in our form–we show the flag of the selected country.
The short answer
Currently, adding an image to the HTML of each <option>
contained in our <select>
is not supported. Further, we can’t add the flags through our CSS as a <background-image>
on each <option>
either. There is no obvious path forward here.
The long answer
However; with a little CSS magic, we can find a way to add some images while maintaining accessibility.
- Get a functioning and accessible
<label>
and<select>
on the page - Add a
<div>
with our flag SVGs to the page - Position that flag div over the top of our select list
- Add a touch of JS to tie it all together
Step 1—The basics
I’m adding a <label>
and <select>
to the page with just two country options for this example.
You’ll notice my <label>
is outside of a div containing my <select>
. This is so we can later size our flags correctly with our <select>
. Don’t worry about it being bumped up a level; the for
attribute allows us to keep the label tied to our <select>
to maintain accessibility.
<label for="country-select">Country</label>
<div class="wrapper">
<select class="wrapper__select” name="country-select" id="country-select">
<option value="US">United States</option>
<option value="JP">Japan</option>
</select>
</div>
Step 2—Adding our flags
Next, we’ll add our flags to the page. I’ve used two SVGs–one for each flag. The first SVG is the American flag as evidenced by the data attribute data-country-code="US"
. Similarly you’ll see the second SVG has a data attribute of JP
for Japan.
<div class="wrapper__flags">
<svg class="wrapper__flags__flag" data-country-code="US" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1235 650" fill="#FFF">
<path d="M0 0h1235v650H0"/>
<path stroke="#B22234" stroke-width="2470" stroke-dasharray="50" d="M0 0v651"/>
<path fill="#3C3B6E" d="M0 0h494v350H0"/>
<g id="e">
<g id="d">
<g id="f">
<g id="c">
<g id="b">
<path d="M30.1 50.6l12-36 12 36-30.8-22h37.8" id="a"/>
<use xlink:href="#a" x="82"/>
</g>
<use xlink:href="#b" x="164"/>
<use xlink:href="#a" x="328"/>
</g>
<use xlink:href="#a" x="410"/>
</g>
<use xlink:href="#c" x="41" y="35"/>
</g>
<use xlink:href="#d" y="70"/>
</g>
<use xlink:href="#e" y="140"/>
<use xlink:href="#f" y="280"/>
</svg>
<svg class="wrapper__flags__flag" data-country-code="JP” xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
<path fill="#fff" d="M0 0h900v600H0z"/>
<circle fill="#bc002d" cx="450" cy="300" r="180"/>
</svg>
</div>
From here on out, I’ll be keeping this <div>
collapsed. There isn’t any relevant code in our SVGs and they are kind of a pain to look at when they’re expanded. Keep in mind that, in a more fully featured version of this solution, you’d likely be dynamically loading these flags and you wouldn’t have to see them in your markup at all.
Since this example doesn’t have a quick and easy way to dynamically load each SVG when we need it, I’ve added a bit of CSS to mimic that. Notice that we hide all flags on the page and only show the flag that has the class of .is-visible
appended:
.wrapper__flags__flag {
height: 100%;
border: 1px solid #ddd;
box-sizing: border-box;
display: none;
}
.wrapper__flags__flag.is-visible {
display: block;
}
There are a few other stylistic additions here, mainly sizing and borders for our flags to make sure they all look uniform.
Step 3—Positioning our flags
With all the elements we’ll need on our page, it’s time to position everything correctly. But first, let’s take a look at what we have so far:
<label for="country-select">Country</label>
<div class="wrapper">
<select class="wrapper__select" name="country-select" id="country-select">
<option value="US">United States</option>
<option value="JP">Japan</option>
</select>
<div class="wrapper__flags">
<svg class="wrapper__flags__flag" data-country-code="US" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 0 1235 650" fill="#FFF">
...
</svg>
<svg class="wrapper__flags__flag" data-country-code="JP” xmlns="http://www.w3.org/2000/svg" viewBox="0 0 900 600">
...
</svg>
</div>
</div>
With our HTML built out, here’s what we have so far:
Now let’s add some CSS to position everything.
.wrapper
styles
- We want the sizing of this parent
<div>
to define the size of both of its children (the.wrapper__select
and.wrapper__flags
), so I’ve set it to35px
. - Set
position: relative;
. This will let us work with our flags in a few lines.
.wrapper__select
styles
- Set
width: 100%;
to stretch the<select>
to the size of its parent (35px
). - Visually hide the
<select>
from the page usingopacity: 0;
.
.wrapper__flags
styles
- Set
width: 100%;
andheight: 100%;
to size the flags to the parent. - Set
position: absolute;
as well astop
andleft
properties to align the flags to the upper left corner of the.wrapper
div. - Set
pointer-events: none;
to tell the browser to ignore all pointer events on this element.
All our CSS
.wrapper {
width: 35px;
position: relative;
}
.wrapper__select {
width: 100%;
opacity: 0;
}
.wrapper__flags {
width: 100%;
height: 100%;
position: absolute;
top: 0;
left: 0;
pointer-events: none;
}
With CSS styles added, here’s what the codepen looks like:
The real trick here is that the pointer-events: none;
. This property tells the browser to ignore all pointer events. In our use-case, this means when the user tries to click on the flags, they’ll actually be clicking on the <select>
element hidden beneath them and open the <options>
for that <select>
. Now we have a faux-flag button!
Step 4—Swapping our flags (with a touch of JS)
The last step to make our flags dynamic requires a bit of Javascript. Our very own Sarah Canieso has got it down to six lines.
We’re setting up an event listener to check what value our <select>
has whenever it is changed. Then we’re using the currently selected value of our <select>
and making sure the corresponding SVG is the only one with our class of .is-visible
appended.
let countrySelect = document.getElementById('country-select');
let updateSelected = function(e) {
document.querySelector('.is-visible').classList.remove('is-visible');
document.querySelector('[data-country-code="' + e.target.value + '"]').classList.add('is-visible');
};
countrySelect.addEventListener('change', updateSelected);
After adding JS, our codepen is looking pretty good:
All together now
So, let’s recap. We’ve added a <label>
and <select>
to the page, added sibling flags <div>
, positioned our sibling flags <div>
to sit on top of our <select>
, and added a bit of JS to the page to swap out our country flags on select. All of these techniques create something bigger than the sum of its parts. We’ve found a way to accessibly and semantically show a user the country flag of their selected country all while using browser native inputs for data. This is big!
Where do we go from here?
Of course, this is just a proof of concept. There are many possible ways to expand and improve on this approach but in this post, I wanted to lay out the foundation in the lightest way possible.
In a real world use-case, you’d want to add some styling to get our faux-flag button to look like a clickable input. Further, a user still may not understand what the styled faux-flag button does–perhaps you’d need to add the country name next to the flag.
There are plenty of directions to take it, but as long as you stick with an accessible and semantic approach, you’ll have a great starting point to continue iterating.
DockYard is a digital product agency offering custom software, mobile, and web application development consulting. We provide exceptional professional services in strategy, user experience, design, and full stack engineering using Ember.js, React.js, Ruby, and Elixir. With a nationwide staff, we’ve got consultants in key markets across the United States, including San Francisco, Los Angeles, Denver, Chicago, Austin, New York, and Boston.