Headless
Logoheadless

Headless

Reusable Logic, Any Visual

Write behavior once - focus, keyboard, accessibility, state. Style it however you want, in every project.

Behavior
M
Material UI
C
Cupertino UI
1
Custom UI 1
2
Custom UI 2
3
Custom UI 3
slots a11y tokens focus

Highlights

Behavior, accessibility, and keyboard handling are built in. You bring the visuals.

🧩

Headless Architecture

Behavior, keyboard handling, and accessibility are separated from visuals. Swap renderers without forking logic.

🎨

Fully Customizable

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.

⌨️

Keyboard & A11y

Focus management, keyboard navigation, and state transitions live in headless_foundation. Semantics contracts are defined in headless_contracts and implemented by each renderer.

🔧

Slots & Overrides

Customize any part of a component per-instance via slots, style overrides, or scoped themes without touching the source.

Install#

flutter pub add headless

View on pub.dev

Quick Start#

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

Components#

ComponentPackageDescription
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

Customization#

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.

Packages#

PackageRole
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

Why Headless?#

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.

  • One component, many brands - swap renderers, tokens, or the entire theme without forking
  • Edge cases handled once - focus traps, nested overlays, dismiss-on-outside-click live in headless_foundation
  • Predictable by design - POLA is enforced through controlled/uncontrolled models and explicit state priorities
  • Testable behavior - test state transitions, callbacks, a11y semantics, and keyboard scenarios without pixel matching

Built-in Presets#

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.

Inspired By#

ProjectWhat 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)

Learn More#