Skip to content

Select

SelectA placeholder until an option is chosen; th:field marks the bound one.
Thymeleaf source
<sl:field th:field="*{fruit}">
<sl:field-label>Fruit</sl:field-label>
<sl:select placeholder="Select a fruit">
<sl:select-group label="Fruits">
<sl:select-item value="apple">Apple</sl:select-item>
<sl:select-item value="banana">Banana</sl:select-item>
<sl:select-item value="blueberry">Blueberry</sl:select-item>
<sl:select-item value="grapes">Grapes</sl:select-item>
<sl:select-item value="pineapple">Pineapple</sl:select-item>
</sl:select-group>
</sl:select>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="fruit">Fruit</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" id="fruit" name="fruit">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="" hidden data-placeholder="true" selected>Select a fruit</option>
<optgroup class="select-group" label="Fruits">
<option class="select-item" value="apple">
Apple
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="banana">
Banana
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="blueberry">
Blueberry
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="grapes">
Grapes
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="pineapple">
Pineapple
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>

A select is <sl:select> holding <sl:select-item>s, optionally in <sl:select-group>s with <sl:select-separator>s between them. It renders a real <select> with <option>, <optgroup> and <hr> elements, so th:field binds it as it binds any select: Spring marks the bound item as selected, and the form submits the chosen value.

<form th:object="${order}" th:action="@{/orders}" method="post">
<sl:field th:field="*{fruit}">
<sl:field-label>Fruit</sl:field-label>
<sl:select placeholder="Select a fruit">
<sl:select-group label="Fruits">
<sl:select-item th:each="fruit : ${fruits}" th:value="${fruit.id}">[[${fruit.name}]]</sl:select-item>
</sl:select-group>
</sl:select>
<sl:field-error/>
</sl:field>
</form>

Give an item its text as content ([[${fruit.name}]]), not with th:text: that attribute passes through to the <option> and replaces its check mark too. (The select still works; the browser’s own check mark shows instead.)

Both are a real <select> underneath, bind with th:field and submit with the form.

  • <sl:select> looks like shadcn/ui’s Select: its list is drawn in the page’s style, with a check mark on the chosen item, and items can start with an icon. Use it where the look matters and the list is short enough to scan: a few to a few dozen items.
  • <sl:native-select> styles only the closed control and leaves the list to the operating system: on a phone that is the system’s own picker wheel or sheet, which many people find easier to use. Use it for long lists (countries, years), inside dense forms, and for multiple, which <sl:select> does not support.

placeholder adds a first option with an empty value, hidden from the list. It is chosen while the field has no value, and shown in the muted colour. A required select with the placeholder chosen is invalid, as for any select whose first option has an empty value; with server-side validation (@NotBlank, @NotNull) the field shows its error instead.

<sl:select-group label="..."> is an <optgroup>: its label shows above its items and is read with them. <sl:select-separator/> is an <hr>, which browsers draw as a line in a select’s list.

Groups and separators
Thymeleaf source
<sl:field th:field="*{food}">
<sl:field-label>Food</sl:field-label>
<sl:select>
<sl:select-group label="Fruits">
<sl:select-item value="apple">Apple</sl:select-item>
<sl:select-item value="banana">Banana</sl:select-item>
</sl:select-group>
<sl:select-separator/>
<sl:select-group label="Vegetables">
<sl:select-item value="carrot">Carrot</sl:select-item>
<sl:select-item value="broccoli">Broccoli</sl:select-item>
</sl:select-group>
</sl:select>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="food">Food</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" id="food" name="food">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<optgroup class="select-group" label="Fruits">
<option class="select-item" value="apple">
Apple
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="banana">
Banana
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
<hr class="select-separator">
<optgroup class="select-group" label="Vegetables">
<option class="select-item" value="carrot" selected>
Carrot
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="broccoli">
Broccoli
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>

An item may start with an icon. The closed select shows the chosen item’s icon and text.

