What Is a Tailwind Select?
Spacing, borders, focus states and dark mode all live in the markup when you style a native select with Tailwind, so there’s no separate stylesheet to chase. The utility-first approach Tailwind takes supplies the looks. The browser keeps the behavior, meaning keyboard navigation and form submission, plus the platform popup.
So a page that sells a “Tailwind select component” is showing you a kit built on top of that element, or a replacement for it.
Don’t mix any of this up with the select-none, select-text, select-all and select-auto utilities. Those set user-select and decide whether people can highlight text in any element (Tailwind CSS documentation). They never touch the form control.
Basic select markup
Start with a <label for> whose value matches the select’s id. Then add the name attribute, because that becomes the key sent on submission.
Each choice gets its own <option value>, and the value is the data that actually gets submitted (MDN Web Docs). The first option should carry an empty value as a placeholder. The WHATWG HTML Standard requires that when the select is required, has no multiple and shows one row.
<label for="plan" class="block text-sm font-medium">Plan</label>
<select id="plan" name="plan" required class="mt-2 block w-full rounded-md border border-gray-300 px-3 py-2">
<option value="">Choose a plan</option>
<option value="free">Free</option>
<option value="pro">Pro</option>
</select>
Drop the placeholder option and a required select starts on its first real choice, so the form validates with a value nobody picked.
Grouped and multiple selects
Without multiple, a select shows one row. Add it and the default jumps to 4 rows (WHATWG HTML Standard).
Wrap related options in <optgroup label="..."> and browsers render a bold group title above indented choices. MDN lists group as the implicit ARIA role of optgroup, but how screen readers announce the group label varies by browser and select type. Keep each option label understandable on its own.
The implicit ARIA role of the select itself changes with the attributes. A plain select exposes a combobox role, while one with multiple or a size above 1 exposes a listbox (MDN Web Docs).
A multiple select renders as a list box, so it takes its own padding and border classes. The forms plugin gives it a separate class, covered next.
Setting Up the Forms Plugin for a Native Select
You can skip the forms plugin for a single select. It resets form controls to a consistent baseline that utilities then override, and without it you write that reset yourself with appearance-none, borders and padding.
Install it with npm install -D @tailwindcss/forms. In Tailwind CSS v4, register it in the main stylesheet:
@import "tailwindcss";
@plugin "@tailwindcss/forms";
Tailwind CSS v3 projects add require('@tailwindcss/forms') to the plugins array in tailwind.config.js instead (plugin README).
On a select, padding becomes customizable, so rounded-full px-4 py-3 works as written. A select[multiple] gets the same treatment.
The plugin also generates form-select and form-multiselect classes. They apply the same reset to any element, such as a <div> that has to look like a select.
The maintainers call the plugin a form reset, not a set of finished components. They also admit the default approach can be heavy-handed in existing projects, which is why the strategy option exists.
@plugin "@tailwindcss/forms" {
strategy: "class";
}
| Setup | What gets styled | Classes needed | Main risk |
|---|
| Base strategy | Every select on the page | None | Clashes with existing global styles |
| Class strategy | Only elements you mark | form-select, form-multiselect | Unmarked selects stay unstyled |
| No plugin | Nothing automatically | appearance-none plus your own borders | You maintain the reset yourself |
Add the plugin when a project has several form controls, since inputs, checkboxes and selects then share one baseline. For a single select on a marketing page, a few utilities do the job without it.
Those same resets carry over to the rest of the form, which the guide to building forms with Tailwind walks through.
How to Replace the Default Select Arrow With a Custom Chevron
Set appearance-none on the select, then lay an SVG chevron over its right edge with a grid wrapper and pointer-events-none. Tailwind’s own appearance documentation uses this overlay pattern.
- Wrap the select and the icon in a
grid grid-cols-1 container. - Put
col-start-1 row-start-1 appearance-none on the select, plus w-full and right padding such as pr-8. - Place the SVG in the same grid cell with
col-start-1 row-start-1 self-center justify-self-end. - Add
pointer-events-none to the SVG so clicks reach the select underneath. - Give the SVG
currentColor strokes, so text color utilities recolor it in dark mode too.
<div class="grid grid-cols-1">
<select id="plan" name="plan" class="col-start-1 row-start-1 w-full appearance-none rounded-md border border-gray-300 bg-white py-2 pr-8 pl-3 text-gray-900 dark:bg-gray-800 dark:text-white">
<option>Free</option>
<option>Pro</option>
</select>
<svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" aria-hidden="true" class="pointer-events-none col-start-1 row-start-1 mr-2 size-4 self-center justify-self-end text-gray-500">
<path d="M5 7l5 5 5-5" />
</svg>
</div>
The select stays the only interactive element, so focus, clicks and keyboard input behave natively.
Overlay icon or background image
The overlay recolors with text utilities and follows dark mode without a second asset. Heroicons offers ready-made chevrons you can paste in as inline SVG.
A background image keeps the markup to one element, using an arbitrary bg-[url(...)] value with bg-no-repeat and a right-hand position. The catch is that the arrow color is fixed inside the image, so a dark theme needs a second file.
Add bg-none to the select when only one arrow should show, so a background arrow from a plugin or kit cannot double up with the overlay.
The same idea works outside utilities, and the guide to styling a select in plain CSS shows the stylesheet version.
When forced colors mode hides your chevron
Tailwind’s appearance documentation says appearance-auto restores native rendering in certain accessibility modes. Its example pairs forced-colors:appearance-auto on the control with forced-colors:hidden on the custom SVG.
The docs demonstrate this on a checkbox, and the same two variants work on a select: the native arrow returns and the overlay disappears.
Test it by emulating forced-colors: active in your browser’s developer tools (Tailwind CSS documentation, October 2026).
Styling Focus, Disabled and Invalid States
Put a focus: or disabled: prefix on a utility, or invalid: for validation styling, and Tailwind generates CSS for the matching pseudo-class on the select. Variants stack, so focus:invalid:border-pink-500 targets a focused select that fails validation (Tailwind CSS documentation).
For focus, keep a visible indicator, for example focus:outline-2 focus:outline-offset-2 focus:outline-indigo-600. Disabled selects usually want disabled:cursor-not-allowed disabled:opacity-60, and invalid:border-red-500 covers the error border.
Removing the outline without a replacement leaves keyboard users with no position marker. That is the most common focus mistake on a restyled select.
A disabled select is also left out of the form submission, so use it for display only and keep the value in a hidden input if the server needs it.
In Tailwind CSS v4, hover: applies only on devices that support hover. Touch screens skip it, so the select’s essential states belong on focus and disabled, not hover.
Invalid without the false alarm
The :invalid pseudo-class matches from page load for a required select still sitting on its empty placeholder option. Error styling can therefore appear before anyone touches the field.
Tailwind CSS 4.1, released April 1, 2025, added user-valid and user-invalid variants for this case (Tailwind CSS changelog). They follow the browser’s user-interaction rules, so an untouched select stays neutral. On v3, stay with invalid: and scope it to a submitted form.
Errors set by the server or by JavaScript need a different hook. Set the ARIA attribute aria-invalid="true" on the select and style it with an arbitrary variant such as aria-[invalid=true]:border-red-500. Tailwind’s built-in aria-* variants cover only attributes such as aria-checked, aria-disabled and aria-required, so aria-invalid needs the bracket syntax or a custom variant.
For helper text, mark the select with peer and put invisible peer-invalid:visible on an error message that follows it in the markup.
Dark Mode, Size and Width Classes
The dark: variant follows prefers-color-scheme out of the box, so dark:border-gray-600 on a select needs no setup. A manual toggle takes one line in the stylesheet (Tailwind CSS v4.3 documentation, October 2026).
@import "tailwindcss";
@custom-variant dark (&:where(.dark, .dark *));
After that override, every dark: utility applies whenever a dark class sits higher in the tree, usually on <html>. A data-theme attribute works the same way with an attribute selector.
Color scheme for the parts your classes cannot reach
Tailwind’s scheme-* utilities set the CSS color-scheme property. Put scheme-light dark:scheme-dark on <html> and the browser draws its own form controls in matching defaults (Tailwind CSS documentation).
It does not make the open option list stylable. It only tells the browser which scheme to draw it in.
Size classes
| Size | Padding | Text | Chevron room |
|---|
| Small | py-1 pl-2 | text-xs | pr-7 |
| Medium | py-2 pl-3 | text-sm | pr-8 |
| Large | py-3 pl-4 | text-base | pr-10 |
Scale the right padding with the icon, or long option text runs under the chevron. On the small size, drop the SVG to size-3 as well.
Width
Inside a form column, w-full is usually all you need. Use w-full max-w-xs for a capped field, or w-full sm:w-64 if the width should change at a breakpoint.
Without a width class, a native select sizes itself to its longest option (MDN Web Docs). One long label can stretch a whole form row.
When the chevron overlay is in use, set the width on the grid wrapper so the icon and the control stay aligned.
Accessibility Requirements for a Styled Select
A native select carries most of its accessibility with it. A Tailwind select stays accessible as long as the styling does not remove its name, its focus indicator or its error message, so those are worth checking first.
Give it a visible label, or aria-label when the design has none. Make sure the focus indicator contrasts with its surroundings. Errors need a message linked to the control, since a red border alone says nothing to a screen reader. And don’t script anything that changes native key behavior.
WCAG 2.2 success criterion 1.4.11 asks for 3:1 contrast on the visual information needed to identify a form field, often its border, and on author-styled focus indicators (W3C Understanding documentation). Pale borders such as border-gray-300 on a white page are a common miss.
Step the border darker, for example border-gray-500, and confirm the result with a color contrast checker before shipping.
Point aria-describedby at the error message id so screen readers announce it together with the select. Inside a Field, Headless UI’s Description component creates that link automatically (Headless UI documentation).
The wider rules for labels, grouping and error handling sit in this guide to accessible forms.
Keyboard behavior is where replacements drift from the native control. In the WAI-ARIA Authoring Practices select-only combobox pattern, Tab selects the highlighted option, closes the list and moves focus onward, while a native select closes the list and keeps focus in place.
Small differences like that are one more reason to keep the native element when the design allows it.
Why Can’t You Style the Dropdown Options?
Because the browser or operating system draws the open option list, Tailwind classes reach the closed control and nothing beyond it. Setting appearance-none removes the arrow and native chrome and leaves the popup untouched (MDN Web Docs).
What the browser keeps control of
Position, background and spacing of the option list all belong to the browser. On phones the select opens operating system components, which Chrome’s documentation lists as something base-select gives up. And images or SVG placed inside an option are parsed away in a regular select.
Utilities on <option> elements work only where a given popup honors them, so a design cannot depend on them.
What appearance: base-select changes
One CSS property opts the select and its popup into a styleable state.
Chrome 135 shipped appearance: base-select, announced March 24, 2025, and Safari 27.0 added it, according to WebKit’s release notes. Firefox 149 added the value behind the dom.select.customizable_select.enabled and layout.css.appearance-base.enabled preferences, which stay off by default. MDN Web Docs still marks the feature as limited availability.
Support is uneven, so cross-browser compatibility decides whether it fits a project. Older guides still describe Safari as unsupported, which predates the Safari 27.0 release.
@supports (appearance: base-select) {
select,
::picker(select) {
appearance: base-select;
}
}
Tailwind’s appearance utilities cover only none and auto, so the opt-in belongs in the stylesheet, and the rest of the select stays on utilities.
New pseudo-elements address the parts: ::picker(select) for the popup, ::picker-icon for the arrow and ::checkmark for the selected mark (MDN Web Docs).
What you get is the real select with its keyboard handling and validation intact, options that can hold HTML such as images and SVG, a popup styled through ::picker(select), and no JavaScript at all.
What you pay is less obvious. Mobile operating system pickers no longer open, and the select stops taking the width of its longest option. Parser changes carried a risk of breaking existing markup, and Chrome kept the feature behind a Finch experiment at launch. If your options are rich, check the selected-value logic too.
Fallback for unsupported browsers
Wrap the opt-in in @supports (appearance: base-select) so unsupported browsers keep the ordinary select. WebKit gives the same advice: style for browsers that support it and let the rest fall back to built-in rendering.
Options whose meaning lives in an image or SVG lose it in the fallback, so keep a text label in every option.
Which Tailwind Select Approach Should You Choose?
A native select with utilities fits most forms. Step up to appearance: base-select when a styled popup matters and a native fallback is acceptable, and use a JavaScript component only for search or a fully custom option layout.
| Approach | Popup control | JavaScript | Best fit |
|---|
| Native select with utilities | Browser-drawn | None | Most forms, mobile |
appearance: base-select | Fully styleable in supporting browsers | None | Branded popup with fallback |
| Headless UI Select | Browser-drawn | React component | Data-attribute states on a native select |
| Headless UI Listbox or Combobox | Fully custom | Yes | Custom options, search |
| Radix UI or shadcn/ui Select | Fully custom | Yes, React | Design systems on React |
If the options are plain text and nobody needs search, keep the native select. If you want a branded popup and a native fallback is fine, go with base-select. Search, avatars or a custom layout that has to look identical in every browser point to Listbox, Combobox or a Radix-based Select.
Headless UI Select, Listbox and Combobox
Only two of Headless UI’s select components replace the browser popup. The Select itself is a light wrapper around the native element that exposes hover, focus, invalid and disabled states as data attributes, so a class like data-focus:bg-blue-100 works (Headless UI documentation). The popup stays browser-drawn, and the component is documented for React.
Listbox is a custom select with full control over the option list. Combobox is the same idea with a search input added.
Both custom components bring Floating UI positioning since v2.0 for React, so popups stay inside the viewport. The trade-off is a custom popup in place of the native mobile picker.
Radix UI Select and shadcn/ui
shadcn/ui’s documentation recommends its Native Select for native browser behavior, better performance and mobile-optimized dropdowns, and its Select for custom styling, animations and complex interactions.
In the Radix variant of shadcn/ui, the Select is built on Radix UI’s Select primitive and copied into your project, so you own the Tailwind classes. The docs also offer Base UI and React Aria variants. Either one assumes a project already using Tailwind CSS in React.
For a short list of plain options on a phone, that official guidance points back to the native element.
Prebuilt kits and Tom Select
- Tailwind Plus has select menus built by the Tailwind team. It is a paid product separate from the framework, and its
@tailwindplus/elements library adds an el-select element for plain HTML. - Flowbite styles a native select with Tailwind classes and shows multiple, size, disabled and underline examples.
- daisyUI uses a
select class. Version 5 removed the select-bordered modifier because the border is now the default (daisyUI upgrade guide). - Preline relies on the forms plugin by default and sends searchable and multiple-selection needs to its advanced select.
A Tailwind UI kit saves time when the whole interface is built from one source, and costs consistency when only a single select is needed.
Tom Select adds search to an existing select and weighs about 16 KB gzipped, per its README. The original select stays in the DOM and keeps its value, so plain form posts still work, but the library ships its own stylesheet themes that need matching to your Tailwind design tokens.
Tailwind Select FAQ
Why does a multiple select show no arrow to replace?
A select with the multiple attribute displays as a list box, so the browser draws no dropdown arrow.
Skip the chevron overlay for that field and set the number of visible rows with the size attribute instead.
How do you gray out the placeholder text in a select?
A select has no placeholder pseudo-element, so the empty first option takes the same text color as every other option.
On a required select, that option makes the control invalid, so invalid:text-gray-400 grays it until a real choice is made. Use user-invalid for error styling on the same field, and check that the open list stays readable.
Can you put an icon on the left side inside a Tailwind select?
Yes. Reuse the grid overlay from the chevron, place the SVG with justify-self-start and pointer-events-none, and add left padding such as pl-10 to the select so the text clears the icon.
How do you use a Tailwind select in React or Vue?
Keep the same classes on the native element and bind its value, with value and onChange in React or v-model in Vue.
Headless UI’s Select wraps the native element for React and adds data-attribute states, but the styling approach does not change.
What to Test Before Shipping a Tailwind Select
Before release, do a keyboard-only pass, emulate forced colors, load the page in a browser without appearance: base-select support and open it on a real phone.
Run them in that order. The first three take minutes in a desktop browser and catch the failures that styling introduces, while the phone check needs a device and only pays off once the others pass.
The phone check carries a catch. Chrome’s documentation says base-select stops the operating system picker from opening, while WebKit says unsupported browsers fall back to built-in rendering, so one page can show two different mobile pickers depending on the browser.
Verified on October 29, 2026: Firefox keeps the feature behind a preference. Once it ships by default, the fallback check shrinks to older browser versions.
Run the finished form through a web accessibility checklist before it goes live.