Icon Mode Toggle
A compact icon-only segmented mode picker. Each entry renders as a square icon button; switching slides a shared highlight between segments via layoutId. Built for header strips, toolbars, and reader chrome where text labels would crowd the layout.
Customize
Shape
2
State
0
Installation
npx shadcn@latest add https://craftbits.dev/r/icon-mode-toggle.jsonUsage
import { IconModeToggle } from "@craft-bits/core";
import { ListTree, Square } from "lucide-react";
<IconModeToggle
aria-label="Display mode"
defaultValue="continuous"
modes={[
{ value: "continuous", label: "Scroll mode", icon: <ListTree /> },
{ value: "block", label: "Focus mode", icon: <Square /> },
]}
/>Controlled — pair value with onChange:
const [value, setValue] = useState("continuous");
<IconModeToggle
aria-label="Display mode"
value={value}
onChange={setValue}
modes={[
{ value: "continuous", label: "Scroll mode", icon: <ListTree /> },
{ value: "block", label: "Focus mode", icon: <Square /> },
]}
/>Understanding the component
- Icon-only by design. Where
ModeTogglesurfaces a label per segment,IconModeTogglecollapses to just the glyph. Thelabelfield is still required — it becomes each segment'saria-labelandtitle, so the action stays announceable and discoverable on hover. - Shared moving indicator. Only the active segment renders a
motion.spanwith alayoutIdscoped to this instance viauseId. Motion auto-tweens the highlight from the previous segment's bounds to the new one —SPRINGS.smoothdoes the easing. - Layout-group isolation. The whole toggle is wrapped in a
<LayoutGroup>keyed by the same scope, so multipleIconModeToggles on a page never collide onlayoutId. - Controlled + uncontrolled.
value+onChangemirrors your state;defaultValuelets the component manage its own. Same pattern as Radixvalue/defaultValue. - Keyboard model. Arrow Left / Up / Right / Down cycle focus through segments with wrap-around; Home / End jump to the ends. Selection only changes on click, Space, or Enter.
- State hooks. Each segment exposes
data-state="on" | "off", plusdata-valuecarrying its id, so CSS and assistive tech can both observe the active mode.
Props
| Prop | Type | Default | Description |
|---|---|---|---|
modes | IconModeToggleMode[] | required | Ordered list of mode definitions: value, icon, label. |
value | string | — | Controlled active mode value. Pair with onChange. |
defaultValue | string | first mode | Uncontrolled initial active mode value. |
onChange | (value: string) => void | — | Fired when the active mode changes. |
disabled | boolean | false | Disables every segment. |
aria-label | string | — | Accessible name for the group. |
className | string | — | Merged onto the rendered group <div>. |
Accessibility
- The toggle renders
<div role="group">. Passaria-labelso the group is announced. - Each segment is a real
<button>witharia-pressed/aria-checkedreflecting its active state, plusdata-state="on" | "off"for styling hooks. - Because no text label is visible, each mode's
labelbecomes itsaria-labelandtitleso the action stays announceable and surfaces on hover. - Arrow Left / Up / Right / Down cycle focus through segments with wrap-around. Home / End jump to the first / last.
- Focus is visible via a
focus-visible:ring keyed to--cb-accent. - Color contrast: active glyph sits on
--cb-bgagainst the muted track; inactive glyphs use--cb-fg-mutedand elevate to--cb-fgon hover — both pass WCAG AA in the default theme. - The animated indicator respects
prefers-reduced-motion: Motion'slayoutIdtransition collapses to an instant swap.
Credits
- Extracted from:
algoflashcards(src/platform/ui/IconModeToggle.tsx). The source was wired to a project-specificuseLessonDisplayModehook and a fixedListTree/Squareicon pair; craft-bits generalises it into a typed icon-mode picker with controlled+uncontrolled state, keyboard navigation, and token-driven styling.