easy_expandable

pub package license: MIT

Wrap any widget so it smoothly expands and collapses when its header is tapped. No controllers to wire up, no boilerplate — but full control when you want it.

  • 🎯 Wrap anything — the header and the body are your widgets.
  • 🪗 Smooth reveal — the body slides out from under a clip (no fade flicker).
  • 🎛️ Controlled or uncontrolled — let the widget own its state, or drive it yourself (great for accordions).
  • 💤 Lazy by default — a collapsed body is not built at all, unless you ask to keep it with maintainState.
  • Accessible — the header is exposed as an expandable button to screen readers.

Getting started

dependencies:
  easy_expandable: ^0.1.0
import 'package:easy_expandable/easy_expandable.dart';

Usage

Uncontrolled — the simplest case

The widget remembers whether it is open. Just give it a starting value.

Expandable(
  initiallyExpanded: false,
  header: (context, isExpanded) => ListTile(
    title: const Text('Tap me'),
    trailing: Icon(isExpanded ? Icons.expand_less : Icons.expand_more),
  ),
  child: const Padding(
    padding: EdgeInsets.all(16),
    child: Text('Anything can go here.'),
  ),
)

A standard header, without the boilerplate

The header is always your widget — but for the common "title + chevron" case you can drop in ExpandableHeader instead of wiring the rotating icon yourself. Full custom headers stay exactly as above; this is purely opt-in.

Expandable(
  header: (context, isExpanded) => ExpandableHeader(
    leading: const Icon(Icons.folder_outlined),
    title: const Text('Section'),
    isExpanded: isExpanded, // drives the auto-rotating chevron
  ),
  child: const Text('Body'),
)

Controlled — you own the state

Pass isExpanded and update it in onExpansionChanged. This is what lets you coordinate several tiles, persist state, or open one from a button elsewhere.

bool _open = false;

Expandable(
  isExpanded: _open,
  onExpansionChanged: (value) => setState(() => _open = value),
  header: (context, isExpanded) => const Text('Controlled header'),
  child: const Text('Body'),
)

In controlled mode the body only moves when the parent updates isExpanded. If you forget to call setState, the tap is reported but nothing opens.

With a controller — drive it programmatically

Skip the bool and setState entirely: give it an ExpandableController and call expand(), collapse(), or toggle() from anywhere.

final controller = ExpandableController(); // dispose it in State.dispose()

Expandable(
  controller: controller,
  header: (context, isExpanded) => const Text('Controller-driven'),
  child: const Text('Body'),
)

// elsewhere:
controller.toggle();

Accordion — a group of tiles

ExpandableGroup coordinates several entries. By default it is an accordion (one open at a time); pass allowMultiple: true to let several stay open.

ExpandableGroup(
  // controller: myGroupController,   // optional; for collapseAll()/expandAll()
  separatorBuilder: (context, i) => const Divider(height: 1),
  children: [
    for (final section in sections)
      ExpandableGroupChild(
        header: (context, isExpanded) => Text(section.title),
        body: Text(section.body),
      ),
  ],
)

Parameters

Parameter Type Default Description
header Widget Function(context, bool isExpanded) required Always-visible, tappable header.
child Widget required Content revealed when expanded.
initiallyExpanded bool false Starting value (uncontrolled mode).
isExpanded bool? null Non-null switches to controlled mode.
onExpansionChanged ValueChanged<bool>? null Called with the requested state on tap.
duration Duration 200ms Animation duration.
curve Curve Curves.easeOut Animation curve.
maintainState bool false Keep the body built while collapsed.
crossAxisAlignment CrossAxisAlignment .start Column alignment.
revealAlignment Alignment .topCenter Where the body slides out from.

Additional information

Issues and contributions are welcome on the issue tracker.

Libraries

easy_expandable
A small, batteries-included expand/collapse widget: wrap any widget so it smoothly reveals and hides when its header is tapped.