---
title: NakedToggle
description: Headless toggle component with configurable semantics and state management
keywords: [flutter, toggle, switch, button, headless, accessibility, widgetstate]
---

Headless toggle component. Handles binary state management and accessibility with configurable semantics. Use builder pattern for custom styling.

## Choose the right control

| Interaction need | Use | Why |
| --- | --- | --- |
| Switch visible content | `NakedTabs`, `NakedTabBar`, and `NakedTabView` | Tabs own both selection and the associated visible panel. |
| Capture one exclusive form value | Flutter `RadioGroup` with `NakedRadio` | Radio semantics communicate one choice from a labeled set. |
| Run independently combinable commands | Standalone `NakedToggle`s | Each command, such as Bold, Italic, or Underline, keeps its own boolean state. |
| Preserve existing single-select segmented-button behavior | `NakedToggleGroup` with `NakedToggleOption` | The current group is a controlled, single-value compatibility component with roving focus. |

> **Selection model:** multiple/set/`toggled` mode is not shipped. `NakedToggleGroup` accepts one controlled `selectedValue`; it does not provide a multiple-selection value set or per-option toggle semantics.

Use a standalone `NakedToggle` for button-like feature switches and toolbar commands. Set `asSwitch` when the same boolean state should use switch semantics.

<Info>
  Complete runnable examples live in [`packages/example/lib/api/naked_toggle.0.dart`](https://github.com/btwld/naked_ui/blob/main/packages/example/lib/api/naked_toggle.0.dart).
</Info>

## Basic implementation

<CodeGroup title="Toggle Button" defaultLanguage="dart">
```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class ToggleButtonExample extends StatefulWidget {
  const ToggleButtonExample({super.key});

  @override
  State<ToggleButtonExample> createState() => _ToggleButtonExampleState();
}

class _ToggleButtonExampleState extends State<ToggleButtonExample> {
  bool _isBold = false;
  bool _isItalic = false;
  bool _isUnderlined = false;

  Widget _buildToggleButton({
    required IconData icon,
    required bool isSelected,
    required ValueChanged<bool> onChanged,
    required String tooltip,
  }) {
    return NakedToggle(
      value: isSelected,
      asSwitch: false, // Toggle button semantics
      onChanged: onChanged,
      semanticLabel: tooltip,
      builder: (context, state, child) {
        final backgroundColor = state.when(
          selected: Colors.blue.shade600,
          hovered: Colors.grey.shade200,
          orElse: Colors.transparent,
        );

        final border = state.when(
          focused: Border.all(color: Colors.blue, width: 2),
          orElse: Border.all(color: Colors.grey.shade300),
        );

        return AnimatedContainer(
          duration: const Duration(milliseconds: 150),
          width: 40,
          height: 40,
          decoration: BoxDecoration(
            color: backgroundColor,
            borderRadius: BorderRadius.circular(6),
            border: border,
            boxShadow: state.isPressed
                ? [
                    BoxShadow(
                      color: Colors.black.withValues(alpha: 0.1),
                      blurRadius: 2,
                      offset: const Offset(0, 1),
                    ),
                  ]
                : null,
          ),
          child: Icon(
            icon,
            color: isSelected ? Colors.white : Colors.grey.shade700,
            size: 18,
          ),
        );
      },
    );
  }

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.start,
      children: [
        const Text(
          'Text Formatting',
          style: TextStyle(
            fontSize: 18,
            fontWeight: FontWeight.bold,
          ),
        ),
        const SizedBox(height: 16),
        Row(
          mainAxisSize: MainAxisSize.min,
          children: [
            _buildToggleButton(
              icon: Icons.format_bold,
              isSelected: _isBold,
              onChanged: (value) => setState(() => _isBold = value),
              tooltip: 'Bold',
            ),
            const SizedBox(width: 8),
            _buildToggleButton(
              icon: Icons.format_italic,
              isSelected: _isItalic,
              onChanged: (value) => setState(() => _isItalic = value),
              tooltip: 'Italic',
            ),
            const SizedBox(width: 8),
            _buildToggleButton(
              icon: Icons.format_underlined,
              isSelected: _isUnderlined,
              onChanged: (value) => setState(() => _isUnderlined = value),
              tooltip: 'Underline',
            ),
          ],
        ),
      ],
    );
  }
}
```
</CodeGroup>

<Info>
  Need neutral semantics? Leave `asSwitch` at `false` for button semantics. Flip it to `true` for settings-style switches that announce "on/off" to assistive tech.
</Info>

## Toggle Builder State

The optional `builder` receives a strongly typed `NakedToggleState`:

- `isToggled` → `bool`: current boolean value
- `isHovered`, `isFocused`, `isPressed`, `isDisabled` → derived from `NakedWidgetState`
- `widgetStates` → raw `Set<WidgetState>` if you need manual resolution

If you prefer simple composition, pass a `child` instead and style it externally.

## Constructor

```dart
const NakedToggle({
  Key? key,
  required this.value,
  this.onChanged,
  this.child,
  this.enabled = true,
  this.mouseCursor,
  this.enableFeedback = true,
  this.focusNode,
  this.autofocus = false,
  this.onFocusChange,
  this.onHoverChange,
  this.onPressChange,
  this.builder,
  this.semanticLabel,
  this.asSwitch = false,
  this.excludeSemantics = false,
})
```

### Key Properties

- `value` → `bool`: the current on/off state (required)
- `onChanged` → `ValueChanged<bool>?`: called with the next value; omit to render a disabled toggle
- `builder` → `ValueWidgetBuilder<NakedToggleState>?`: tailor visuals from the typed state snapshot
- `child` → `Widget?`: static child when you do not need dynamic styling
- `asSwitch` → `bool`: `false` (button semantics) or `true` (switch semantics)
- `enabled` → disable interaction entirely when false (visuals are still rendered)
- `mouseCursor` → override the hover cursor when the toggle is enabled
- `enableFeedback` → `bool`: haptic/audio feedback on activation (default `true`)
- `focusNode`, `autofocus`: focus management options
- interaction callbacks: `onFocusChange`, `onHoverChange`, `onPressChange`
- `semanticLabel` → `String?`: accessibility label, especially important when no text label is visible
- `excludeSemantics` → `bool`: hide the toggle and its descendants from the semantic tree

## Single-select Toggle Group Compatibility

`NakedToggleGroup` preserves the existing single-select segmented-button contract. The deliberately exclusive formatting example below demonstrates that compatibility behavior; real Bold, Italic, and Underline commands normally combine and should remain standalone `NakedToggle`s as shown above.

```dart
import 'package:flutter/material.dart';
import 'package:naked_ui/naked_ui.dart';

class ToggleGroupExample extends StatelessWidget {
  const ToggleGroupExample({super.key, required this.value, required this.onChanged});

  final String value;
  final ValueChanged<String> onChanged;

  @override
  Widget build(BuildContext context) {
    return NakedToggleGroup<String>(
      selectedValue: value,
      onChanged: onChanged,
      child: Row(
        children: [
          NakedToggleOption<String>(
            value: 'bold',
            builder: (context, state, _) {
              final isCurrent = state.isSelected;
              return _SegmentChip(label: 'Bold', selected: isCurrent);
            },
          ),
          const SizedBox(width: 8),
          NakedToggleOption<String>(
            value: 'italic',
            builder: (context, state, _) {
              final isCurrent = state.isSelected;
              return _SegmentChip(label: 'Italic', selected: isCurrent);
            },
          ),
        ],
      ),
    );
  }
}

class _SegmentChip extends StatelessWidget {
  const _SegmentChip({required this.label, required this.selected});

  final String label;
  final bool selected;

  @override
  Widget build(BuildContext context) {
    return AnimatedContainer(
      duration: const Duration(milliseconds: 150),
      padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
      decoration: BoxDecoration(
        borderRadius: BorderRadius.circular(8),
        color: selected ? Colors.blue.shade600 : Colors.grey.shade200,
      ),
      child: Text(
        label,
        style: TextStyle(
          color: selected ? Colors.white : Colors.grey.shade800,
          fontWeight: FontWeight.w600,
        ),
      ),
    );
  }
}
```

### Group API

```dart
const NakedToggleGroup({
  Key? key,
  required Widget child,
  required T? selectedValue,
  ValueChanged<T?>? onChanged,
  bool enabled = true,
  Axis orientation = Axis.horizontal,
  bool loop = true,
  String? semanticLabel,
  bool excludeSemantics = false,
})
```

- `selectedValue` / `onChanged` → control the currently active option externally
- `enabled` → cascade a disabled state to every descendant option (while still rendering them)
- `orientation` → bind Left/Right arrows for horizontal groups or Up/Down arrows for vertical groups
- `loop` → wrap arrow-key focus at the first and last enabled options (default `true`)
- `semanticLabel` → identify the group to assistive technologies
- `excludeSemantics` → hide the group and its options from the semantic tree

### Option API

```dart
const NakedToggleOption({
  Key? key,
  required T value,
  Widget? child,
  ValueWidgetBuilder<NakedToggleOptionState<T>>? builder,
  bool enabled = true,
  MouseCursor? mouseCursor,
  bool enableFeedback = true,
  FocusNode? focusNode,
  bool autofocus = false,
  ValueChanged<bool>? onFocusChange,
  ValueChanged<bool>? onHoverChange,
  ValueChanged<bool>? onPressChange,
  String? semanticLabel,
  bool excludeSemantics = false,
})
```

- Provide either `child` or `builder`; builders receive the typed toggle option state
- `mouseCursor`, `enableFeedback`, and the interaction callbacks mirror the single toggle API so you can style segmented controls consistently
- `focusNode` remains caller-owned; the option manages its focusability and traversal flags while mounted
- `excludeSemantics` hides one option from the semantic tree

`NakedToggleOptionState<T>` mirrors the base state helpers and exposes:
- `value` → the option's value
- `isSelected` → convenience getter for selection state
- `widgetStates` → hover/focus/pressed/disabled flags for styling

### Access from Context

- `NakedToggleState.of(context)` / `maybeOf(context)` → read the nearest toggle state
- `NakedToggleOptionState.of<T>(context)` / `maybeOf<T>(context)` → read the nearest option state
- `NakedToggleState.controllerOf(context)` / `maybeControllerOf(context)` → obtain the shared `WidgetStatesController`

## Accessibility Notes

- Standalone keyboard: Space/Enter toggles the boolean value
- Group keyboard: one option participates in Tab traversal; Tab enters and exits the group once
- Group navigation: Left/Right follows visual direction in horizontal groups, Up/Down navigates vertical groups, and Home/End moves to the first/last enabled option
- Group selection: arrows and Home/End move focus without selecting; Space/Enter activates the focused option and proposes the next controlled value
- Group resilience: disabled options are skipped, and removing the focused option repairs focus to the next option in final visual order or its predecessor
- Screen readers: provide `semanticLabel` (and `asSwitch` when acting as a settings switch)
- Focus management: callers that supply a `FocusNode` retain ownership and must dispose it
- Visual feedback: use `state.isFocused` / `isPressed` to surface strong focus & press affordances

Need more patterns? Review the full sample app under `packages/example/lib/api` for end-to-end scenarios.
