Skip to content

Input Group

Input group
Thymeleaf source
<sl:input-group>
<sl:input-group-input type="search" name="q" placeholder="Search..." aria-label="Search"/>
<sl:input-group-addon><sl:icon name="search"/></sl:input-group-addon>
<sl:input-group-addon align="inline-end">12 results</sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="search" name="q" placeholder="Search..." aria-label="Search">
<div class="input-group-addon" role="group">
<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 21-4.34-4.34"></path>
<circle cx="11" cy="11" r="8"></circle>
</svg>
</div>
<div class="input-group-addon" role="group" data-align="inline-end">12 results</div>
</div>
sl:input-group
├── sl:input-group-input or sl:input-group-textarea
└── sl:input-group-addon
├── sl:input-group-text
├── sl:input-group-button
├── sl:icon
└── sl:kbd

The group draws the border, and its control has none of its own: the group shows the control’s focus ring, its error and its read-only look. A disabled control fades the whole group. A click on an addon, outside a button or link in it, focuses the control.

Write the addons after the control, so the control comes first in the tab order and is read before them; align on the addon places it. inline-start, the default, puts it before the control on its line:

Inline startThe default align. The addon is written after the input, so the input comes first in the tab order.
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="query" placeholder="Search..." aria-label="Search"/>
<sl:input-group-addon><sl:icon name="search"/></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="query" placeholder="Search..." aria-label="Search">
<div class="input-group-addon" role="group">
<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 21-4.34-4.34"></path>
<circle cx="11" cy="11" r="8"></circle>
</svg>
</div>
</div>
Inline end
Thymeleaf source
<sl:input-group>
<sl:input-group-input type="password" name="apiKey" placeholder="Enter password" aria-label="Password"/>
<sl:input-group-addon align="inline-end"><sl:icon name="eye-off"/></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="password" name="apiKey" placeholder="Enter password" aria-label="Password">
<div class="input-group-addon" role="group" data-align="inline-end">
<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="M10.733 5.076a10.744 10.744 0 0 1 11.205 6.575 1 1 0 0 1 0 .696 10.747 10.747 0 0 1-1.444 2.49"></path>
<path d="M14.084 14.158a3 3 0 0 1-4.242-4.242"></path>
<path d="M17.479 17.499a10.75 10.75 0 0 1-15.417-5.151 1 1 0 0 1 0-.696 10.75 10.75 0 0 1 4.446-5.143"></path>
<path d="m2 2 20 20"></path>
</svg>
</div>
</div>

block-start puts the addon above the control, across the whole group. border draws a line between the addon and the control. In such a row an sl:input-group-text takes the free space, so what follows it sits at the end.