IconsAn item may start with an icon; the select shows it with the chosen item's text. Older browsers, which drop markup inside an option, show the text alone.
Thymeleaf source
<sl:field th:field="*{chartType}">
<sl:field-label>Chart</sl:field-label>
<sl:select>
<sl:select-item value="bar"><sl:icon name="chart-bar"/>Bar</sl:select-item>
<sl:select-item value="line"><sl:icon name="chart-line"/>Line</sl:select-item>
<sl:select-item value="pie"><sl:icon name="chart-pie"/>Pie</sl:select-item>
</sl:select>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="chartType">Chart</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" id="chartType" name="chartType">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="bar">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M3 3v16a2 2 0 0 0 2 2h16"></path>
<path d="M7 16h8"></path>
<path d="M7 11h12"></path>
<path d="M7 6h3"></path>
</svg>
Bar
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="line" selected>
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M3 3v16a2 2 0 0 0 2 2h16"></path>
<path d="m19 9-5 5-4-4-3 3"></path>
</svg>
Line
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="pie">
<svg xmlns="http://www.w3.org/2000/svg" class="icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M21 12c.552 0 1.005-.449.95-.998a10 10 0 0 0-8.953-8.951c-.55-.055-.998.398-.998.95v8a1 1 0 0 0 1 1z"></path>
<path d="M21.21 15.89A10 10 0 1 1 8 2.83"></path>
</svg>
Pie
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>

Long lists scroll; the list never grows taller than the window.

