selectInput--CSS customization

shiny

#1

Hi everyone, do you know is there a way to configure selectInput in a more meta way using CSS? For example, I want the background of the first option to be green; is there a way to do it?


#2

Are you wanting to apply styles to only the first element or the entire selectInput?

If you want to style the entire element, using tags$select or writing it as HTML("<select>...</select>") might be a better option than selectInput. The css property/value -webkit-appearance: none; will allow you to add custom styling. However, the appearance property is still experimental and may behave differently across browsers/platforms (see Mozilla - appearance reference page).

Do you have an example? I'd be happy to take a look.


#3

Sure! In this app, can I highlight NY and WA, i.e. the first option of each list

shinyApp(
  ui = fluidPage(
    selectInput("state", "Choose a state:",
      list(`East Coast` = c("NY", "NJ", "CT"),
           `West Coast` = c("WA", "OR", "CA"),
           `Midwest` = c("MN", "WI", "IA"))
    ),
    textOutput("result")
  ),
  server = function(input, output) {
    output$result <- renderText({
      paste("You chose", input$state)
    })
  }
)
}```

#4

This was a fun one! I went ahead and applied custom styling for <select>. Here's what it looks like.

The first thing I did was create a new r project and created a separate css file. I originally had it all in one file, but it was starting to become difficult to manage. At the end of this post, you can find a link to the project on github.

ui.R

The two things that I'd like to point out are the label and select elements. In the previous post, I mentioned that using tags$select might be a better option for adding custom styling. The structure to create the input using example provided ended up looking like this.

# label
tags$label("for"="state", "Select a State", class="input-label"),

# input
tags$select(id="state","onfocus"='this.size=13;', "onblur"='this.size=1;' ,
            "onchange"='this.size=1; this.blur();',
            
            # default
            tags$option(value = "none", ""),
            
            # east coast
            tags$optgroup("label" = "East Coast",
                          tags$option(value = "NY", "NY"),
                          tags$option(value = "NJ", "NJ"),
                          tags$option(value = "CT", "CT")
            ),
            
            # west coast
            tags$optgroup("label" = "West Coast",
                          tags$option(value = "WA","WA"),
                          tags$option(value = "OR","OR"),
                          tags$option(value = "CA","CA")
            ),
            
            # midwest
            tags$optgroup("label" = "Midwest",
                          tags$option(value = "MN","MN"),
                          tags$option(value = "WI","WI"),
                          tags$option(value = "IA","IA")
            )
)

In tags$select, the attributes onfocus, onblur, and onchange are event listeners and allow you to define the behaviors when the input is active, inactive, and closed. For example, If you used "onfocus"='this.size=6;', you will only see the first 6 elements (-ish depending on other styling - padding, margins, etc.). onblur sets the number of elements to display after is inactive (no longer focused; e.g., if the menu is opened and the user clicks another part of the page) and onchange sets the number of elements to display once an option is selected.

CSS

I won't go through all of the css properties as I'd like to highlight the parts the address your questions. I've added comments in the css file that explain what each block is doing.

Styling <select>

In order to add styling to the input, I set the -webkit-appearance to none (webkit and moz are vender prefixes for chrome and mozilla's FireFox). I set the width of the input to 90% of the parent element (sidebarPanel). This will allow the input to be resized accordingly when the browser dimensions are changed (resized).

Since we are adding new styles, I wrote a menu button and set it as the background. background-position sets the position of the icon (x = 95%; y = 20px).

/* primary styling for <select> + manually add menu button */
#state{
    -webkit-appearance:none;
    -moz-appearance:none;
    width: 90%;
    padding: 15px;
    font-size: 14pt;
    border-radius: 0;
    outline: none;
    border: 2px solid #3A506B;
    color: #3A506B;
    background: url(menu-chevron-down.svg) no-repeat;
    background-position: 95% 20px;
    background-color: white;
}

For the first <option> in each <optgroup>, set the background to green

For setting the background color, I used descendent selector paths to select the first element in each group. This says within the element with the id of state select the option groups, and then select the first child in each group. (If you wanted the second item in each group, it would be :nth-child(2)).

#state optgroup option:nth-child(1){
    background-color: #31E981 !important;
    color: white;
}

I was a bit confused as to if you wanted the first options in all of the groups or the first options in the first two groups. If the later, use the following recipe:

#state optgroup:nth-child(-n+3) option:nth-child(1){...}

For other options, see the css tricks :nth-child recipes doc.

Hope that helps!

As promised, here's the link to the github repo: shiny select input styling demo.


#5

Thank you! That certainly helps!


#6

Hi, I might be demanding too much but is there a way to make this apply to a general list? For example, is it possible if I supply iris as choices and do the same thing, using a generic approach?


#7

Yes, it is possible. Using the iris dataset, the first step is to transform the unique species into HTML by wrapping each choice in the tag <option> (add other attributes as needed).

# unique species
choices <- c(
    "<option value='none'></option>",                 # add default opt
    sapply(sort(unique(iris$Species)), function(x){   # turn choices into html
        paste0("<option value='",x,"'>", x, "<option>")
    })
)

# print
choices
#> [1] "<option value='none'></option>"               
#> [2] "<option value='setosa'>setosa<option>"        
#> [3] "<option value='versicolor'>versicolor<option>"
#> [4] "<option value='virginica'>virginica<option>"

In order to integrate the choices into tags$select, use HTML(choices). (the following works in the ui and server; if server, use renderUI and htmlOutput)

# input use as is in the ui or in the server (make sure id is linked in the css)
tags$select(id="someId","onfocus"='this.size=13;', "onblur"='this.size=1;' , "onchange"='this.size=1; this.blur();',
    HTML(choices)
)