Headless Architecture
Behavior, keyboard handling, and accessibility are separated from visuals. Swap renderers without forking logic.
Reusable Logic, Any Visual
Write behavior once - focus, keyboard, accessibility, state. Style it however you want, in every project.
Behavior, accessibility, and keyboard handling are built in. You bring the visuals.
Behavior, keyboard handling, and accessibility are separated from visuals. Swap renderers without forking logic.
Ship Material, Cupertino, or a completely custom look from the same component set. Perfect for package authors - your users restyle everything via renderers, tokens, and slots without forking your code.
Focus management, keyboard navigation, and state transitions live in headless_foundation. Semantics contracts are defined in headless_contracts and implemented by each renderer.
Customize any part of a component per-instance via slots, style overrides, or scoped themes without touching the source.
flutter pub add headless
Ready-to-use Material 3 theme out of the box. Just wrap your app and start building - all components get Material styling automatically. Switch to a custom theme later without changing any widget code.
import 'package:flutter/material.dart';
import 'package:headless/headless.dart';
void main() => runApp(const HeadlessMaterialApp(home: Demo()));
class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) {
return Scaffold(
body: Column(children: [
RTextButton(
onPressed: () {},
child: const Text('Save'),
),
RDropdownButton<String>(
items: const ['Paris', 'Berlin', 'Tokyo'],
itemAdapter: HeadlessItemAdapter.simple(
id: (v) => ListboxItemId(v),
titleText: (v) => v,
),
value: 'Paris',
onChanged: (_) {},
),
RCheckboxListTile(
value: true,
onChanged: (_) {},
title: const Text('I agree'),
),
]),
);
}
}
Same components, iOS look. Replace HeadlessMaterialApp
with HeadlessCupertinoApp
- every widget automatically renders with Cupertino styling. Your code stays the same.
import 'package:flutter/cupertino.dart';
import 'package:headless/headless.dart';
void main() => runApp(const HeadlessCupertinoApp(home: Demo()));
class Demo extends StatelessWidget {
const Demo({super.key});
@override
Widget build(BuildContext context) {
return CupertinoPageScaffold(
navigationBar: const CupertinoNavigationBar(middle: Text('Headless')),
child: SafeArea(
child: Column(children: [
RTextButton(onPressed: () {}, child: const Text('Save')),
RDropdownButton<String>(
items: const ['Paris', 'Berlin', 'Tokyo'],
itemAdapter: HeadlessItemAdapter.simple(
id: (v) => ListboxItemId(v),
titleText: (v) => v,
),
value: 'Paris',
onChanged: (_) {},
),
]),
),
);
}
}
Customize a single widget without creating a renderer. Slots let you override any visual part (surface, icon, spinner) per-instance while keeping all behavior intact.
RTextButton(
onPressed: () {},
slots: RButtonSlots(
surface: Decorate(
(ctx, child) => DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4B6FFF), Color(0xFF7B4BFF)],
),
borderRadius: BorderRadius.all(Radius.circular(12)),
),
child: child,
),
),
),
child: const Text('Upgrade'),
)
Start with Material as a base and swap renderers for specific components. Perfect when you want Material defaults but need a custom look for buttons or dropdowns in certain screens.
void main() => runApp(HeadlessMaterialApp(
home: HeadlessThemeOverridesScope(
overrides: CapabilityOverrides.build((b) {
b.set<RButtonRenderer>(MyBrandButtonRenderer());
b.set<RDropdownButtonRenderer>(MyBrandDropdownRenderer());
}),
child: const MyApp(),
),
));
Full control from scratch. Create your own theme by implementing HeadlessTheme
- register custom renderers and token resolvers for every component. The same RTextButton,
RCheckbox
widgets render with your design.
class NeonTheme extends HeadlessTheme {
final _capabilities = <Type, Object>{
RButtonRenderer: NeonButtonRenderer(),
RButtonTokenResolver: NeonButtonTokenResolver(),
RCheckboxRenderer: NeonCheckboxRenderer(),
RCheckboxTokenResolver: NeonCheckboxTokenResolver(),
};
@override
T? capability<T>() => _capabilities[T] as T?;
}
void main() => runApp(HeadlessApp(
theme: NeonTheme(),
appBuilder: (overlayBuilder) => MaterialApp(
builder: overlayBuilder,
home: const MyApp(),
),
));
// Same RTextButton, RCheckbox widgets - completely different look
| Component | Package | Description |
|---|---|---|
| Button | headless_button |
Filled, outlined, tonal, and text variants with icon and loading support |
| Checkbox | headless_checkbox |
Checkbox and checkbox list tile |
| Switch | headless_switch |
Toggle switch with interaction states |
| Dropdown | headless_dropdown_button |
Menu overlay with keyboard navigation and typeahead |
| TextField | headless_textfield |
Input field with validation and editing controllers |
| Autocomplete | headless_autocomplete |
Combobox with async sources and filtering |
Quick visual tweaks - colors, radii, spacing:
RDropdownButton<String>(
items: cities,
style: const RDropdownStyle(
menuBackgroundColor: Color(0xFFFFFFFF),
menuBorderRadius: BorderRadius.all(Radius.circular(12)),
),
// ...
)
Replace structure per-instance - wrap, decorate, or swap any part:
RTextButton(
onPressed: () {},
slots: RButtonSlots(
surface: Decorate(
(ctx, child) => DecoratedBox(
decoration: const BoxDecoration(
gradient: LinearGradient(
colors: [Color(0xFF4B6FFF), Color(0xFF7B4BFF)],
),
),
child: child,
),
),
),
child: const Text('Upgrade'),
)
Override renderers, tokens, or policies for an entire subtree:
HeadlessThemeOverridesScope(
overrides: CapabilityOverrides.build((b) {
b.set<RButtonRenderer>(MyBrandButtonRenderer());
}),
child: MyFeatureScreen(),
)
All buttons inside MyFeatureScreen will use MyBrandButtonRenderer without any per-widget changes.
| Package | Role |
|---|---|
headless |
All-in-one facade - single import for apps |
headless_foundation
|
Overlay, focus, listbox, FSM, state resolution |
headless_contracts
|
Renderer contracts and slot overrides |
headless_tokens
|
Raw + semantic design tokens (pure Dart) |
headless_theme
|
Capability-based theme runtime |
headless_material
|
Material 3 preset (renderers + tokens) |
headless_cupertino
|
Cupertino preset (renderers + tokens) |
headless_test
|
A11y, overlay, focus, keyboard test helpers |
The problem
When every team member writes custom widgets, behavior drifts: different hover/focus/disabled states, inconsistent keyboard handling, duplicated overlay logic. Headless provides contracts and mechanisms so the right path is the easy path.
headless_foundation
Headless ships with two ready-made renderer presets. Same components, different visuals:
import 'package:headless/headless.dart';
// Material look out of the box
void main() => runApp(const HeadlessMaterialApp(home: MyApp()));
import 'package:headless/headless.dart';
// iOS look - same components, different renderer
void main() => runApp(const HeadlessCupertinoApp(home: MyApp()));
Both presets use the exact same RTextButton, RDropdownButton, RCheckbox etc. - only the renderer changes.
Open Demo App - WIP, more components coming soon.
| Project | What we borrowed |
|---|---|
| React Aria | Parts/slots composition, unified press events across pointer/keyboard/assistive tech |
| Ark UI / Zag.js | Minimal FSM discipline - "impossible states are impossible" |
| Radix UI | Typed slots for point-wise overrides without full renderer rewrite |
| Downshift | Controlled/uncontrolled pattern, stateReducer for intercepting transitions |
| Angular CDK | Overlay infrastructure - OverlayRef + positioning strategies |
| Floating UI | Middleware pipeline for offset/flip/shift/arrow composition |
| Forui | FWidgetStateMap for state combination handling |
| W3C Design Tokens | Token format standardization, group inheritance ($extends) |