accordion_custom

flutter dart platform license pub pub points likes stars CI

A customizable, zero-dependency accordion (expandable panel list) for Flutter. Simple to set up, yet scales to real needs: single or multiple open panels, a programmatic controller, nested accordions, keyboard navigation, and full styling — colors, borders, corner radius, padding, and the icon.

Works on every Flutter platform: Android · iOS · Web · Windows · macOS · Linux. It is a pure-Dart package (no native code, no platform channels, no third-party dependencies), so it runs anywhere Flutter runs.

Why another accordion?

Flutter's built-in ExpansionPanelList/ExpansionTile is rigid: limited styling, no single-vs-multiple switch, and awkward programmatic control. accordion_custom gives you full control with zero dependencies:

  • Single or multiple — open one panel at a time (classic accordion) or let panels open independently, via AccordionMode.
  • Slot-based, any widget — each panel's header and content are ordinary widgets, so a panel can hold anything, including another accordion (nesting).
  • .builder for lists — build panels lazily from a data list, like ListView.builder.
  • Programmatic control — an AccordionController (a ChangeNotifier) with expand, collapse, toggle, expandAll, collapseAll, and read-back of which panels are open.
  • Full styling — background (collapsed and expanded), border color/width, corner radius, padding, text style, and a rotating icon with configurable side, through AccordionHeaderStyle and AccordionContentStyle.
  • Keyboard & accessible — ↑/↓ move focus between headers, Enter/Space toggle, and each header carries Semantics (button + expanded state).
  • Simple by default — the basic case is a list of AccordionItems; every extra feature is opt-in.

Preview

All features preview

Getting started

Add the package:

flutter pub add accordion_custom

Import it:

import 'package:accordion_custom/accordion_custom.dart';

Basic usage

By default the accordion is in single mode: opening one panel closes the others.

Single-mode accordion demo

AccordionCustom(
  children: const [
    AccordionItem(
      header: Text('Section 1'),
      content: Text('Body of the first section.'),
    ),
    AccordionItem(
      header: Text('Section 2'),
      content: Text('Body of the second section.'),
    ),
  ],
)

Multiple open panels

Set mode: AccordionMode.multiple to let panels open and close independently — any number can be open at once.

Multiple open panels demo

AccordionCustom(
  mode: AccordionMode.multiple,
  children: const [
    AccordionItem(
      header: Text('Panel one'),
      content: Text('Stays open independently.'),
      initiallyExpanded: true,
    ),
    AccordionItem(
      header: Text('Panel two'),
      content: Text('Also independent.'),
    ),
  ],
)

Building from a data list

Use AccordionCustom.builder to create panels lazily from a list, like ListView.builder — ideal for long or dynamic data such as an FAQ.

Data-list builder demo

AccordionCustom.builder(
  itemCount: faqs.length,
  itemBuilder: (context, index) => AccordionItem(
    header: Text(faqs[index].question),
    content: Text(faqs[index].answer),
  ),
)

Programmatic control

Attach an AccordionController to open, close, or toggle panels from anywhere in your code (e.g. another button), and to read which panels are currently open.

Programmatic controller demo

final controller = AccordionController();

AccordionCustom(
  mode: AccordionMode.multiple,
  controller: controller,
  children: const [
    AccordionItem(header: Text('A'), content: Text('...')),
    AccordionItem(header: Text('B'), content: Text('...')),
  ],
);

// Elsewhere:
controller.expandAll();
controller.collapse(0);
controller.toggle(1);
print(controller.expandedIndexes); // e.g. {1}
controller.addListener(() => print(controller.expandedIndexes));

Panels are addressed by their zero-based index. Dispose the controller when you are done, like any ChangeNotifier.

Styling

AccordionHeaderStyle controls the header and the panel outline (border and radius); AccordionContentStyle controls the content area.

Custom colors, border, and radius demo

AccordionCustom(
  itemSpacing: 12,
  headerStyle: AccordionHeaderStyle(
    backgroundColor: Colors.indigo.shade50,
    expandedBackgroundColor: Colors.indigo.shade100,
    borderColor: Colors.indigo,
    borderWidth: 1.5,
    borderRadius: BorderRadius.circular(16),
    iconColor: Colors.indigo,
    iconPosition: AccordionIconPosition.leading,
    textStyle: const TextStyle(fontWeight: FontWeight.bold),
  ),
  contentStyle: AccordionContentStyle(
    backgroundColor: Colors.indigo.shade50,
    dividerColor: Colors.indigo.shade100,
  ),
  children: const [
    AccordionItem(header: Text('Themed'), content: Text('Fully styled.')),
  ],
)

Per-panel styling

headerStyle/contentStyle on AccordionCustom apply to every panel. To style one panel differently, set headerStyle/contentStyle on its AccordionItem — they replace the accordion-level style for that panel:

Per-panel styling demo

AccordionCustom(
  children: [
    AccordionItem(
      header: const Text('Success'),
      headerStyle: AccordionHeaderStyle(backgroundColor: Colors.green.shade50),
      contentStyle: AccordionContentStyle(backgroundColor: Colors.green.shade50),
      content: const Text('Green header and body.'),
    ),
    AccordionItem(
      header: const Text('Warning'),
      headerStyle: AccordionHeaderStyle(backgroundColor: Colors.orange.shade50),
      contentStyle: AccordionContentStyle(backgroundColor: Colors.orange.shade50),
      content: const Text('Orange header and body.'),
    ),
  ],
)

