Skip to content

Select (Native)

Native select
Thymeleaf source
<sl:label for="status">Status</sl:label>
<sl:native-select th:field="*{status}">
<option value="">Select status</option>
<option th:each="status : ${statuses}" th:value="${status}" th:text="#{|status.${status}|}">Todo</option>
</sl:native-select>
Rendered HTML
<label class="label" for="status">Status</label>
<div class="native-select-wrapper">
<select class="native-select" id="status" name="status">
<option value="">Select status</option>
<option value="todo">Todo</option>
<option value="in-progress">In progress</option>
<option value="done">Done</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>

Both are a real <select> underneath, bind with th:field and submit with the form. This one styles only the closed control and leaves the list to the operating system: on a phone that is the system’s own picker, which many people find easier to use. Use it for long lists (countries, years), in dense forms, and for multiple. <sl:select> draws its list in the page’s style, like shadcn/ui’s Select, with a check mark on the chosen item and room for an icon; use it where the look matters and the list is short enough to scan.

Write plain <option> and <optgroup> elements as the content, with th:each for options from the model. With th:field on the select, Spring marks the option that matches the field’s value as selected.

Groups
Thymeleaf source
<sl:label for="select-department">Department</sl:label>
<sl:native-select id="select-department" name="department">
<optgroup label="Engineering">
<option value="frontend">Frontend</option>
<option value="backend">Backend</option>
</optgroup>
<optgroup label="Sales">
<option value="sales-rep">Sales representative</option>
<option value="account-manager">Account manager</option>
</optgroup>
</sl:native-select>
Rendered HTML
<label class="label" for="select-department">Department</label>
<div class="native-select-wrapper">
<select class="native-select" id="select-department" name="department">
<optgroup label="Engineering">
<option value="frontend">Frontend</option>
<option value="backend">Backend</option>
</optgroup>
<optgroup label="Sales">
<option value="sales-rep">Sales representative</option>
<option value="account-manager">Account manager</option>
</optgroup>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>
Sizes
Thymeleaf source
<sl:native-select size="sm" name="priority" aria-label="Priority (small)">
<option>Low</option>
<option>High</option>
</sl:native-select>
<sl:native-select name="priority" aria-label="Priority">
<option>Low</option>
<option>High</option>
</sl:native-select>
Rendered HTML
<div class="native-select-wrapper" data-size="sm">
<select class="native-select" name="priority" aria-label="Priority (small)">
<option>Low</option>
<option>High</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>
<div class="native-select-wrapper">
<select class="native-select" name="priority" aria-label="Priority">
<option>Low</option>
<option>High</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>

size picks the height, as in shadcn/ui. It replaces HTML’s own size attribute, which turns a select into a list box; multiple still works, and hides the chevron.

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>.

As with the input, a field with errors marks the select aria-invalid="true".

InvalidWith th:field, a field that has errors marks the select aria-invalid by itself.
Thymeleaf source
<sl:native-select th:field="*{status}" aria-label="Status">…</sl:native-select>
Rendered HTML
<div class="native-select-wrapper">
<select class="native-select" name="status" aria-label="Status" aria-invalid="true">
<option value="">Select status</option>
<option value="todo">Todo</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>

The select fades with its chevron; the lyra skin also gives it a grey background, as it does a disabled input. HTML has no read-only select: readonly does nothing on one. To show a choice that cannot be changed, use a read-only input with the option’s text, and a hidden input if the value has to be submitted.

Disabled
Thymeleaf source
<sl:native-select name="status" aria-label="Status" disabled>
<option>Todo</option>
</sl:native-select>
Rendered HTML
<div class="native-select-wrapper">
<select class="native-select" name="status" aria-label="Status" disabled>
<option>Todo</option>
</select>
<svg xmlns="http://www.w3.org/2000/svg" class="icon native-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>
</div>
AttributeValuesDefaultDescription
sizesm defaultdefaultHeight of the select. It replaces HTML's size attribute, which turns a select into a list box.

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