Headless tabs component. Handles keyboard navigation (arrow keys) and selection state. Use builder pattern for custom styling.
- 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
NakedTabs– owns the selected tab id and focus handlingNakedTab– trigger button that receives a typed state snapshotNakedTabView– panel that shows/hides content based on the active tab
Information
See the complete implementation in packages/example/lib/api/naked_tabs.0.dart.
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),
);
}
}NakedTabState exposes:
tabId→ identifier for the tabisSelected→ convenience getter for selection statewidgetStates→ hover/focus/pressed/disabled flags for styling
NakedTabState.of(context)/maybeOf(context)→ obtain the nearest tab stateNakedTabState.controllerOf(context)/maybeControllerOf(context)→ reach the sharedWidgetStatesController
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
controlleror (selectedTabIdwith optionalonChanged) selectedTabId→ active tab id when managing state externallyonChanged→ handle selection changes when no controller is suppliedorientation→ horizontal (default) or vertical arrow-key handlingonEscapePressed→ optional callback when Escape is pressed while focused
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
childorbuilder builder→ValueWidgetBuilder<NakedTabState>?semanticLabel→ override accessible nameexcludeSemantics→ hide the tab and its descendants from the semantic tree- interaction callbacks:
onFocusChange,onHoverChange,onPressChange
dart
const NakedTabView({
Key? key,
required this.child,
required this.tabId,
this.maintainState = true,
})maintainStatekeeps the widget subtree alive while hidden (defaults to true)
- 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