Skip to content

feat(switch): Switch primitive (shadcn v4) + gallery showcase#52

Merged
SiphoChris merged 5 commits into
mainfrom
feat/flutterbits-switch
Jun 15, 2026
Merged

feat(switch): Switch primitive (shadcn v4) + gallery showcase#52
SiphoChris merged 5 commits into
mainfrom
feat/flutterbits-switch

Conversation

@SiphoChris

Copy link
Copy Markdown
Owner

Switch — the canonical interactive + animated toggle (5th primitive)

Ships Switch at apps/gallery/lib/components/ui/switch.dart — a Material-free, themeable, shadcn-v4-faithful toggle. It's the repo's first animated component: it mirrors Button's interaction scaffolding (FocusableActionDetector + tap detector, Space/Enter activation, focus-visible ring, MinTapTarget, Semantics) and adds a dependency-free sliding-thumb animation. It also unblocks the structure layer's ThemeToggle.

shadcn v4 class mapping

shadcn flutterbits
track rounded-full border-transparent, checked:bg-primary / unchecked:bg-input .tw.bg(<lerped>).roundedFull.border(1, color: Color(0x00000000)), color = Color.lerp(input, primary, t)
thumb bg-background size-4 (default) / size-3 (sm) .tw.size(4/3).bg(background).roundedFull
translate-x-0translate-x-[calc(100%-2px)] Align lerping AlignmentDirectional.centerStartcenterEnd inside a .px(0.5) inset
data-[size=default] / data-[size=sm] SwitchSize { md, sm } (md = shadcn default)
focus-visible:ring-[3px] ring-ring/50 .ring(3, color: ring.withValues(alpha: 0.5)) when focus-visible
disabled:opacity-50 pointer-events-none .opacity(0.5) + non-interactive

Design decisions

  • Controlled (like shadcn/Radix) — the parent owns value; tap/Space/Enter calls onChanged(!value). The widget holds no value state, only the toggle animation. onChanged == null ⇒ disabled (the Flutter idiom).
  • Dependency-free animation — a 200ms easeInOut AnimationController drives one t that lerps both the track color and the thumb position via a single AnimatedBuilder (the static thumb is passed as the builder's child so it isn't rebuilt per frame). No flutter_animate — a state toggle is a built-in implicit animation, so a copied Switch drags in nothing.
  • Two sizes sm/md (shadcn parity), via an exhaustive switch geometry record.
  • AccessibilitySemantics(toggled: value, enabled:, label:) (a switch role announcing on/off); MinTapTarget gives a ≥48px touch area around the ~18px visual (mobile-first).

Faithful deviations (documented, not bugs)

shadow-xs and the dark-only refinements (dark:…unchecked:bg-input/80, the per-state dark thumb colors) are dropped — the thumb is background in both brightnesses (faithful in light; the "on" state stays high-contrast in dark). Same precedent as Badge's/Input's dropped dark: refinements. Note: Switch's focus ring is ring-[3px] with no ring-offset (matching its shadcn class) — unlike Button, whose class has ring-offset-2.

Tests

  • 13 behavior tests: tap on↔off; track color off=input/on=primary; md wider than sm; animation settles (no pending timers); Space + Enter activation; focus-visible ring appears; disabled (non-interactive + dimmed); ≥48px MinTapTarget; toggled semantics (Tristate) + label; RTL; dark reskin.
  • Golden grid (off/on × sm/md + disabled pair, light/dark/RTL).
  • Rendered in apps/gallery (a small stateful Switches demo) + smoke-tested. Charter §8 progress note.

Goldens are provisional (Windows); re-baselined from CI Linux if the gallery job diffs (Badge & Input needed none).

Executed via writing-plans → subagent-driven-development (implementer + spec-review + code-quality-review; review findings triaged — dispose the CurvedAnimation, AnimatedBuilder child idiom, a focus-ring test added; the reviewer's ring-offset and excludeSemantics suggestions were verified against the shadcn class + the no-text-child structure and correctly not applied).

🤖 Generated with Claude Code

Co-Authored-By: Claude Opus 4.8 (1M context) noreply@anthropic.com

Sipho Nkebe added 5 commits June 15, 2026 14:57
Controlled animated toggle (TDD): switch.dart + switch_behavior_test.dart.

All 12 behavior tests pass: tap toggles on/off, off=input/on=primary track
color, md wider than sm, animation settles, Space/Enter keyboard toggle,
disabled dimmed + non-interactive, >=48px hit target (MinTapTarget),
toggled semantics + Wi-Fi label, RTL no overflow, dark reskin.

SDK deviation: SemanticsFlags.isToggled is Tristate in Flutter 3.41.9
(no hasToggledState getter); test uses Tristate.isTrue comparison.
@vercel

vercel Bot commented Jun 15, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
flutterbits Ready Ready Preview, Comment Jun 15, 2026 1:20pm

@SiphoChris SiphoChris merged commit c4860ad into main Jun 15, 2026
8 checks passed
@SiphoChris SiphoChris deleted the feat/flutterbits-switch branch June 15, 2026 13:23
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant