Select
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>Built from parts
Section titled “Built from parts”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.)
Select or native select?
Section titled “Select or native select?”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 formultiple, which<sl:select>does not support.
Placeholder
Section titled “Placeholder”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.
Groups, separators and icons
Section titled “Groups, separators and icons”<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.
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.
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.
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 & 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 & 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>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>.
In a form
Section titled “In a form”th:field, on the select or on its field, binds it. In a field, the select also takes the field’sid(the label points at it) andaria-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 itautofocuswhen it is the first control with errors. - In a field set with a
th:fieldof its own (<sl:field-set th:field="*{plan}">), the select takes the field but no id; give it anaria-label, as the legend names the group, not the select. - Events. Choosing an option fires
inputandchangeon the<select>, in every browser and with Shadleaf’s list box too, sohx-trigger="change"and your own listeners work unchanged. A script that sets the select’s value should firechangeitself, 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.
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
Section titled “Disabled”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.
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>Keyboard
Section titled “Keyboard”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.
With htmx
Section titled “With htmx”- A response that renders the form again with errors keeps the chosen value (
th:fieldmarks it), and focus goes whereautofocussays, 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-targeton the select) is picked up by the list box.
Forced colours
Section titled “Forced colours”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.
Left out
Section titled “Left out”These shadcn/Radix parts and props have no counterpart, on purpose:
SelectTrigger,SelectValueandSelectContent. In HTML the select is the trigger, shows its own value and holds its options, so<sl:select>covers all three:sizefrom the trigger,placeholderfrom the value.SelectLabelon its own. A group’s heading is thelabelof<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 withth:field, and the select’schangeevent reports a new one. multiple, which shadcn does not have either: use<sl:native-select multiple>or a field set of checkboxes.SelectItem’stextValue: typeahead reads the item’s text.
How it is drawn
Section titled “How it is drawn”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.
Attributes
Section titled “Attributes”sl:select
Section titled “sl:select”| Attribute | Values | Default | Description |
|---|---|---|---|
size | sm default | default | Height of the select. |
placeholder | string | — | 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
Section titled “sl:select-item”sl:select-item declares no props; every attribute passes through.
sl:select-group
Section titled “sl:select-group”| Attribute | Values | Default | Description |
|---|---|---|---|
label | string | required | The 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
Section titled “sl:select-separator”sl:select-separator declares no props; every attribute passes through.
JavaScript
Section titled “JavaScript”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,
slSelecthides 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-labelledbyoraria-label),aria-describedby,aria-invalid,requiredanddisabled, and follows them when they change. Focus that reaches the hidden select, from a click on its label,autofocusor 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-dataoff<sl:select>:classis the only attribute that reaches the wrapper, and an element has onex-data. Listen forchangeon the select instead.