Skip to content

Toggle Group

Toggle group
Thymeleaf source
<sl:toggle-group type="multiple" name="textStyle" aria-label="Text style">
<sl:toggle-group-item value="bold" aria-label="Bold"><sl:icon name="bold"/></sl:toggle-group-item>
<sl:toggle-group-item value="italic" aria-label="Italic"><sl:icon name="italic"/></sl:toggle-group-item>
<sl:toggle-group-item value="underline" aria-label="Underline"><sl:icon name="underline"/></sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="group" aria-label="Text style">
<label class="toggle toggle-group-item">
<input class="toggle-input" type="checkbox" name="textStyle" value="bold" aria-label="Bold">
<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 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"></path>
</svg>
</label>
<label class="toggle toggle-group-item">
<input class="toggle-input" type="checkbox" name="textStyle" value="italic" aria-label="Italic">
<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">
<line x1="19" x2="10" y1="4" y2="4"></line>
<line x1="14" x2="5" y1="20" y2="20"></line>
<line x1="15" x2="9" y1="4" y2="20"></line>
</svg>
</label>
<label class="toggle toggle-group-item">
<input class="toggle-input" type="checkbox" name="textStyle" value="underline" aria-label="Underline">
<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 4v6a6 6 0 0 0 12 0V4"></path>
<line x1="4" x2="20" y1="20" y2="20"></line>
</svg>
</label>
</div>

The items of a toggle group are toggles around radio buttons (type="single", the default: one item is on at a time) or checkboxes (type="multiple": any number). The form submits them as such inputs, and they work without JavaScript. In a single group the arrow keys move between the items and Tab leaves the group, as in any group of radio buttons; in a multiple group every item is a tab stop.

Name the group with aria-label, or with aria-labelledby pointing at its label. Give each item a value; an item that holds only an icon needs an aria-label.

Outline
Thymeleaf source
<sl:toggle-group variant="outline" name="callFilter" aria-label="Calls">
<sl:toggle-group-item value="all" checked>All</sl:toggle-group-item>
<sl:toggle-group-item value="missed">Missed</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" aria-label="Calls">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="callFilter" value="all" checked>
All
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="callFilter" value="missed">
Missed
</label>
</div>

Write th:field on the group, once. The items share it: Spring gives them the field’s name, numbers their ids (layout1, layout2, …) and checks the one whose value matches the field. Without a form object, name on the group gives the items their name. A single group with neither gets a generated name, so its items still exclude each other; give it a name when the form should submit it.

In a formth:field on the group: the items are radio buttons that share it, and the one with the field's value is on.
Thymeleaf source
<p id="layout-label">Layout</p>
<sl:toggle-group th:field="*{layout}" variant="outline" aria-labelledby="layout-label">
<sl:toggle-group-item value="list"><sl:icon name="list"/> List</sl:toggle-group-item>
<sl:toggle-group-item value="grid"><sl:icon name="layout-grid"/> Grid</sl:toggle-group-item>
<sl:toggle-group-item value="table"><sl:icon name="table-2"/> Table</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<p id="layout-label">Layout</p>
<div class="toggle-group" role="radiogroup" aria-labelledby="layout-label">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" value="list" id="layout1" name="layout">
<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 5h.01"></path>
<path d="M3 12h.01"></path>
<path d="M3 19h.01"></path>
<path d="M8 5h13"></path>
<path d="M8 12h13"></path>
<path d="M8 19h13"></path>
</svg>
List
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" value="grid" id="layout2" name="layout" checked>
<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="7" height="7" x="3" y="3" rx="1"></rect>
<rect width="7" height="7" x="14" y="3" rx="1"></rect>
<rect width="7" height="7" x="14" y="14" rx="1"></rect>
<rect width="7" height="7" x="3" y="14" rx="1"></rect>
</svg>
Grid
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" value="table" id="layout3" name="layout">
<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 9h18"></path>
<path d="M9 3v18"></path>
<rect x="3" y="3" width="18" height="18" rx="2"></rect>
</svg>
Table
</label>
</div>

In a field set with th:field, the group takes the field set’s, and the legend labels it.