Block startAbove the input, across the whole group. border draws a line between the addon and the control.
Thymeleaf source
<sl:input-group>
<sl:input-group-input id="input-group-full-name" name="fullName" placeholder="Jane Doe"/>
<sl:input-group-addon align="block-start">
<sl:label for="input-group-full-name">Full name</sl:label>
</sl:input-group-addon>
</sl:input-group>
<sl:input-group>
<sl:input-group-textarea name="script" rows="3" aria-label="Script">console.log('Hello, world!');</sl:input-group-textarea>
<sl:input-group-addon align="block-start" border>
<sl:input-group-text><sl:icon name="file-code"/> script.js</sl:input-group-text>
<sl:input-group-button size="icon-xs" aria-label="Copy"><sl:icon name="copy"/></sl:input-group-button>
</sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" id="input-group-full-name" name="fullName" placeholder="Jane Doe">
<div class="input-group-addon" role="group" data-align="block-start">
<label class="label" for="input-group-full-name">Full name</label>
</div>
</div>
<div class="input-group" role="group">
<textarea class="input-group-textarea" name="script" rows="3" aria-label="Script">console.log(&apos;Hello, world!&apos;);</textarea>
<div class="input-group-addon" role="group" data-align="block-start" data-border="bottom">
<span class="input-group-text">
<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="M6 22a2 2 0 0 1-2-2V4a2 2 0 0 1 2-2h8a2.4 2.4 0 0 1 1.704.706l3.588 3.588A2.4 2.4 0 0 1 20 8v12a2 2 0 0 1-2 2z"></path>
<path d="M14 2v5a1 1 0 0 0 1 1h5"></path>
<path d="M10 12.5 8 15l2 2.5"></path>
<path d="m14 12.5 2 2.5-2 2.5"></path>
</svg>
script.js
</span>
<button class="button input-group-button" type="button" data-variant="ghost" data-size="icon-xs" aria-label="Copy">
<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">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
</svg>
</button>
</div>
</div>
Block endBelow the input, across the whole group.
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="price" placeholder="Enter amount" aria-label="Amount"/>
<sl:input-group-addon align="block-end">
<sl:input-group-text>USD</sl:input-group-text>
</sl:input-group-addon>
</sl:input-group>
<sl:input-group>
<sl:input-group-textarea name="comment" rows="3" placeholder="Write a comment..." aria-label="Comment"/>
<sl:input-group-addon align="block-end" border>
<sl:input-group-text>0/280 characters</sl:input-group-text>
<sl:input-group-button variant="default" size="sm" type="submit">Post</sl:input-group-button>
</sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="price" placeholder="Enter amount" aria-label="Amount">
<div class="input-group-addon" role="group" data-align="block-end">
<span class="input-group-text">USD</span>
</div>
</div>
<div class="input-group" role="group">
<textarea class="input-group-textarea" name="comment" rows="3" placeholder="Write a comment..." aria-label="Comment"></textarea>
<div class="input-group-addon" role="group" data-align="block-end" data-border="top">
<span class="input-group-text">0/280 characters</span>
<button class="button input-group-button" type="submit" data-size="sm">Post</button>
</div>
</div>

sl:input-group-text holds muted text: a currency, a unit, a domain. The input still needs a name of its own, as the text is not its label: aria-label="Amount in US dollars".

TextA currency, a unit or a domain before or after the input.
Thymeleaf source
<sl:input-group>
<sl:input-group-input type="number" name="amount" placeholder="0.00" aria-label="Amount in US dollars"/>
<sl:input-group-addon><sl:input-group-text>$</sl:input-group-text></sl:input-group-addon>
<sl:input-group-addon align="inline-end"><sl:input-group-text>USD</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
<sl:input-group>
<sl:input-group-input name="website" placeholder="example" aria-label="Website"/>
<sl:input-group-addon><sl:input-group-text>https://</sl:input-group-text></sl:input-group-addon>
<sl:input-group-addon align="inline-end"><sl:input-group-text>.com</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
<sl:input-group>
<sl:input-group-input type="number" name="weight" aria-label="Weight in kilograms"/>
<sl:input-group-addon align="inline-end"><sl:input-group-text>kg</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="number" name="amount" placeholder="0.00" aria-label="Amount in US dollars">
<div class="input-group-addon" role="group">
<span class="input-group-text">$</span>
</div>
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">USD</span>
</div>
</div>
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="website" placeholder="example" aria-label="Website">
<div class="input-group-addon" role="group">
<span class="input-group-text">https://</span>
</div>
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">.com</span>
</div>
</div>
<div class="input-group" role="group">
<input class="input-group-input" type="number" name="weight" aria-label="Weight in kilograms">
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">kg</span>
</div>
</div>

sl:input-group-button is an sl:button, ghost and xs by default, with sizes that fit inside the group: xs, sm, icon-xs and icon-sm. An icon button needs an aria-label. As every sl:button, it does not submit unless you write type="submit".

