2024-09-09 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:
bashnpx 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:
-
option
: This property defines the styles for each option in the select dropdown. TheisFocused
argument represents whether the option is currently focused. Based on theisFocused
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 toblack
when focused. -
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. -
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. -
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 thedefaultStyles
argument, which represents the default styles provided by the React Select library. Additionally, thestate.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.