With type="multiple", bind a collection (a List, a Set, an array). Spring checks every item whose value is in it, and adds a hidden marker input, so a form with no item on clears the collection.

Multipletype="multiple" makes the items checkboxes: th:field binds a collection, and Spring's hidden marker input lets the form clear it.
Thymeleaf source
<sl:toggle-group type="multiple" th:field="*{workdays}" variant="outline" aria-label="Workdays">
<sl:toggle-group-item value="MONDAY">Mon</sl:toggle-group-item>
<sl:toggle-group-item value="TUESDAY">Tue</sl:toggle-group-item>
<sl:toggle-group-item value="WEDNESDAY">Wed</sl:toggle-group-item>
<sl:toggle-group-item value="THURSDAY">Thu</sl:toggle-group-item>
<sl:toggle-group-item value="FRIDAY">Fri</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="group" aria-label="Workdays">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="checkbox" value="MONDAY" id="workdays1" name="workdays" checked>
<input type="hidden" name="_workdays" value="on">
Mon
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="checkbox" value="TUESDAY" id="workdays2" name="workdays" checked>
<input type="hidden" name="_workdays" value="on">
Tue
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="checkbox" value="WEDNESDAY" id="workdays3" name="workdays">
<input type="hidden" name="_workdays" value="on">
Wed
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="checkbox" value="THURSDAY" id="workdays4" name="workdays" checked>
<input type="hidden" name="_workdays" value="on">
Thu
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="checkbox" value="FRIDAY" id="workdays5" name="workdays">
<input type="hidden" name="_workdays" value="on">
Fri
</label>
</div>
Sizes
Thymeleaf source
<sl:toggle-group size="sm" variant="outline" spacing="0" name="positionSm" aria-label="Position, small">
<sl:toggle-group-item value="top" checked>Top</sl:toggle-group-item>
<sl:toggle-group-item value="bottom">Bottom</sl:toggle-group-item>
</sl:toggle-group>
<sl:toggle-group variant="outline" spacing="0" name="positionDefault" aria-label="Position">
<sl:toggle-group-item value="top" checked>Top</sl:toggle-group-item>
<sl:toggle-group-item value="bottom">Bottom</sl:toggle-group-item>
</sl:toggle-group>
<sl:toggle-group size="lg" variant="outline" spacing="0" name="positionLg" aria-label="Position, large">
<sl:toggle-group-item value="top" checked>Top</sl:toggle-group-item>
<sl:toggle-group-item value="bottom">Bottom</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" data-spacing="0" aria-label="Position, small">
<label class="toggle toggle-group-item" data-variant="outline" data-size="sm">
<input class="toggle-input" type="radio" name="positionSm" value="top" checked>
Top
</label>
<label class="toggle toggle-group-item" data-variant="outline" data-size="sm">
<input class="toggle-input" type="radio" name="positionSm" value="bottom">
Bottom
</label>
</div>
<div class="toggle-group" role="radiogroup" data-spacing="0" aria-label="Position">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="positionDefault" value="top" checked>
Top
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="positionDefault" value="bottom">
Bottom
</label>
</div>
<div class="toggle-group" role="radiogroup" data-spacing="0" aria-label="Position, large">
<label class="toggle toggle-group-item" data-variant="outline" data-size="lg">
<input class="toggle-input" type="radio" name="positionLg" value="top" checked>
Top
</label>
<label class="toggle toggle-group-item" data-variant="outline" data-size="lg">
<input class="toggle-input" type="radio" name="positionLg" value="bottom">
Bottom
</label>
</div>

spacing sets the gap between the items, in Tailwind’s spacing steps: 2 (0.5rem) by default, 0 to join them into one control.

