AdminKitDocs
Forms

Next.js React Select

Next.js React Select in AdminKit PRO: a searchable, Bootstrap-styled select with groups and multiple values that renders cleanly on the server.

Use Select when a plain dropdown isn't enough: people can search as they type, pick several values and browse grouped options. It is built on React Select 5 and styled to match the other Bootstrap form fields. Select is part of the PRO version of AdminKit for Next.js.

Select is a client component: its file starts with "use client", because the search and the menu run in the browser. Your page can still render it, as the next sections show.

For a short, fixed list, the native <select className="form-select"> is the simpler choice. It is plain markup, so it works in a server component, and it is in the free and PRO versions.

Example

Start with the native select. Set the preselected option with defaultValue on the <select>:

<label className="form-label" htmlFor="nativeSelect">Status</label>
<select className="form-select" id="nativeSelect" defaultValue="active">
  <option value="active">Active</option>
  <option value="paused">Paused</option>
  <option value="archived">Archived</option>
</select>

form-select-lg, form-select-sm, multiple and disabled work as in Bootstrap.

React Select

Pro

This component is only available in AdminKit PRO. Learn more.

Your page can stay a server component and render Select directly, as long as every prop is plain data. The Advanced Inputs page of the demo works this way. options takes a list of { value, label } objects, or groups of them, typed as SelectOption and SelectGroup.

src/app/(dashboard)/locations/page.tsx
import { Select, type SelectGroup } from "@/components/plugins/select";
import { pageMetadata } from "@/lib/site";

export const metadata = pageMetadata("/locations");

const states: SelectGroup[] = [
  {
    label: "Mountain Time Zone",
    options: [
      { value: "AZ", label: "Arizona" },
      { value: "CO", label: "Colorado" },
      { value: "UT", label: "Utah" },
    ],
  },
  {
    label: "Pacific Time Zone",
    options: [
      { value: "CA", label: "California" },
      { value: "OR", label: "Oregon" },
      { value: "WA", label: "Washington" },
    ],
  },
];

export default function LocationsPage() {
  return (
    <>
      <h1 className="h3 mb-3">Locations</h1>
      <label className="form-label" id="state-label">State</label>
      <Select
        options={states}
        defaultValue={states[0]?.options[0]}
        aria-labelledby="state-label"
      />
    </>
  );
}

List /locations in src/data/pages.json, where pageMetadata() reads the page's title and description. The preview shows the select of the HTML version. _react-select.scss gives React Select the same look.

Server render and hydration

Next.js renders the page's HTML on the server, and the browser then takes over that HTML. This takeover is called hydration, and it only works when the browser's first render matches the HTML. React Select adds its layout styles in the browser while it runs, so the server HTML could never match it. That is why Select renders in two steps:

  1. On the server and during hydration, while useMounted() is false, it renders a copy of the closed control with the same react-select__ classes. The copy shows the selected value or values from value or defaultValue, or the placeholder ("Select..." by default). It gets react-select--is-disabled when isDisabled is set.
  2. Once the component has mounted in the browser, React Select replaces the copy. instanceId comes from useId(), so the ids React Select generates are stable and unique on the page.

The control has its final size from the first paint, and the server and browser output never disagree.

Props

Select accepts the props of React Select. AdminKit changes a few defaults, and a prop you pass overrides them:

PropDefault in AdminKitEffect
unstyledtrueReact Select's own colors are off; the Sass styles the react-select__ classes
classNamePrefix"react-select"The class prefix the Sass expects
isClearablefalseNo clear button
closeMenuOnSelect!isMultiA multiple select stays open while you pick
menuPlacement"auto"The menu opens upward when there is no room below
noOptionsMessage"No results found" after a search, "No choices to choose from" otherwise
instanceIdfrom useId()Stable ids for the generated elements

className goes to the container, with react-select and react-select--single or react-select--multi. A server component can only pass props that reach the browser as data, such as strings, arrays and plain objects. onChange, formatOptionLabel, getOptionLabel and other functions need a client component, as shown below.

The component is in src/components/plugins/select.tsx. It renders React Select unstyled, with the react-select__ class prefix, and src/scss/5-vendor/_react-select.scss gives it the Bootstrap look.

Multiple values

When people may pick several values, add isMulti. The values show as tags with a remove button. The demo passes placeholder="".

<label className="form-label" id="states-label">States</label>
<Select options={states} isMulti placeholder="" aria-labelledby="states-label" />

Controlled value

When your code needs to read or set the selection, pass value and onChange from a client component. Give the select a name too: React Select then adds hidden inputs, so FormData in your submit handler includes the values. Send them to your API with fetch, because a static export has no Server Actions.

src/components/states-filter.tsx
"use client";

import { useState } from "react";
import { Select, type SelectOption } from "@/components/plugins/select";

export function StatesFilter({ options }: { options: SelectOption[] }) {
  const [selected, setSelected] = useState<readonly SelectOption[]>([]);

  return (
    <>
      <label className="form-label" id="filter-label">States</label>
      <Select
        options={options}
        isMulti
        value={selected}
        onChange={(values) => setSelected(values)}
        name="states"
        aria-labelledby="filter-label"
      />
    </>
  );
}

options is plain data, so the server component page can pass it to StatesFilter.

Accessibility

  • Name the select with aria-labelledby and the id of a visible label, or with inputId and a <label htmlFor>, as the tags field of the Prompts app does.
  • Type to search, move with the arrow keys, pick with Enter and close with Escape.
  • The server-rendered copy is not interactive: the select becomes usable once the page has hydrated.

Last updated on

On this page