ButtonAn icon button needs an aria-label.
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="shareUrl" value="https://shadleaf.dev" readonly aria-label="Link"/>
<sl:input-group-addon align="inline-end">
<sl:input-group-button size="icon-xs" aria-label="Copy"><sl:icon name="copy"/></sl:input-group-button>
</sl:input-group-addon>
</sl:input-group>
<sl:input-group>
<sl:input-group-input type="search" name="docsSearch" placeholder="Type to search..." aria-label="Search the docs"/>
<sl:input-group-addon align="inline-end">
<sl:input-group-button variant="secondary" type="submit">Search</sl:input-group-button>
</sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="shareUrl" value="https://shadleaf.dev" readonly aria-label="Link">
<div class="input-group-addon" role="group" data-align="inline-end">
<button class="button input-group-button" type="button" data-variant="ghost" data-size="icon-xs" aria-label="Copy">
<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">
<rect width="14" height="14" x="8" y="8" rx="2" ry="2"></rect>
<path d="M4 16c-1.1 0-2-.9-2-2V4c0-1.1.9-2 2-2h10c1.1 0 2 .9 2 2"></path>
</svg>
</button>
</div>
</div>
<div class="input-group" role="group">
<input class="input-group-input" type="search" name="docsSearch" placeholder="Type to search..." aria-label="Search the docs">
<div class="input-group-addon" role="group" data-align="inline-end">
<button class="button input-group-button" type="submit" data-variant="secondary" data-size="xs">Search</button>
</div>
</div>

The key is decoration: say the shortcut with aria-keyshortcuts on the input, and hide the addon from screen readers.

Kbd
Thymeleaf source
<sl:input-group>
<sl:input-group-input type="search" name="commandSearch" placeholder="Search..." aria-label="Search"
aria-keyshortcuts="Control+K"/>
<sl:input-group-addon><sl:icon name="search"/></sl:input-group-addon>
<sl:input-group-addon align="inline-end" aria-hidden="true"><sl:kbd>⌘K</sl:kbd></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="search" name="commandSearch" placeholder="Search..." aria-label="Search" aria-keyshortcuts="Control+K">
<div class="input-group-addon" role="group">
<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 21-4.34-4.34"></path>
<circle cx="11" cy="11" r="8"></circle>
</svg>
</div>
<div class="input-group-addon" role="group" data-align="inline-end" aria-hidden="true">
<kbd class="kbd">⌘K</kbd>
</div>
</div>

loading on the button shows a spinner, disables it and announces the busy state.

LoadingA loading button shows a spinner, announces the busy state and is disabled.
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="username" value="wim" aria-label="Username"/>
<sl:input-group-addon align="inline-end">
<sl:input-group-button loading>Check</sl:input-group-button>
</sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="username" value="wim" aria-label="Username">
<div class="input-group-addon" role="group" data-align="inline-end">
<button class="button input-group-button" type="button" data-variant="ghost" data-size="xs" aria-busy="true" disabled>
<span class="button-icon" data-icon="inline-start">
<svg xmlns="http://www.w3.org/2000/svg" class="icon button-spinner" 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 12a9 9 0 1 1-6.219-8.56"></path>
</svg>
</span>
<span class="sl-sr-only">Loading</span>
Check
</button>
</div>
</div>

Put th:field on the field, as for an input. The sl:input-group-input (or sl:input-group-textarea) inside the group takes it: Spring sets its id, name and value, the label points at it, and it is described by the field’s description and error.

