In this (long) part of the customizable select series, it’s all about gamification. In this article, I’d like to highlight one of my demos, where I aimed to recreate a piece of UI found in the Monster Hunter games. To re-create this behavior, I had to think in terms of keyboard navigation first. This demo requires quite a lot of CSS, as well as some scripting, and in the end, I do want to highlight some accessibility concerns. This is an experiment on how far we can take it when styling select elements.
This is a long one… and I still wasn’t able to cover everything. So I can understand if you want to jump to some sections.
For those that just want to play around with it, here is that final result already 😉
The idea: Monster Hunter games
I am a big fan of the Monster Hunter games, with “Monster Hunter Wilds” being the last in the series. I started this demo while waiting for that last entry to be released, so visually it has more of a Monster Hunter Wold vibe (the previous version). If you are clueless about what I’m going on about, here is a video that shows the UI in the game:
I also added a little screenshot for easy reference:
Where we left off…
In the previous parts, we’ve covered basic styling, radial positioning, and sticky options. This time, we’re going horizontal and focusing on keystrokes and making it draggable. It’s a different idea, but I think the result turned out really cool. It’s quite a long demo so I won’t be going over every border or style in detail. Instead, I’d like to highlight a few parts on how this actually works.
Setting up our HTML
Let’s start with the structure. It’s a bit more complex than our previous demos because we need scroll arrows and an extra frame, more about those later on.
< select aria-label = " Monster Hunter items " > < button class = " trigger " > < selectedcontent > selectedcontent > button > < div class = " frame " > div > < div class = " items " id = " itemlist " > < button class = " arrow arrow-left " type = " button " > button > < option > < div class = " item " > < svg class = " icon " aria-hidden = " true " > < use xlink: href = " #potion " /> svg > < div class = " title " > Potion div > < div class = " amount " > 10 div > div > option > < button class = " arrow arrow-right " type = " button " > button > div > select >
The options
The options consist out of a few elements that we’re going to position differently. Each of the options will hold an icon with the image of an item, a .title containing a name and an .amount containing how many items are left of the type.
The extra elements
There are few extra divs to found here, more info on them below but for a fast read, here is a run down of why certain elements are added:
The .frame is used because we want the items to be visually dragged behind the frame, the item in the frame is the focussed item which will be selected
is used because we want the items to be visually dragged behind the frame, the item in the frame is the focussed item which will be selected The extra .items element is what we’ll need to set up our scroll-snapping
element is what we’ll need to set up our scroll-snapping The arrows are for single pointer modality
We are taking things a bit further here with the .frame element and the scroll arrows. Notice that I’m adding the type="button" on the scroll arrows as well as I don’t want the opt-in to think of this as a trigger button. The arrows are added because I wanted to make this accessible as possible by at least offering a single pointer modality. More on the accessibility part of things later on because it does raise a few questions.
Basic CSS setup and not so progressive enhancement…
For this demo I didn’t really go the full progressive enhancement route. Don’t get me wrong, it’s still a fully functional select, it’s just not styled because I thought the CSS was already getting complicated enough. I still added a feature query tho.
First of all, let’s set some variables we’ll be using throughout this demo. They are pretty much self-explanatory, just your colors, borders, and sizes.
:root { --base-icon-size : 64px ; --icon-size-wrap : 50px ; --btn-bg : rgba ( 70 70 70 / 0.9 ) ; --border-color : #282929 ; --border-width : 4px ; }
Sizes to note:
The --base-icon-size will be the size of an option
will be the size of an option The --icon-size-wrap will be the actual size of an icon.
Next up, it’s time to set our opt-in. The following part of the CSS setup will be wrapped in a feature query:
@supports ( appearance : base-select ) { select, ::picker(select) { appearance : base-select ; } }
Ok, let’s start with some of that basic select styling:
select { position : relative ; display : flex ; padding : 0 ; justify-content : center ; anchor-name : --my-select ; background : none ; border : none ; &::picker-icon { display : none ; } }
A few things are going on here. The first thing to notice is that the select is relatively positioned. The reason for this is that we’ll be placing some pseudo-elements on it later on. However, compared to my other demos, where I position the picker on top of the select, for this one, I am setting my custom anchor-name to the select element. We will be hanging a lot more items on the select, such as the .title and .amount , which is why we need that bit of extra control.
We’re also hiding the ::picker-icon again.
The borders and selectedcontent on open state
In the example UI, we see a bunch of borders. To create this kind of behavior, I wanted to add those to the open state of the select. Feel free to visit the example in detail to get the full styling of it. Another important thing that we wanted to do is that we don’t see the SVG in our when the select is :open , so we’ll make that one invisible as well.
select { &:open { selectedcontent svg { opacity : 0 ; } &::before, &::after { } } }
Creating the frame
This was the trickiest part to figure out. I wanted items to scroll under a frame, like they’re behind glass. The solution was to create two identical-looking elements, one of those will be our button holding the content, which I gave the .trigger class for convenience, the other one is the dedicated .frame element:
.trigger, .frame { inline-size : var ( --base-icon-size ) ; aspect-ratio : 1 ; border : var ( --border-width ) solid var ( --border-color ) ; border-radius : 5px ; &::before, &::after { position : absolute ; inset-block-start : 40% ; inset-inline : -17px auto ; inline-size : 30px ; aspect-ratio : 1 ; background : var ( --border-color ) ; clip-path : polygon ( 50% 0% , 80% 50% , 50% 100% , 20% 50% ) ; content : "" ; } &::after { inset-inline : auto -17px ; } }
But, there is more, we’re going full anchoring in this demo by setting an anchor-name for the trigger button ( --button ) and for the frame ( --frame ). While also providing a bit of positioning. We also want a visible overflow on the trigger because we’re going to position some of the text in our outside of our