2024-11-22 web, development, javascript

Styling a react-select Combobox to Match shadcn/ui

By O. Wolfson

If you're using the shadcn/ui component system in your Next.js 13 application and want to integrate a multi-select dropdown combobox with similar styles, the react-select library can be a good choice. In this tutorial, I'll walk through the process of styling a react-select combobox component to match the styles of shadcn/ui, including support for dark and light modes.

Here is a deployed example: https://shadcn-tester.vercel.app/multi-select

Here is the source code: https://github.com/owolfdev/shadcn-tester

Prerequisites

To follow along with this tutorial, make sure you have the following installed:

  • Next.js
  • react-select package
  • shadcn/ui component system (or a similar component system)
  • next-themes package

Note that we are comparing the shadcn/ui input component to the react-select combobox component, so we'll need to import the the shadcn/ui input component as well as the react-select combobox component. We've set up a component called InputDemo that uses the shadcn input component.

Note we are styling a dark mode and light mode version of the component, so we'll need to use the next-themes package to access the current theme. If you are using shadcn/ui template, as I am, next-themes is already installed.

You can install the template as follows:

bash
npx create-next-app -e https://github.com/shadcn/next-template

I am not giving comprehensive installation instructions here, so it's highly recommend to read the installation documentation for shadcn/ui:

https://ui.shadcn.com/docs/installation

Here is the the page component that we'll be working with:

javascript
"use client"
//this component needs to be a client component as we are using react hooks.

import { useEffect, useState } from "react"

//import the next-themes hook
import { useTheme } from "next-themes"

//import the react-select combobox component
import Select from "react-select"

//import the shadcn/ui input component
import { InputDemo } from "@/components/scn-input-demo"

//set up dummy categories for the multi-select dropdown. This is just for demo purposes. Otherwise the data would be coming from an API.
let categories = [
  "All",
  "Design",
  "Development",
  "Marketing",
  "Productivity",
  "Sales",
  "Support",
  "Writing",
]

//create a custom styles interface as we are using typescript
interface CustomStyles {
  option: (defaultStyles: any, state: any) => any
  placeholder: (provided: any, state: any) => any
  multiValue: (provided: any, state: any) => any
  control: (defaultStyles: any, state: any) => any
}