Spacingspacing="0" joins the items into one control; the default is 2.
Thymeleaf source
<sl:toggle-group variant="outline" spacing="0" name="alignment" aria-label="Alignment">
<sl:toggle-group-item value="left" checked aria-label="Align left"><sl:icon name="align-left"/></sl:toggle-group-item>
<sl:toggle-group-item value="center" aria-label="Align center"><sl:icon name="align-center"/></sl:toggle-group-item>
<sl:toggle-group-item value="right" aria-label="Align right"><sl:icon name="align-right"/></sl:toggle-group-item>
</sl:toggle-group>
<sl:toggle-group variant="outline" spacing="4" name="alignmentSpaced" aria-label="Alignment, spaced">
<sl:toggle-group-item value="left" checked aria-label="Align left"><sl:icon name="align-left"/></sl:toggle-group-item>
<sl:toggle-group-item value="center" aria-label="Align center"><sl:icon name="align-center"/></sl:toggle-group-item>
<sl:toggle-group-item value="right" aria-label="Align right"><sl:icon name="align-right"/></sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" data-spacing="0" aria-label="Alignment">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignment" value="left" checked aria-label="Align left">
<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 5H3"></path>
<path d="M15 12H3"></path>
<path d="M17 19H3"></path>
</svg>
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignment" value="center" aria-label="Align center">
<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 5H3"></path>
<path d="M17 12H7"></path>
<path d="M19 19H5"></path>
</svg>
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignment" value="right" aria-label="Align right">
<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 5H3"></path>
<path d="M21 12H9"></path>
<path d="M21 19H7"></path>
</svg>
</label>
</div>
<div class="toggle-group" role="radiogroup" data-spacing="4" aria-label="Alignment, spaced">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignmentSpaced" value="left" checked aria-label="Align left">
<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 5H3"></path>
<path d="M15 12H3"></path>
<path d="M17 19H3"></path>
</svg>
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignmentSpaced" value="center" aria-label="Align center">
<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 5H3"></path>
<path d="M17 12H7"></path>
<path d="M19 19H5"></path>
</svg>
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="alignmentSpaced" value="right" aria-label="Align right">
<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 5H3"></path>
<path d="M21 12H9"></path>
<path d="M21 19H7"></path>
</svg>
</label>
</div>
Vertical
Thymeleaf source
<sl:toggle-group orientation="vertical" variant="outline" spacing="0" name="rowDensity" aria-label="Density">
<sl:toggle-group-item value="compact">Compact</sl:toggle-group-item>
<sl:toggle-group-item value="comfortable" checked>Comfortable</sl:toggle-group-item>
<sl:toggle-group-item value="spacious">Spacious</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" data-spacing="0" data-orientation="vertical" aria-label="Density">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="rowDensity" value="compact">
Compact
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="rowDensity" value="comfortable" checked>
Comfortable
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="rowDensity" value="spacious">
Spacious
</label>
</div>

A field with errors marks every item aria-invalid="true".

InvalidWith th:field, a field that has errors marks every item aria-invalid by itself.
Thymeleaf source
<sl:toggle-group th:field="*{shippingSpeed}" variant="outline" aria-label="Shipping">
<sl:toggle-group-item value="standard">Standard</sl:toggle-group-item>
<sl:toggle-group-item value="express">Express</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" aria-label="Shipping">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" aria-invalid="true" value="standard" id="shippingSpeed1" name="shippingSpeed">
Standard
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" aria-invalid="true" value="express" id="shippingSpeed2" name="shippingSpeed">
Express
</label>
</div>
Disableddisabled on the group disables every item; on an item, only that one.
Thymeleaf source
<sl:toggle-group type="multiple" disabled name="textStyleLocked" aria-label="Text style (locked)">
<sl:toggle-group-item value="bold" checked aria-label="Bold"><sl:icon name="bold"/></sl:toggle-group-item>
<sl:toggle-group-item value="italic" aria-label="Italic"><sl:icon name="italic"/></sl:toggle-group-item>
</sl:toggle-group>
<sl:toggle-group variant="outline" name="billingPeriod" aria-label="Plan">
<sl:toggle-group-item value="monthly" checked>Monthly</sl:toggle-group-item>
<sl:toggle-group-item value="yearly" disabled>Yearly</sl:toggle-group-item>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="group" aria-label="Text style (locked)">
<label class="toggle toggle-group-item">
<input class="toggle-input" type="checkbox" name="textStyleLocked" value="bold" checked aria-label="Bold" disabled>
<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 12h9a4 4 0 0 1 0 8H7a1 1 0 0 1-1-1V5a1 1 0 0 1 1-1h7a4 4 0 0 1 0 8"></path>
</svg>
</label>
<label class="toggle toggle-group-item">
<input class="toggle-input" type="checkbox" name="textStyleLocked" value="italic" aria-label="Italic" disabled>
<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">
<line x1="19" x2="10" y1="4" y2="4"></line>
<line x1="14" x2="5" y1="20" y2="20"></line>
<line x1="15" x2="9" y1="4" y2="20"></line>
</svg>
</label>
</div>
<div class="toggle-group" role="radiogroup" aria-label="Plan">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="billingPeriod" value="monthly" checked>
Monthly
</label>
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="billingPeriod" value="yearly" disabled>
Yearly
</label>
</div>