<form th:object="${order}" th:action="@{/orders}" method="post">
<sl:field th:field="*{budget}">
<sl:field-label>Budget</sl:field-label>
<sl:input-group>
<sl:input-group-input type="number" placeholder="0.00"/>
<sl:input-group-addon><sl:input-group-text>€</sl:input-group-text></sl:input-group-addon>
<sl:input-group-addon align="inline-end"><sl:input-group-text>EUR</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
<sl:field-description>Excluding VAT.</sl:field-description>
<sl:field-error/>
</sl:field>
</form>
In a fieldPut th:field on the field. The input in the group takes it, and the label, description and error point at the input.
Thymeleaf source
<sl:field th:field="*{budget}">
<sl:field-label>Budget</sl:field-label>
<sl:input-group>
<sl:input-group-input type="number" placeholder="0.00"/>
<sl:input-group-addon><sl:input-group-text>€</sl:input-group-text></sl:input-group-addon>
<sl:input-group-addon align="inline-end"><sl:input-group-text>EUR</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
<sl:field-description>Excluding VAT.</sl:field-description>
<sl:field-error/>
</sl:field>
Rendered HTML
<div class="field">
<label class="label field-label" for="budget">Budget</label>
<div class="input-group" role="group">
<input class="input-group-input" type="number" placeholder="0.00" id="budget" aria-describedby="budget-description" name="budget" value="">
<div class="input-group-addon" role="group">
<span class="input-group-text">€</span>
</div>
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">EUR</span>
</div>
</div>
<p class="field-description" id="budget-description">Excluding VAT.</p>
</div>

Outside a field, write th:field on the sl:input-group-input itself.

When the field has errors, the input marks itself aria-invalid="true", and the group draws the destructive border and ring.

InvalidWith th:field, a field that has errors marks the input aria-invalid, and the group draws the error.
Thymeleaf source
<sl:field th:field="*{packageWeight}">
<sl:field-label>Weight</sl:field-label>
<sl:input-group>
<sl:input-group-input type="number"/>
<sl:input-group-addon align="inline-end"><sl:input-group-text>kg</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
<sl:field-error/>
</sl:field>
Rendered HTML
<div class="field" data-invalid="true">
<label class="label field-label" for="packageWeight">Weight</label>
<div class="input-group" role="group">
<input class="input-group-input" type="number" aria-invalid="true" id="packageWeight" aria-describedby="packageWeight-error" name="packageWeight" value="-2">
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">kg</span>
</div>
</div>
<div class="field-error" id="packageWeight-error">must be greater than 0</div>
</div>
DisabledA disabled input fades the whole group.
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="discount" value="10" disabled aria-label="Discount"/>
<sl:input-group-addon align="inline-end"><sl:input-group-text>%</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="discount" value="10" disabled aria-label="Discount">
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">%</span>
</div>
</div>

As a read-only input: the group gets a muted background, and the value can still be focused, selected and copied.

Read-only
Thymeleaf source
<sl:input-group>
<sl:input-group-input name="volume" value="1.5" readonly aria-label="Volume"/>
<sl:input-group-addon align="inline-end"><sl:input-group-text>l</sl:input-group-text></sl:input-group-addon>
</sl:input-group>
Rendered HTML
<div class="input-group" role="group">
<input class="input-group-input" type="text" name="volume" value="1.5" readonly aria-label="Volume">
<div class="input-group-addon" role="group" data-align="inline-end">
<span class="input-group-text">l</span>
</div>
</div>

sl:input-group declares no props; every attribute passes through.

AttributeValuesDefaultDescription
aligninline-start inline-end block-start block-endinline-startWhere the addon goes: before or after the control on its line, or above or below it across the whole group.
borderboolean—With align block-start or block-end, a dividing line between the addon and the control, with matching spacing.

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

sl:input-group-text declares no props; every attribute passes through.

AttributeValuesDefaultDescription
variantdefault outline ghost destructive secondary linkghostVisual style of the button, as on sl:button.
sizexs sm icon-xs icon-smxsSize of the button. The icon sizes are square, for a button that holds only an icon, and need an aria-label.

Every prop can also be set from an expression: th:variant="${...}". Any other attribute passes through to the rendered element, and class is appended. An aria-label or aria-labelledby is required when size is icon-xs or icon-sm; rendering fails without one.

AttributeValuesDefaultDescription
typetext email password number search tel url date datetime-local month week timetextType of the input.

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

sl:input-group-textarea declares no props; every attribute passes through.