export default function IndexPage() {
  //set up a state variable to hold the selected categories
  const [selectedCategories, setSelectedCategories] = useState<string[]>([])

  //set up the next-themes useTheme hook to access the current theme
  const { setTheme, theme } = useTheme()

  //set up the initial custom styles. This is necessary so that the component will look consistent upon initial render.
  const initialCustomStyles: CustomStyles = {
    option: (defaultStyles, state) => ({
      ...defaultStyles,
    }),
    placeholder: (provided, state) => ({
      ...provided,
      color: "#6B7280",
      fontSize: "14px",
    }),
    multiValue: (provided, state) => ({
      ...provided,
      backgroundColor: "#e2e8f0",
      borderRadius: "0.35rem",
      color: "#6B7280",
      fontSize: "14px",
    }),
    control: (defaultStyles, state) => ({
      ...defaultStyles,
      borderRadius: "0.35rem",
      backgroundColor: "transparent",
      borderColor: "gray-300",
    }),
  }

  //set up the custom styles state variable
  const [customStyles, setCustomStyles] =
    useState<CustomStyles>(initialCustomStyles)

  //set up the custom styles for dark mode and light mode. This is necessary so that the component will style properly when the theme changes. Note how the style setting change based on the theme state being 'dark' or 'light'.
  useEffect(() => {
    console.log("theme", theme)
    setCustomStyles({
      option: (defaultStyles: any, { isFocused }) => ({
        ...defaultStyles,
        backgroundColor: isFocused
          ? theme === "dark"
            ? "#e2e8f0"
            : "#e2e8f0"
          : "transparent",
        color: isFocused
          ? theme === "dark"
            ? "black"
            : "#6B728"
          : theme === "dark"
          ? "black"
          : "#6B728",
        ":active": {
          ...defaultStyles[":active"],
          backgroundColor: isFocused
            ? theme === "dark"
              ? "#e2e8f0"
              : "#e2e8f0"
            : "transparent",
          color: isFocused
            ? theme === "dark"
              ? "black"
              : "#6B728"
            : theme === "dark"
            ? "black"
            : "#6B728",
        },
      }),
      placeholder: (provided: any, state: any) => ({
        // Styles for the placeholder text
        ...provided,
        color: "#6B7280",
        fontSize: "14px",
      }),

      multiValue: (provided: any, state: any) => ({
        // Styles for the placeholder text
        ...provided,
        backgroundColor: "#e2e8f0",
        borderRadius: "0.35rem",
        color: "#6B7280",
        fontSize: "14px",
      }),

      control: (defaultStyles: any, state: any) => ({
        ...defaultStyles,
        borderRadius: "0.35rem",
        backgroundColor: "transparent",
        borderColor: "gray-300",
        boxShadow: state.isFocused
          ? theme === "dark"
            ? "0 0 0 2px black, 0 0 0 4px rgba(30, 41, 59, 1)"
            : "0 0 0 2px white, 0 0 0 4px rgba(113, 128, 150, 0.85)"
          : "none",
        "&:hover": {
          borderColor: "gray-300",
        },
      }),
    })
  }, [theme])

  return (
    <section className="container grid items-center gap-6 pt-6 pb-8 md:py-10">
      <div>
        <InputDemo />
      </div>
      <div>
        <div>
          <Select
            // pass the custom styles to the component
            styles={customStyles}
            className=" border-red-50"
            value={selectedCategories.map((category) => ({
              value: category,
              label: category,
            }))}
            onChange={(selectedOptions) => {
              const selectedValues = selectedOptions.map(
                (option) => option.value
              )
              setSelectedCategories(selectedValues)
            }}
            options={categories.map((category) => ({
              value: category,
              label: category,
            }))}
            isMulti
          />
        </div>
      </div>
    </section>
  )
}

The useEffect hook is used to update the custom styles of the React Select component based on the current theme.

The theme variable is obtained from the useTheme hook provided by the next-themes library. It represents the current theme mode, which can be either "dark" or "light".

Inside the useEffect hook, a new set of custom styles is created using the setCustomStyles function. These custom styles are defined as an object with properties corresponding to different elements of the React Select component.

Let's go through each property and understand how it works with dark mode:

  1. option: This property defines the styles for each option in the select dropdown. The isFocused argument represents whether the option is currently focused. Based on the isFocused value and the current theme, the background color and text color of the option are set. In dark mode, the background color is set to #e2e8f0 and the text color to black when focused.

  2. placeholder: This property defines the styles for the placeholder text in the select input. The provided styles include the color (#6B7280) and font size (14px). These styles are applied regardless of the theme mode.

  3. multiValue: This property defines the styles for the selected values when multiple options are selected. The provided styles include the background color (#e2e8f0), border radius (0.35rem), text color (#6B7280), and font size (14px). These styles are applied regardless of the theme mode.

  4. control: This property defines the styles for the control element of the select input, including the input field and the dropdown indicator. The styles are defined using the defaultStyles argument, which represents the default styles provided by the React Select library. Additionally, the state.isFocused property is used to determine whether the control element is currently focused. Based on the focus state and the current theme, the styles for the control element are modified. In dark mode, a black box shadow is added when focused.

By including the theme variable in the dependency array [theme] of the useEffect hook, the custom styles will be updated whenever the theme changes. This ensures that the React Select component adapts its styles based on the selected theme mode.

That's how the provided code section works with dark mode to style the React Select component. By modifying the custom styles based on the theme, you can create a visually consistent and appealing select input in your React application.