Accordion

In ProgressDark modeRTL

Expandable and collapsible content sections.


7:21
Is it accessible?
Yes. It adheres to the WAI-ARIA design pattern and works with screen readers and keyboard navigation.
Is it styled?
Is it animated?

Installation

Copy the component into your project using the CLI:

$ronakcn add accordion

Usage

Import and use the component in your Flutter widget tree:

lib/pages/example.dart
import 'package:flutter/material.dart';
import '../components/accordion/accordion.dart';

class ExamplePage extends StatelessWidget {
  const ExamplePage({super.key});

  @override
  Widget build(BuildContext context) {
    return Scaffold(
      appBar: AppBar(title: const Text('RcnAccordion Example')),
      body: Center(
        child: RcnAccordion(
          // Configure props here
        ),
      ),
    );
  }
}

Variants

Accordion ships with the following variants:

default

Only one section open at a time.

7:21
Is it accessible?
Yes. It adheres to the WAI-ARIA design pattern and works with screen readers and keyboard navigation.
Is it styled?
Is it animated?

multiple

Multiple sections can be open simultaneously.

7:21
Is it accessible?
Yes. It adheres to the WAI-ARIA design pattern and works with screen readers and keyboard navigation.
Is it styled?
Is it animated?

Props

PropTypeDefaultDescription
items*List<AccordionItem>List of accordion sections.
allowMultipleboolfalseAllow multiple sections to be open.
initialOpenIndexint?nullIndex of initially opened section.

Source files

lib/components/ui/accordion.dart
import 'package:flutter/material.dart';

class AccordionItem {
  const AccordionItem({required this.title, required this.content, this.subtitle});
  final String title;
  final String content;
  final String? subtitle;
}

class RcnAccordion extends StatefulWidget {
  const RcnAccordion({super.key, required this.items, this.allowMultiple = false, this.initialOpenIndex});
  final List<AccordionItem> items;
  final bool allowMultiple;
  final int? initialOpenIndex;

  @override
  State<RcnAccordion> createState() => _RcnAccordionState();
}

class _RcnAccordionState extends State<RcnAccordion> {
  late Set<int> _openIndices;

  @override
  void initState() {
    super.initState();
    _openIndices = widget.initialOpenIndex != null ? {widget.initialOpenIndex!} : {};
  }

  void _toggle(int index) {
    setState(() {
      if (_openIndices.contains(index)) {
        _openIndices.remove(index);
      } else {
        if (!widget.allowMultiple) _openIndices.clear();
        _openIndices.add(index);
      }
    });
  }

  @override
  Widget build(BuildContext context) {
    final colors = Theme.of(context).colorScheme;
    return Column(
      children: List.generate(widget.items.length, (i) {
        final item = widget.items[i];
        final isOpen = _openIndices.contains(i);
        return Column(
          children: [
            if (i > 0) Divider(height: 1, color: colors.outline.withOpacity(0.2)),
            InkWell(
              onTap: () => _toggle(i),
              child: Padding(
                padding: const EdgeInsets.symmetric(vertical: 14),
                child: Row(
                  children: [
                    Expanded(child: Text(item.title, style: TextStyle(fontSize: 14, fontWeight: FontWeight.w500, color: colors.onSurface))),
                    AnimatedRotation(
                      turns: isOpen ? 0.5 : 0,
                      duration: const Duration(milliseconds: 200),
                      child: Icon(Icons.keyboard_arrow_down, size: 18, color: colors.onSurface.withOpacity(0.6)),
                    ),
                  ],
                ),
              ),
            ),
            AnimatedCrossFade(
              duration: const Duration(milliseconds: 200),
              crossFadeState: isOpen ? CrossFadeState.showFirst : CrossFadeState.showSecond,
              firstChild: Padding(
                padding: const EdgeInsets.only(bottom: 14),
                child: Text(item.content, style: TextStyle(fontSize: 13, color: colors.onSurface.withOpacity(0.7))),
              ),
              secondChild: const SizedBox.shrink(),
            ),
          ],
        );
      }),
    );
  }
}
Version 0.1.0 · display category