Toggle Group
Group optional pressed segments with a declared selection policy and keyboard navigation.
Segments and selection policy
Use ToggleGroup for related pressed actions or view segments. This maps the segmented-button family to this library’s web toggle group. The current styling permits wrapping separated rounded segments; it does not claim native Material geometry.
multiple=false permits at most one pressed item and can still permit none. Use RadioGroup or an explicit consumer rule when exactly one choice is required. multiple=true permits independent pressed members. Switching policy must reconcile the retained array deliberately.
Anatomy and specifications
| Part | Contract |
|---|---|
| Root | Named group, HTMLDivElement ref, horizontal default |
| Members | Native toggle buttons, stable unique string values |
| Target | Minimum 44px height; wrapping text and 4px group gaps |
| Shape | Each segment --radius-md so reflow retains usable boundaries |
| Orientation | vertical lays out a column and changes primitive navigation |
| State | Pressed members expose actual aria-pressed; disabled members remain unavailable |
Working local content segments
Local title section
Pressed local sections: title. At most one; empty is allowed.
'use client';
import { useState } from 'react';
import { Button, Toggle, ToggleGroup, ToggleGroupItem } from '@mobtranslate/ui';
export function ToggleSelectionExample() {
const [pressed, setPressed] = useState(false);
return <div data-toggle-lab className="space-y-4"><Toggle pressed={pressed} onPressedChange={setPressed} className={state => state.pressed ? 'local-toggle-on' : 'local-toggle-off'}>Emphasize local example</Toggle><p style={{ fontWeight: pressed ? 700 : 400 }}>This English-only sample changes its actual local emphasis.</p><Toggle disabled>Unavailable emphasis</Toggle><p role="status">Local emphasis {pressed ? 'on' : 'off'}.</p></div>;
}
export function ToggleGroupSelectionExample() {
const [multiple, setMultiple] = useState(false);
const [value, setValue] = useState<string[]>(['title']);
const [vertical, setVertical] = useState(false);
return <div data-toggle-group-lab className="space-y-4">
<Button type="button" variant="outline" aria-pressed={multiple} onClick={() => { setMultiple(current => !current); setValue([]); }}>Allow multiple local segments</Button>
<Button type="button" variant="outline" aria-pressed={vertical} onClick={() => setVertical(current => !current)}>Use vertical local segments</Button>
<ToggleGroup multiple={multiple} value={value} onValueChange={setValue} orientation={vertical ? 'vertical' : 'horizontal'} aria-label="Local reading sections" className={state => state.multiple ? 'local-segments-multiple' : 'local-segments-single'}>
<ToggleGroupItem value="title">Title section</ToggleGroupItem><ToggleGroupItem value="notes">Notes section</ToggleGroupItem><ToggleGroupItem value="unavailable" disabled>Unavailable section</ToggleGroupItem>
</ToggleGroup>
<div data-local-sections>{value.includes('title') && <h3 className="font-semibold">Local title section</h3>}{value.includes('notes') && <p>English-only local notes section.</p>}{value.length === 0 && <p>No local sections selected.</p>}</div>
<p role="status">Pressed local sections: {value.join(', ') || 'none'}. {multiple ? 'Multiple choices permitted.' : 'At most one; empty is allowed.'}</p>
</div>;
}
API reference
| Export / prop | Contract |
|---|---|
| ToggleGroup / ToggleGroupProps | value/defaultValue readonly arrays; onValueChange(array, details). |
| multiple | false default; true permits several pressed values. |
| orientation / loopFocus | horizontal / true defaults; vertical and non-looping navigation are installed alternatives. |
| disabled | false default; root unavailable state applies to members. |
| ToggleGroupItem | Installed Toggle props/ref; value identifies a member; native button/render props and disabled supported. |
| Refs / state callbacks | Group div and item button refs; group state includes multiple/disabled; item state includes pressed/disabled. |
| Form data | No automatic checkbox/radio payload. Serialize intended values in the consuming form. |
| className / style / render / native props | Installed primitive props; styled wrappers retain string-or-state class callbacks. A custom render must preserve semantics, events and refs. |
Keyboard and recovery
- Arrow navigation follows the declared orientation and primitive direction; Tab enters/leaves the composite group. Activation and focus are different states.
- Skip unavailable items and retain a meaningful focus destination if selected members disappear.
- Keep group/member names stable and describe the actual displayed content. The example changes real local sections, not only button color.
- For remote filtering, retain applied/query state and expose loading, empty, error and retry results separately.
- Changing selection policy clears this local example’s array explicitly; a real application must reconcile it according to its task.
Failure cases
- Assuming single mode requires exactly one selection.
- Duplicate values, unnamed groups, or arrow navigation trapped on unavailable members.
- Vertical behavior with a horizontal-only layout.
- Form submission that silently omits the selected segments.
- Pressed color asserted as review approval or successful data filtering.
Verification and release scope
- Check actual names, roles, state and focus with pointer and keyboard. Inspect the retained value and submitted FormData when relevant.
- Exercise empty, selected, mixed, disabled, read-only, invalid and pending/recovery states that apply to the control. Styling alone does not prove enforcement.
- Check both themes, long labels, 320px reflow, meaningful graphic/text contrast, reduced motion, forced colors and the actual target device.
- Use a named screen-reader/browser pair to verify spoken state. DOM assertions and local examples do not prove announcements or backend persistence.
- Selection never establishes language quality, source authority, consent, qualified review or permission. Keep that evidence explicit in the product task.