UI ELEMENTS (MISC)

Accordion (Card)

A clean, card-style accordion where each panel is a self-contained bordered card with a smoothly rotating chevron. Only one panel stays open at a time and the body height animates open and closed — perfect for FAQs and grouped settings. Built with pure Flutter SDK, no external packages.

accordionfaqexpandable
Preview
FlutterKit is an open-source library of production-ready Flutter widgets — buttons, dialogs, navbars, and more. Copy the code, paste it in, and ship faster.
Yes. Every component is designed in Figma first, so you get a pixel-perfect design reference alongside the code.
Absolutely. All components are MIT licensed and free to use in personal and commercial apps, forever.

What's Included

  • AccordionCard widget that takes a list of AccordionItem (title + body)
  • Single-open behavior — opening one panel collapses the others
  • AnimatedRotation chevron that flips 180° on expand
  • Animated height transition when a panel opens or closes
  • Card styling — rounded corners, subtle border and shadow per panel
  • First panel open by default, with full tap target via InkWell ripple

Use Cases

  • FAQ sections on landing pages and help screens
  • Grouped settings or preferences that expand on tap
  • Order / invoice detail breakdowns inside a list
  • Onboarding or docs content split into collapsible topics

PRO TIP

For long lists of accordion panels, wrap the column in a ListView.builder and lift the open index into the parent so only one item rebuilds on tap. Use AnimatedSize (not a fixed height) so the body grows to fit any amount of text without overflow.

Complete Dart Code

Copy this into your Flutter project. No external packages required.

dart
import 'package:flutter/material.dart';

void main() {
  runApp(const MyApp());
}

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

  @override
  Widget build(BuildContext context) {
    return MaterialApp(
      debugShowCheckedModeBanner: false,
      home: Scaffold(
        backgroundColor: Colors.white,
        body: Center(
          child: SingleChildScrollView(
            padding: const EdgeInsets.all(24),
            child: ConstrainedBox(
              constraints: const BoxConstraints(maxWidth: 480),
              child: AccordionCard(
                items: const [
                  AccordionItem(
                    title: 'What is FlutterKit?',
                    body:
                        'FlutterKit is an open-source library of production-ready '
                        'Flutter widgets — buttons, dialogs, navbars, and more. '
                        'Copy the code, paste it in, and ship faster.',
                  ),
                  AccordionItem(
                    title: 'Is there a Figma file available?',
                    body:
                        'Yes. Every component is designed in Figma first, so you '
                        'get a pixel-perfect design reference alongside the code.',
                  ),
                  AccordionItem(
                    title: 'Can I use it in commercial projects?',
                    body:
                        'Absolutely. All components are MIT licensed and free to '
                        'use in personal and commercial apps, forever.',
                  ),
                ],
              ),
            ),
          ),
        ),
      ),
    );
  }
}

/// A single panel's content.
class AccordionItem {
  final String title;
  final String body;
  const AccordionItem({required this.title, required this.body});
}

/// Card-style accordion. Only one panel is open at a time.
/// Pass [initialOpen] = -1 to start with everything collapsed.
class AccordionCard extends StatefulWidget {
  final List<AccordionItem> items;
  final int initialOpen;
  const AccordionCard({
    super.key,
    required this.items,
    this.initialOpen = 0,
  });

  @override
  State<AccordionCard> createState() => _AccordionCardState();
}

class _AccordionCardState extends State<AccordionCard> {
  late int _openIndex = widget.initialOpen;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        for (int i = 0; i < widget.items.length; i++)
          Padding(
            padding: EdgeInsets.only(top: i == 0 ? 0 : 16),
            child: _AccordionPanel(
              item: widget.items[i],
              isOpen: _openIndex == i,
              onTap: () => setState(
                () => _openIndex = _openIndex == i ? -1 : i,
              ),
            ),
          ),
      ],
    );
  }
}

class _AccordionPanel extends StatelessWidget {
  final AccordionItem item;
  final bool isOpen;
  final VoidCallback onTap;
  const _AccordionPanel({
    required this.item,
    required this.isOpen,
    required this.onTap,
  });

  static const Color _border = Color(0xFFE5E7EB);
  static const Color _heading = Color(0xFF111827);
  static const Color _body = Color(0xFF4B5563);

  @override
  Widget build(BuildContext context) {
    final BorderRadius headerRadius = isOpen
        ? const BorderRadius.vertical(top: Radius.circular(8))
        : BorderRadius.circular(8);

    return Column(
      mainAxisSize: MainAxisSize.min,
      children: [
        // Header button
        Material(
          color: Colors.white,
          borderRadius: headerRadius,
          child: InkWell(
            borderRadius: headerRadius,
            onTap: onTap,
            child: Container(
              padding: const EdgeInsets.all(20),
              decoration: BoxDecoration(
                color: Colors.white,
                borderRadius: headerRadius,
                border: Border.all(color: _border),
                boxShadow: isOpen
                    ? null
                    : const [
                        BoxShadow(
                          color: Color(0x0A000000),
                          blurRadius: 2,
                          offset: Offset(0, 1),
                        ),
                      ],
              ),
              child: Row(
                children: [
                  Expanded(
                    child: Text(
                      item.title,
                      style: TextStyle(
                        fontSize: 15,
                        fontWeight: FontWeight.w500,
                        color: isOpen ? _heading : _body,
                      ),
                    ),
                  ),
                  const SizedBox(width: 12),
                  AnimatedRotation(
                    duration: const Duration(milliseconds: 200),
                    turns: isOpen ? 0.5 : 0,
                    child: const Icon(
                      Icons.keyboard_arrow_down,
                      size: 20,
                      color: _body,
                    ),
                  ),
                ],
              ),
            ),
          ),
        ),
        // Body — height animates open/closed
        AnimatedCrossFade(
          duration: const Duration(milliseconds: 200),
          firstCurve: Curves.easeOut,
          secondCurve: Curves.easeOut,
          sizeCurve: Curves.easeInOut,
          crossFadeState:
              isOpen ? CrossFadeState.showFirst : CrossFadeState.showSecond,
          firstChild: Container(
            width: double.infinity,
            padding: const EdgeInsets.all(20),
            decoration: const BoxDecoration(
              color: Colors.white,
              border: Border(
                left: BorderSide(color: _border),
                right: BorderSide(color: _border),
                bottom: BorderSide(color: _border),
              ),
              borderRadius: BorderRadius.vertical(
                bottom: Radius.circular(8),
              ),
            ),
            child: Text(
              item.body,
              style: const TextStyle(
                fontSize: 14,
                height: 1.6,
                color: _body,
              ),
            ),
          ),
          secondChild: const SizedBox(width: double.infinity),
        ),
      ],
    );
  }
}

How to use this widget

  1. 1Copy the complete Dart code above
  2. 2Create a new file — accordion_card.dart in your Flutter project
  3. 3Use AccordionCard(items: [AccordionItem(title: '...', body: '...')]) — pass initialOpen: -1 to start fully collapsed

Related components

Feedback & Alerts

Alert Widget

Dismissible alert banners in 5 styles — Info, Success, Warning, and Danger

View component →
Data Display

Stat Card

Dashboard metric cards in 4 styles — Simple, Icon, Trend, and Progress

View component →
Fintech & Wallet

Code Scan

Payment QR + barcode card with header, tabs, and security footer

View component →