Put an item in a tooltip. The group still joins the items, the tooltip shows on hover and when the keyboard focuses the item, and its text describes the item’s input. An icon-only item already has its aria-label: give the sl:tooltip mode="label", so a screen reader does not say it twice.

With tooltipsA sl:tooltip around an item. The tooltip shows next to the item, on hover and on keyboard focus; mode="label" because it repeats the item's aria-label.
Thymeleaf source
<sl:toggle-group variant="outline" spacing="0" name="tooltipAlign" aria-label="Alignment with tooltips">
<sl:tooltip mode="label">
<sl:toggle-group-item value="left" checked aria-label="Align left"><sl:icon name="align-left"/></sl:toggle-group-item>
<sl:tooltip-content>Align left</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip mode="label">
<sl:toggle-group-item value="center" aria-label="Align center"><sl:icon name="align-center"/></sl:toggle-group-item>
<sl:tooltip-content>Align center</sl:tooltip-content>
</sl:tooltip>
<sl:tooltip mode="label">
<sl:toggle-group-item value="right" aria-label="Align right"><sl:icon name="align-right"/></sl:toggle-group-item>
<sl:tooltip-content>Align right</sl:tooltip-content>
</sl:tooltip>
</sl:toggle-group>
Rendered HTML
<div class="toggle-group" role="radiogroup" data-spacing="0" aria-label="Alignment with tooltips">
<span class="tooltip" x-data="slTooltip">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="tooltipAlign" value="left" checked aria-label="Align left">
<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 5H3"></path>
<path d="M15 12H3"></path>
<path d="M17 19H3"></path>
</svg>
</label>
<span class="tooltip-content" role="tooltip" popover="manual" aria-hidden="true">
Align left
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="tooltipAlign" value="center" aria-label="Align center">
<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 5H3"></path>
<path d="M17 12H7"></path>
<path d="M19 19H5"></path>
</svg>
</label>
<span class="tooltip-content" role="tooltip" popover="manual" aria-hidden="true">
Align center
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
<span class="tooltip" x-data="slTooltip">
<label class="toggle toggle-group-item" data-variant="outline">
<input class="toggle-input" type="radio" name="tooltipAlign" value="right" aria-label="Align right">
<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 5H3"></path>
<path d="M21 12H9"></path>
<path d="M21 19H7"></path>
</svg>
</label>
<span class="tooltip-content" role="tooltip" popover="manual" aria-hidden="true">
Align right
<span class="tooltip-arrow" aria-hidden="true"></span>
</span>
</span>
</div>
AttributeValuesDefaultDescription
typesingle multiplesinglesingle: one item is on at a time, as radio buttons (the arrow keys move between them). multiple: any number, as checkboxes.
variantdefault outlinedefaultVisual style of the items.
sizesm default lgdefaultSize of the items.
spacing0 1 2 3 42The gap between the items, in Tailwind's spacing steps (2 is 0.5rem). 0 joins them into one control.
orientationhorizontal verticalhorizontalPut the items on a row, or in a column.
namestring—The name every item submits under, for a group without th:field. A single group without either gets a generated one, so its items still exclude each other.
disabledboolean—Disable every item.

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:toggle-group-item declares no props; every attribute passes through.