---
title: NakedTabs
description: Headless tab primitives with roving focus and typed state builders
keywords: [flutter, tabs, tablist, headless, focus, accessibility]
---

Headless tabs component. Handles keyboard navigation (arrow keys) and selection state. Use builder pattern for custom styling.

## When to use this

- **Content organization**: Switch between related content sections
- **Settings panels**: Group settings into categorized tabs
- **Data views**: Toggle between different data representations
- **Navigation**: Section navigation within a single page

## Components

- `NakedTabs` – owns the selected tab id and focus handling
- `NakedTab` – trigger button that receives a typed state snapshot  
- `NakedTabView` – panel that shows/hides content based on the active tab

<Info>
  See the complete implementation in [`packages/example/lib/api/naked_tabs.0.dart`](https://github.com/btwld/naked_ui/blob/main/packages/example/lib/api/naked_tabs.0.dart).
</Info>

## Basic implementation

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

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

  @override
  State<TabsExample> createState() => _TabsExampleState();
}

class _TabsExampleState extends State<TabsExample> {
  String _tab = 'overview';

  @override
  Widget build(BuildContext context) {
    return NakedTabs(
      selectedTabId: _tab,
      onChanged: (id) => setState(() => _tab = id),
      child: Column(
        mainAxisSize: MainAxisSize.min,
        children: [
          Container(
            padding: const EdgeInsets.all(8),
            decoration: BoxDecoration(
              color: Colors.grey.shade100,
              borderRadius: BorderRadius.circular(12),
            ),
            child: NakedTabBar(
              child: Row(
                mainAxisSize: MainAxisSize.min,
                children: const [
                  _TabChip(tabId: 'overview', label: 'Overview'),
                  SizedBox(width: 8),
                  _TabChip(tabId: 'usage', label: 'Usage'),
                  SizedBox(width: 8),
                  _TabChip(tabId: 'api', label: 'API'),
                ],
              ),
            ),
          ),
          const SizedBox(height: 16),
          NakedTabView(
            tabId: 'overview',
            child: _PanelBox(
              color: Colors.blue.shade50,
              text: 'High-level description lives here.',
            ),
          ),
          NakedTabView(
            tabId: 'usage',
            child: _PanelBox(
              color: Colors.green.shade50,
              text: 'Usage guidance, best practices, etc.',
            ),
          ),
          NakedTabView(
            tabId: 'api',
            child: _PanelBox(
              color: Colors.orange.shade50,
              text: 'Detailed API summary for devs.',
            ),
          ),
        ],
      ),
    );
  }
}

class _TabChip extends StatelessWidget {
  const _TabChip({required this.tabId, required this.label});

  final String tabId;
  final String label;

  @override
  Widget build(BuildContext context) {
    return NakedTab(
      tabId: tabId,
      builder: (context, state, _) {
        final bool isFocused = state.isFocused;

        final backgroundColor = state.when(
          selected: Colors.white,
          hovered: Colors.grey.shade200,
          orElse: Colors.transparent,
        );

        return AnimatedContainer(
          duration: const Duration(milliseconds: 140),
          padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10),
          decoration: BoxDecoration(
            borderRadius: BorderRadius.circular(8),
            color: backgroundColor,
            border: Border.all(
              color: state.when(
                selected: Colors.blue,
                focused: Colors.blue.shade300,
                orElse: Colors.grey.shade300,
              ),
              width: isFocused ? 2 : 1,
            ),
          ),
          child: Text(
            label,
            style: TextStyle(
              fontWeight: FontWeight.w600,
              color: state.when(
                selected: Colors.blue.shade800,
                orElse: Colors.grey.shade700,
              ),
            ),
          ),
        );
      },
    );
  }
}

class _PanelBox extends StatelessWidget {
  const _PanelBox({required this.color, required this.text});

  final Color color;
  final String text;

  @override
  Widget build(BuildContext context) {
    return Container(
      width: double.infinity,
      padding: const EdgeInsets.all(16),
      decoration: BoxDecoration(
        color: color,
        borderRadius: BorderRadius.circular(12),
      ),
      child: Text(text),
    );
  }
}
```

## Typed Tab State

`NakedTabState` exposes:

- `tabId` → identifier for the tab
- `isSelected` → convenience getter for selection state
- `widgetStates` → hover/focus/pressed/disabled flags for styling

### Access from Context

- `NakedTabState.of(context)` / `maybeOf(context)` → obtain the nearest tab state
- `NakedTabState.controllerOf(context)` / `maybeControllerOf(context)` → reach the shared `WidgetStatesController`

## Constructors

### NakedTabs

```dart
const NakedTabs({
  Key? key,
  required this.child,
  this.controller,
  this.selectedTabId,
  this.onChanged,
  this.orientation = Axis.horizontal,
  this.enabled = true,
  this.onEscapePressed,
})
```

- Provide either `controller` or (`selectedTabId` with optional `onChanged`)
- `selectedTabId` → active tab id when managing state externally
- `onChanged` → handle selection changes when no controller is supplied
- `orientation` → horizontal (default) or vertical arrow-key handling
- `onEscapePressed` → optional callback when Escape is pressed while focused

### NakedTab

```dart
const NakedTab({
  Key? key,
  this.child,
  required this.tabId,
  this.enabled = true,
  this.mouseCursor = SystemMouseCursors.click,
  this.enableFeedback = true,
  this.focusNode,
  this.autofocus = false,
  this.onFocusChange,
  this.onHoverChange,
  this.onPressChange,
  this.builder,
  this.semanticLabel,
  this.excludeSemantics = false,
})
```

- Provide either `child` or `builder`
- `builder` → `ValueWidgetBuilder<NakedTabState>?`
- `semanticLabel` → override accessible name
- `excludeSemantics` → hide the tab and its descendants from the semantic tree
- interaction callbacks: `onFocusChange`, `onHoverChange`, `onPressChange`

### NakedTabView

```dart
const NakedTabView({
  Key? key,
  required this.child,
  required this.tabId,
  this.maintainState = true,
})
```

- `maintainState` keeps the widget subtree alive while hidden (defaults to true)

## Accessibility Guidance

- Tabs expose button semantics; Tab/Shift+Tab traverse groups
- Arrow keys move between tabs (horizontal or vertical based on `orientation`)
- Provide strong focus states via `tabState.isFocused`
- Each panel should contain headings or text to clarify context for screen readers
