Select inputs, aka dropdowns or pickers, show up on nearly every web page or app, but their default look is pretty… basic. Or to put it differently: it’s sometimes even downright ugly. That’s because browsers handle most of their styling, using the system’s native styles, which makes customization tricky. Your normal CSS styling becomes pretty much useless. The usual fix is turning to JavaScript libraries like selectize.js, which Shiny also uses by default. It makes select inputs look nice again! But wouldn’t it be nice if you can also style select inputs entirely with CSS? With Chrome 135 (April 2025), you finally can. A feature that has been long awaited! Styling icons, checkmarks, dropdown lists: all possible. You just need to know which properties you can style. In this article, I’ll walk you through how to switch off selectize.js in Shiny and start taking advantage of this new customizable select behaviour.
The basics
First things first: the basics. The HTML markup of the select input is simple:
An optional label can be added above the select element. For example:
Which results in:
The looks of this basic markup (without any Bootstrap or anything like that) look quite horrible. Not cool, because we want things to look pretty 💅. Thing is, this basic select element is difficult to style. Browsers generally defer to the operating system’s native styles for <select> elements. That’s why they’re hard to customize. But that doesn’t mean we can’t improve things! Let’s first walk through two improvement rounds: using Bootstrap and adding JavaScript.
First improvement: Bootstrap
<input>, <select> and <textarea> a consistent look and feel across browsers, and does simple stuff like adding padding, border styles and font sizing. To see the effect of the Bootstrap class only, we turn off the JavaScript styling for now with selectize = FALSE(more on that later):
selectInput(
inputId = "old_select2",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = FALSE # use base select input element instead of selectize.js
)
tagQuery from htmltools:
tagQuery(
selectInput(
inputId = "old_select",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = FALSE
)
)$find("select")$removeClass("form-control")$allTags()
The Bootstrap CSS is better than the plain HTML, but Bootstrap doesn’t really style the dropdown list itself. It only styles the outer shell of the dropdown with padding, borders, background, and font. That box that opens when you click on it, the actual dropdown, is still handled by the browser.
Second improvement: JavaScript
selectInput() (it sets selectize = TRUE), which results in:
selectInput(
inputId = "old_select3",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = TRUE
)
<select>, you can set selectize = FALSE. As you’ve seen in the previous example, it will return a much simpler picker than the selectize one, but it still has some “looks” due to the earlier mentioned Bootstrap class. Selectize.js provides a nice looking dropdown list, and also makes sure there is an arrow button that changes direction once the dropdown list is open. Another improvement! The new customizable select in Chrome
Why does all of this matter? Well, for years web developers wanted to better style the select input with CSS instead of relying on JS. Another dependency, bit more hassle, and other inputs don’t have the hard-to-style problem. The select input was lagging due to its quirks. But that’s about to change! With Chrome 135, released in April 2025, you can welcome the customizable select input 👋🏻.
And you can do lots with this new behaviour: change the picker and checkmark icons, appearance of the selected option, styling of the options box, and more. All of that with CSS. So how does it work in Shiny?
In Shiny, you need to turn off selectize.js and actively opt in on the new customizable select element with appearance:base-select. From there, you can style the element with CSS. For example, you can target:
::picker-icon: the little icon indicating the select (arrow-down). Or you can use::not(:open): arrow-down when closed:open: arrow-down when open
::checkmark: the check icon that is next to the checked option:checked: A pseudo-class to select our checked option::picker(select): The listbox that surrounds/lists options
The opt in happens in your CSS and is pretty simple:
.custom {
appearance: none;
@supports (appearance: base-select) {
&,
&::picker(select) {
appearance: base-select;
}
}
This CSS styles a <select> element with the class custom , which can easily be changed to just select for all select elements, or a particular ID. It first removes the browser’s default styling using appearance: none, which strisp away some of the OS-level styling. Then, inside an @supports block, it checks if the browser supports the new appearance: base-select feature (which only works in newer Chrome versions like 135+). If supported, it applies appearance: base-select both to the select itself (&) and to its popup dropdown (&::picker(select)), enabling the new customizable select features.
From there on, you can start adding stuff. And just like Bootstrap does you can start with styling the outer shell, which works on all browsers. Note that the dropdown list looks already different due to appearance: base select on &::picker(select):
.custom {
appearance: none; /* remove default browser styling */
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
@supports (appearance: base-select) {
appearance: base-select;
&::picker(select) {
appearance: base-select;
}
}
}
& syntax. This makes it easier to read later on.
The real power of the customizable select is in all those new pseudo-elements we can target, like ::picker-icon :
.custom {
appearance: none; /* remove default browser styling */
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
@supports (appearance: base-select) {
appearance: base-select;
&::picker(select) {
appearance: base-select;
}
/* default/closed state*/
&::picker-icon {
color: #555;
margin-right: 1rem;
transition: transform 0.2s ease;
}
/* open state */
&:open::picker-icon {
transform: rotate(180deg);
color: #007acc;
}
}
}
Eventually, you can do something like this:
.custom {
appearance: none; /* remove default browser styling */
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
/* move text a bit to the right */
padding-left: 1rem;
@supports (appearance: base-select) {
appearance: base-select;
/* Purple glow to show clearly when customizable select is supported */
/* this is more for demonstration purposes */
box-shadow: 0 3px 9px rgba(147, 112, 219, 0.6);
/* list box styling */
&::picker(select) {
appearance: base-select;
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
}
/* styling the dropdown icon */
&::picker-icon {
color: #555;
margin-right: 1rem;
transition: transform 0.2s ease;
}
&:open::picker-icon {
transform: rotate(180deg);
color: #4b0033;
}
/* styling the options */
option {
padding: 0.75rem 1rem;
border-radius: 1rem;
margin: 0.25rem;
font-size: 1rem;
background-color: #f7f7f7;
color: #333;
display: flex;
align-items: center;
gap: 0.5rem;
transition: background-color 0.2s;
&:where(:hover) {
background: linear-gradient(135deg, plum, thistle);
color: #4b0033; /* deep plum tone for text */
transform: scale(1.05);
}
&:checked {
background: linear-gradient(135deg, plum, thistle);
color: #4b0033;
font-weight: bold;
}
/* styling checkmark icon (or not displaying any) */
&::checkmark {
font-weight: 900;
/* for not displaying any icon: */
/* display: none; */
}
}
}
}
And achieve the following result:
selectInput() that brings options, and also the bootstrap-select jQuery plugin that is used by shinyWidgets in pickerInput() gives you some room for creativity. But if you’re looking for really specific styles, this customizable select input might be just what you need!
The applied styles here are pretty simple, but you think about other cool stuff like animating the opening of the dropdown list, or adding images/icons. Anyway, if you want to see all examples side by side you can run this small Shiny app:
library(shiny)
library(bslib)
library(htmltools)
ui <- page_fillable(
title = "Pick your pet",
# add CSS to the page
tags$head(
tags$style(HTML("
.custom {
appearance: none; /* remove default browser styling */
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
/* move text a bit to the right */
padding-left: 1rem;
@supports (appearance: base-select) {
appearance: base-select;
/* Purple glow to show clearly when customizable select is supported */
/* this is more for demonstration purposes */
box-shadow: 0 3px 9px rgba(147, 112, 219, 0.6);
/* list box styling */
&::picker(select) {
appearance: base-select;
padding: 0.5rem;
margin-top: 0.5rem;
border-radius: 1rem;
border: none;
box-shadow: 0px 12.8px 28.8px rgba(0, 0, 0, 0.13), 0px 0px 9.2px rgba(0, 0, 0, 0.11);
}
/* styling the dropdown icon */
&::picker-icon {
color: #555;
margin-right: 1rem;
transition: transform 0.2s ease;
}
&:open::picker-icon {
transform: rotate(180deg);
color: #4b0033;
}
/* styling the options */
option {
padding: 0.75rem 1rem;
border-radius: 1rem;
margin: 0.25rem;
font-size: 1rem;
background-color: #f7f7f7;
color: #333;
display: flex;
align-items: center;
gap: 0.5rem;
transition: background-color 0.2s;
&:where(:hover) {
background: linear-gradient(135deg, plum, thistle);
color: #4b0033; /* deep plum tone for text */
transform: scale(1.05);
}
&:checked {
background: linear-gradient(135deg, plum, thistle);
color: #4b0033;
font-weight: bold;
}
/* styling checkmark icon (or not displaying any) */
&::checkmark {
font-weight: 900;
/* for not displaying any icon: */
/* display: none; */
}
}
}
}
"))
),
h2("Without selectize and without Bootstrap"),
tagQuery(
selectInput(
inputId = "old_select",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = FALSE
)
)$find("select")$removeClass("form-control")$allTags(),
h2("Without selectize and with Bootstrap"),
selectInput(
inputId = "old_select2",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = FALSE # use base select input element instead of selectize.js
),
h2("With selectize and with Bootstrap"),
selectInput(
inputId = "old_select3",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = TRUE
),
h2("An alternative way (Chrome 135 and up)"),
tagQuery(
selectInput(
inputId = "new_select",
label = "Choose your pet:",
choices = c("Dog", "Cat", "Fish", "Bird"),
selectize = FALSE
)
)$find("select")$removeClass("form-control")$addClass("custom")$allTags()
)
server <- function(input, output) {
}
shinyApp(ui = ui, server = server)
What about other browsers?
Chrome is a widely used browser, but it would be a shame if your application can only be displayed properly on one browser alone. So what about other browsers? The base-select property is still experimental, and it’s not widely supported yet. Safari and Firefox might be working on it, but it’s not there yet. However, Edge (based on Chromium, so it will typically follow Chrome’s lead) does already support it. It’s always nice to check out Can I Use to get a sense of how far along adoption of a web feature is.
At this point, it’s best to treat this feature as progressive enhancement. That means as much as: use it where it’s supported, but make sure your <select> elements still work and look good in browsers that don’t support it.
Inspiration and resources
I always like to browse through CodePen for inspiration. Here are some of my inspo-sources for the customizable select:
- A very cool horizontal select: https://codepen.io/utilitybend/pen/MWdaMxm
- Rounded select with images, that shows only the image for the selected option, and no text (cryptic description, I know, but just check it out): https://codepen.io/utilitybend/pen/WbNZBWQ
- A nice hover effect: https://codepen.io/una/details/dyBWYxR
- An example with custom checkmarks: https://codepen.io/sfrisk/pen/NPWELrw
- Multiple examples with icons and images: https://codepen.io/web-dot-dev/pen/gbOKyRZ
Want to know more about the background story of the customizable select? You can check out these resources:
Wrap up
I’m curious to see what you create! At least with this new feature, you can get creative pretty easily. Did you like this article and would you like to see more? If you want to support me, you can:
☕️ Buy me a coffee!
📚 Take a look at my online Shiny courses: ProductioniZing Shiny , CustomiZing WidgetZ and Mobile StructureZ.
🤓 Let me help you with your R and Shiny questions. Do you need help with your project? Let’s have a chat!