Long listsThe list scrolls when it is taller than the window.
Thymeleaf source
<sl:field th:field="*{timezone}">
<sl:field-label>Timezone</sl:field-label>
<sl:select placeholder="Select a timezone">
<sl:select-group label="Europe &amp; Africa">
<sl:select-item value="gmt">Greenwich Mean Time (GMT)</sl:select-item>
<sl:select-item value="cet">Central European Time (CET)</sl:select-item>
<sl:select-item value="eet">Eastern European Time (EET)</sl:select-item>
<sl:select-item value="west">Western European Summer Time (WEST)</sl:select-item>
<sl:select-item value="cat">Central Africa Time (CAT)</sl:select-item>
<sl:select-item value="eat">East Africa Time (EAT)</sl:select-item>
</sl:select-group>
<sl:select-group label="Asia">
<sl:select-item value="msk">Moscow Time (MSK)</sl:select-item>
<sl:select-item value="ist">India Standard Time (IST)</sl:select-item>
<sl:select-item value="cst_china">China Standard Time (CST)</sl:select-item>
<sl:select-item value="jst">Japan Standard Time (JST)</sl:select-item>
<sl:select-item value="kst">Korea Standard Time (KST)</sl:select-item>
</sl:select-group>
<sl:select-group label="Americas">
<sl:select-item value="est">Eastern Standard Time (EST)</sl:select-item>
<sl:select-item value="cst">Central Standard Time (CST)</sl:select-item>
<sl:select-item value="mst">Mountain Standard Time (MST)</sl:select-item>
<sl:select-item value="pst">Pacific Standard Time (PST)</sl:select-item>
</sl:select-group>
</sl:select>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="timezone">Timezone</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" id="timezone" name="timezone">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="" hidden data-placeholder="true" selected>Select a timezone</option>
<optgroup class="select-group" label="Europe &amp; Africa">
<option class="select-item" value="gmt">
Greenwich Mean Time (GMT)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="cet">
Central European Time (CET)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="eet">
Eastern European Time (EET)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="west">
Western European Summer Time (WEST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="cat">
Central Africa Time (CAT)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="eat">
East Africa Time (EAT)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
<optgroup class="select-group" label="Asia">
<option class="select-item" value="msk">
Moscow Time (MSK)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="ist">
India Standard Time (IST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="cst_china">
China Standard Time (CST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="jst">
Japan Standard Time (JST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="kst">
Korea Standard Time (KST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
<optgroup class="select-group" label="Americas">
<option class="select-item" value="est">
Eastern Standard Time (EST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="cst">
Central Standard Time (CST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="mst">
Mountain Standard Time (MST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="pst">
Pacific Standard Time (PST)
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</optgroup>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>
Sizes
Thymeleaf source
<sl:select size="sm" name="density" aria-label="Density (small)">
<sl:select-item value="compact">Compact</sl:select-item>
<sl:select-item value="comfortable">Comfortable</sl:select-item>
</sl:select>
<sl:select name="density" aria-label="Density">
<sl:select-item value="compact">Compact</sl:select-item>
<sl:select-item value="comfortable">Comfortable</sl:select-item>
</sl:select>
Rendered HTML
<div class="select-wrapper" x-data="slSelect" data-size="sm">
<select class="select" name="density" aria-label="Density (small)">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="compact">
Compact
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="comfortable">
Comfortable
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
<div class="select-wrapper" x-data="slSelect">
<select class="select" name="density" aria-label="Density">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="compact">
Compact
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="comfortable">
Comfortable
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>

size picks the height, as in shadcn/ui. The list opens below the select, lined up with its start and at least as wide as it, and above it when there is no room below.

The select sits in a wrapper that also holds the chevron. class goes to that wrapper, so class="w-full" widens both; every other attribute goes to the <select>.

  • th:field, on the select or on its field, binds it. In a field, the select also takes the field’s id (the label points at it) and aria-describedby (the description and, while there are errors, the error).
  • Errors. A field with errors marks the select aria-invalid="true", and <sl:form-errors autofocus/> gives it autofocus when it is the first control with errors.
  • In a field set with a th:field of its own (<sl:field-set th:field="*{plan}">), the select takes the field but no id; give it an aria-label, as the legend names the group, not the select.
  • Events. Choosing an option fires input and change on the <select>, in every browser and with Shadleaf’s list box too, so hx-trigger="change" and your own listeners work unchanged. A script that sets the select’s value should fire change itself, as it should for any select; the list box then shows the new value.
  • Form reset puts the select back to its rendered value, and the list box follows.
With errorsIn a field with errors, the select is aria-invalid and described by the message.
Thymeleaf source
<sl:field th:field="*{shipping}">
<sl:field-label>Shipping</sl:field-label>
<sl:select placeholder="Select a shipping method">
<sl:select-item value="standard">Standard</sl:select-item>
<sl:select-item value="express">Express</sl:select-item>
</sl:select>
<sl:field-error/>
</sl:field>
Rendered HTML
<div class="field" data-invalid="true">
<label class="label field-label" for="shipping">Shipping</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" aria-invalid="true" id="shipping" aria-describedby="shipping-error" name="shipping">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="" hidden data-placeholder="true" selected>Select a shipping method</option>
<option class="select-item" value="standard">
Standard
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="express">
Express
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
<div class="field-error" id="shipping-error">must not be empty</div>
</div>

disabled on the select, or on a <sl:field-set> around it, disables it: it fades with its chevron, and the lyra skin also gives it a grey background. disabled on an item leaves it in the list, faded; the keyboard and the pointer skip it.

HTML has no read-only select: readonly does nothing on one, and Shadleaf’s list box ignores it too. To show a choice that cannot be changed, use a read-only input with the item’s text, and a hidden input if the value has to be submitted.

DisabledA disabled select, and a disabled item that the keyboard and the pointer skip.
Thymeleaf source
<sl:field th:field="*{region}">
<sl:field-label>Region</sl:field-label>
<sl:select disabled>
<sl:select-item value="eu">Europe</sl:select-item>
</sl:select>
</sl:field>
<sl:field th:field="*{seat}">
<sl:field-label>Seat</sl:field-label>
<sl:select>
<sl:select-item value="aisle">Aisle</sl:select-item>
<sl:select-item value="middle" disabled>Middle</sl:select-item>
<sl:select-item value="window">Window</sl:select-item>
</sl:select>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="region">Region</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" disabled id="region" name="region">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="eu" selected>
Europe
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>
<div class="field">
<label class="label field-label" for="seat">Seat</label>
<div class="select-wrapper" x-data="slSelect">
<select class="select" id="seat" name="seat">
<button type="button">
<selectedcontent></selectedcontent>
</button>
<option class="select-item" value="aisle">
Aisle
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="middle" disabled>
Middle
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
<option class="select-item" value="window" selected>
Window
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-icon" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="m6 9 6 6 6-6"></path>
</svg>
<template class="select-indicator-template">
<svg xmlns="http://www.w3.org/2000/svg" class="icon select-item-indicator" viewBox="0 0 24 24" width="24" height="24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true" focusable="false">
<path d="M20 6 9 17l-5-5"></path>
</svg>
</template>
</div>
</div>

With the focus on the select:

Key Closed Open
Arrow down, arrow up Opens the list on the chosen item Next, previous item
Enter, Space Opens the list Chooses the highlighted item and closes
Home, End Opens the list on the first, last item First, last item
A letter Chooses (customizable select) or highlights (list box) the next item that starts with it Highlights the next item that starts with it; typing quickly matches a prefix
Escape Closes without choosing; the value stays as it was
Tab Moves on The customizable select closes without choosing, the focus staying on it; Shadleaf’s list box chooses the highlighted item and moves on, as the ARIA pattern describes

Disabled items, group labels and separators are skipped. The item under the pointer is highlighted, and only keyboard focus draws the focus ring as well. Focus stays on the select (or on the list box’s button) while the list is open, and is back there when it closes.

With the customizable select the browser handles the keys itself; slSelect only adds what it leaves out: Enter opens the list, Home and End open it on the first and last item, and the item under the pointer takes the focus. It also works around a Chrome bug: after a form reset the select showed the option chosen before, not the reset one.

  • A response that renders the form again with errors keeps the chosen value (th:field marks it), and focus goes where autofocus says, the select included; see Forms with htmx.
  • A swap that replaces an open select leaves nothing open behind.
  • Back restores the select as htmx saved it; Shadleaf’s list box builds itself again from the restored select.
  • A select can trigger a request itself: hx-get="/cities" hx-trigger="change" hx-target="#city-field". Swapping in new items (hx-target on the select) is picked up by the list box.

The select and its list keep a visible border, and the highlighted item an outline, when the system’s colours replace the page’s (Windows High Contrast). A disabled select turns GrayText instead of fading.

These shadcn/Radix parts and props have no counterpart, on purpose:

  • SelectTrigger, SelectValue and SelectContent. In HTML the select is the trigger, shows its own value and holds its options, so <sl:select> covers all three: size from the trigger, placeholder from the value.
  • SelectLabel on its own. A group’s heading is the label of <sl:select-group> (an <optgroup> needs one); there is no heading outside a group, which no browser would show.
  • SelectScrollUpButton, SelectScrollDownButton. The list scrolls like any other, with the wheel, the keys or a finger.
  • position="item-aligned", side, align. shadcn’s default lays the list over the select with the chosen item on top of it; the customizable select always opens below the select (above when there is no room), and the list box does the same, so both look alike.
  • State in the browser (value/defaultValue/onValueChange, open/onOpenChange): the server renders the value with th:field, and the select’s change event reports a new one.
  • multiple, which shadcn does not have either: use <sl:native-select multiple> or a field set of checkboxes.
  • SelectItem’s textValue: typeahead reads the item’s text.

Browsers are halfway through gaining the customizable select (appearance: base-select), which lets CSS draw a select’s list. Checked on 30 September 2026:

Browser Customizable select What Shadleaf does
Chrome, Edge 135 and later Yes The browser draws the list in the skin’s style, above everything else, placed with anchor positioning.
Safari 27 and later Yes The same.
Firefox (155 at the time of writing) Behind a setting Once Alpine runs, the slSelect component hides the select and shows a button and a list box of its own in its place, built from the select’s options.
Older browsers, or no Alpine No The select is drawn like the native select: the closed control has the skin’s look, and the list is the browser’s own.

The server always renders the same markup, a real select, so the choice happens in the browser and needs nothing from you. That also means the form works before Alpine starts and without it: the select submits, binds, validates and takes the keyboard like any select (rule 4 of Alpine).

The customizable select is used wherever the browser has it, rather than Shadleaf’s list box everywhere, because the browser’s own is simply the better control: the keyboard, the screen reader, the top layer, the placing next to the select and the form all come from the browser, and keep working if the page’s JavaScript fails. The list box exists only for the browsers still missing it, and follows the same ARIA select-only combobox pattern the browsers follow.

Only options with text work everywhere: an older browser drops any markup inside an <option> and keeps its text, so an item’s icon is extra, never its only label.

AttributeValuesDefaultDescription
sizesm defaultdefaultHeight of the select.
placeholderstring—Text shown while no option is chosen: a first option with an empty value, hidden from the list. A required select with it selected is invalid.

Every prop can also be set from an expression: th:size="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:select-item declares no props; every attribute passes through.

AttributeValuesDefaultDescription
labelstringrequiredThe group's heading, shadcn/ui's SelectLabel.

Every prop can also be set from an expression: th:label="${...}". Any other attribute passes through to the rendered element, and class is appended.

sl:select-separator declares no props; every attribute passes through.

A small Alpine component, slSelect, sits on the select’s wrapper: <div class="select-wrapper" x-data="slSelect">.

  • Without Alpine the select works as the browser’s own, customizable or not.
  • Where the customizable select is missing, slSelect hides the <select> (it stays in the form, and still submits, resets and validates) and adds a <button role="combobox" class="select select-trigger"> and a <div role="listbox" class="select-content" popover> after it. The button takes the select’s label (its <label>s, aria-labelledby or aria-label), aria-describedby, aria-invalid, required and disabled, and follows them when they change. Focus that reaches the hidden select, from a click on its label, autofocus or the browser’s validation, moves on to the button.
  • It needs no Alpine plugin. If you load Alpine yourself (shadleaf.assets.alpine=external), plain Alpine is enough.
  • Keep your own x-data off <sl:select>: class is the only attribute that reaches the wrapper, and an element has one x-data. Listen for change on the select instead.