To inherit the accordion's style and change only a few fields, pass a copyWith: contentStyle: myBaseContentStyle.copyWith(backgroundColor: ...).

Divider between header and content

By default there is no line separating the header from the content. To show one, set dividerColor (and optionally dividerWidth) on the content style:

Header/content divider demo

AccordionCustom(
  contentStyle: AccordionContentStyle(
    dividerColor: Colors.grey.shade300,
    dividerWidth: 1, // optional, defaults to 1
  ),
  children: const [
    AccordionItem(
      header: Text('What is accordion_custom?'),
      content: Text('A line now separates this content from the header.'),
    ),
  ],
)

The divider only shows while the panel is expanded.

Custom padding

The header and the content have independent padding (EdgeInsetsGeometry, so only/symmetric/EdgeInsetsDirectional all work):

Custom padding demo

AccordionCustom(
  headerStyle: const AccordionHeaderStyle(
    padding: EdgeInsets.symmetric(horizontal: 20, vertical: 18),
  ),
  contentStyle: const AccordionContentStyle(
    padding: EdgeInsets.all(24),
  ),
  children: const [
    AccordionItem(header: Text('Roomy panel'), content: Text('More breathing room.')),
  ],
)

Defaults: header EdgeInsets.symmetric(horizontal: 16, vertical: 14), content EdgeInsets.fromLTRB(16, 4, 16, 16).

Custom header

Provide a completely custom header with headerBuilder, which receives the current expanded state:

Custom header demo

AccordionItem(
  headerBuilder: (context, isExpanded) => Row(
    children: [
      Icon(isExpanded ? Icons.folder_open : Icons.folder),
      const SizedBox(width: 8),
      const Text('Custom header'),
    ],
  ),
  content: const Text('...'),
)

Custom or hidden icon

Swap the default chevron for any widget, change its rotation, or hide it:

Custom or hidden icon demo

AccordionCustom(
  headerStyle: const AccordionHeaderStyle(
    icon: Icon(Icons.add),        // any widget
    expandedIconTurns: 0.125,     // 45° turn when expanded (a + becomes ×)
    iconPosition: AccordionIconPosition.leading,
  ),
  children: const [
    AccordionItem(header: Text('Custom icon'), content: Text('...')),
  ],
)

// Or hide the icon entirely:
AccordionCustom(
  headerStyle: const AccordionHeaderStyle(showIcon: false),
  children: const [
    AccordionItem(header: Text('No icon'), content: Text('...')),
  ],
)

Disabled panels

A panel with enabled: false is dimmed and ignores taps (a controller can still change it):

Disabled panel demo

AccordionCustom(
  children: const [
    AccordionItem(
      header: Text('Unavailable'),
      content: Text('You cannot open this by tapping.'),
      enabled: false,
    ),
  ],
)

Initial state

Mark a panel initiallyExpanded: true to have it open on first build. In single mode, if several set it, only the first wins:

Initially expanded demo

AccordionCustom(
  mode: AccordionMode.multiple,
  children: const [
    AccordionItem(
      header: Text('Open on start'),
      content: Text('Visible without a tap.'),
      initiallyExpanded: true,
    ),
    AccordionItem(header: Text('Closed'), content: Text('...')),
  ],
)

Animation

Tune the expand/collapse (and icon rotation) animation:

Animation duration and curve demo

AccordionCustom(
  animationDuration: const Duration(milliseconds: 500),
  animationCurve: Curves.easeInOutBack,
  children: const [
    AccordionItem(header: Text('Slow & bouncy'), content: Text('...')),
  ],
)

Nesting

A panel's content is any widget, so an accordion can contain another:

Nested accordion demo

AccordionCustom(
  children: [
    AccordionItem(
      header: const Text('Outer'),
      content: AccordionCustom(
        children: const [
          AccordionItem(header: Text('Inner'), content: Text('Nested body.')),
        ],
      ),
    ),
  ],
)

API overview

Property Description
children / .builder(itemCount, itemBuilder) The panels, given directly or built lazily.
mode AccordionMode.single (default) or AccordionMode.multiple.
controller Optional AccordionController for programmatic control.
headerStyle Header + panel outline styling (AccordionHeaderStyle).
contentStyle Content area styling (AccordionContentStyle).
animationDuration / animationCurve Expand/collapse animation.
itemSpacing Vertical gap between panels.
enableKeyboardNavigation ↑/↓ focus movement between headers.

AccordionItem: header or headerBuilder, content, initiallyExpanded, enabled, semanticLabel, and optional per-panel headerStyle / contentStyle.

AccordionController: expand, collapse, toggle, expandAll, collapseAll, isExpanded(index), expandedIndexes.

License

MIT — see LICENSE.

Libraries

accordion_custom
A customizable, zero-dependency accordion for Flutter with single/multiple expand modes, nested panels, a programmatic controller, keyboard navigation, and full styling (colors, borders, radius, padding, icon).