RemixRemix
Layout

Accordion

An expandable/collapsible component for showing and hiding content panels with support for multiple expansion modes

Open catalog
Starting Flutter example…
Live Flutter from this checkout’s component catalog. Examples include Fortal styling; the source tab shows the catalog code, not a standalone application.

An accordion component that manages expansion state of content panels with min/max constraints.

When to use this

  • FAQ sections: Display frequently asked questions with expandable answers
  • Content organization: Organize large amounts of content in a compact, scannable format
  • Settings panels: Group related settings that can be shown/hidden
  • Navigation menus: Create collapsible menu structures with nested content

Anatomy

An accordion item is one panel with two parts inside it:

  • container — the outer panel. It owns the shared frame: fill, border, radius, and the clip that crops both parts into that rounded shape. Use containerEffects for layered fills, strokes, and backdrop blur painted with it.
  • trigger — the always-visible header row (leading icon, title, trailing icon). It owns only its own internal layout, such as padding and spacing.
  • content — the expanded body. It owns its own padding and any interior divider, drawn as a foreground border so it does not inset the body by a pixel.

Because the container supplies the rounding, neither the trigger nor the content rounds its own corners, so an expanded trigger meets its content with no notch.

Widget-state variants (onHovered, onPressed, onFocused, onFocusVisible, onDisabled) describe interaction with the trigger, but may style any part of the item, including the container. The expanded content is not a separate hover or press target — moving the pointer into it clears the hovered state.

The top-level Box shorthand on AccordionStyler (.color(), .padding(), .borderRadius(), .decoration(), ...) forwards to trigger, not to container. That forwarding predates the panel and is kept for backward compatibility; reach for .container(...) explicitly when you mean the outer frame.

Basic implementation

Basic implementation

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

class AccordionExample extends StatefulWidget {
  const AccordionExample({super.key});

  @override
  State<AccordionExample> createState() => _AccordionExampleState();
}

class _AccordionExampleState extends State<AccordionExample> {
  final controller = RemixAccordionController<String>(min: 0, max: 1);

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RemixAccordionGroup(
      controller: controller,
      child: ColumnBox(
        style: FlexBoxStyler().spacing(16),
        children: [
          RemixAccordion(
            value: 'accordion1',
            title: 'How do I update my account information?',
            leadingIcon: Icons.help_outline,
            style: itemStyle,
            child: const Text(
              'Insert the accordion description here. It would look better as two lines of text.',
            ),
          ),
          RemixAccordion(
            value: 'accordion2',
            title: 'What payment methods are accepted?',
            leadingIcon: Icons.help_outline,
            style: itemStyle,
            child: const Text(
                'Major credit and debit cards like Visa, MasterCard, and American Express, as well as digital payment options like PayPal and Apple Pay.'),
          ),
          RemixAccordion(
            value: 'accordion3',
            title: 'How can I track my order?',
            leadingIcon: Icons.help_outline,
            style: itemStyle,
            child: const Text(
                'You can track your order status in the "My Orders" section of your account.'),
          ),
        ],
      ),
    );
  }

  AccordionStyler get itemStyle {
    return AccordionStyler()
        // The container owns the shared frame — one fill, one border, one
        // radius, and the clip that crops the trigger and the content into it.
        .container(
          BoxStyler()
              .color(Colors.white)
              .border(
                BoxBorderMix.all(
                  BorderSideMix().color(Colors.grey.shade300).width(1),
                ),
              )
              .borderRadius(.circular(8))
              .clipBehavior(Clip.antiAlias),
        )
        // Trigger and content own only their own internal layout.
        .trigger(
          FlexBoxStyler()
              .direction(Axis.horizontal)
              .mainAxisAlignment(MainAxisAlignment.spaceBetween)
              .spacing(12)
              .padding(.horizontal(16))
              .padding(.vertical(14)),
        )
        .content(BoxStyler().padding(.horizontal(16)).padding(.bottom(14)))
        // A widget-state variant tracks the trigger, and can style any part.
        .onHovered(
          AccordionStyler().container(BoxStyler().color(Colors.grey.shade100)),
        )
        .leadingIcon(IconStyler().color(Colors.grey.shade700).size(20))
        .title(
          TextStyler()
              .color(Colors.grey.shade900)
              .fontWeight(FontWeight.w500)
              .fontSize(14),
        )
        .trailingIcon(IconStyler().color(Colors.grey.shade700).size(20));
  }
}

Fortal widgets

The application-owned Fortal preset provides a themed widget for this component:

Fortal widget

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

class FortalAccordionExample extends StatefulWidget {
  const FortalAccordionExample({super.key});

  @override
  State<FortalAccordionExample> createState() => _FortalAccordionExampleState();
}

class _FortalAccordionExampleState extends State<FortalAccordionExample> {
  final controller = RemixAccordionController<String>();

  @override
  void dispose() {
    controller.dispose();
    super.dispose();
  }

  @override
  Widget build(BuildContext context) {
    return RemixAccordionGroup(
      controller: controller,
      // Each item is its own bordered panel, so adjacent items need spacing
      // between them rather than a shared divider.
      child: Column(
        spacing: 8,
        children: [
          FortalAccordion.surface(
            value: 'item1',
            title: 'First Item',
            size: FortalAccordionSize.size2,
            child: Text('First content'),
          ),
          FortalAccordion.soft(
            value: 'item2',
            title: 'Second Item',
            size: FortalAccordionSize.size3,
            child: Text('Second content'),
          ),
        ],
      ),
    );
  }
}

See the fortalAccordionStyle source code for all available options.

Constructor

Constructor

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

// Accordion Group
RemixAccordionGroup<T> remixAccordionGroupConstructor<T>({
  Key? key,
  required Widget child,
  required RemixAccordionController<T> controller,
  List<T> initialExpandedValues = const [],
}) => throw UnimplementedError();

// Accordion Item
RemixAccordion<T> remixAccordionConstructor<T>({
  Key? key,
  required T value,
  required Widget child,
  String? title,
  IconData? leadingIcon,
  IconData? trailingIcon,
  NakedAccordionTriggerBuilder<T>? builder,
  bool enabled = true,
  MouseCursor mouseCursor = SystemMouseCursors.click,
  bool enableFeedback = true,
  bool autofocus = false,
  FocusNode? focusNode,
  ValueChanged<bool>? onFocusChange,
  ValueChanged<bool>? onHoverChange,
  ValueChanged<bool>? onPressChange,
  String? semanticLabel,
  Widget Function(Widget, Animation<double>) transitionBuilder =
      RemixAccordion.defaultAccordionTransitionBuilder,
  AccordionStyler style = const AccordionStyler.create(),
  AccordionSpec? styleSpec,
}) => throw UnimplementedError();

Properties

Widget Properties

key → Key?

Optional. Controls how one widget replaces another widget in the tree.

value → T

Required. Unique identifier tracked by the controller.

child → Widget

Required. Content rendered while expanded.

title → String?

Optional. Title text for the trigger.

leadingIcon → IconData?

Optional. Optional leading icon for the trigger.

trailingIcon → IconData?

Optional. Optional trailing icon for the trigger.

builder → NakedAccordionTriggerBuilder<T>?

Optional. Custom builder for the trigger.

enabled → bool

Optional. Whether the accordion item is interactive.

mouseCursor → MouseCursor

Optional. Mouse cursor to use when interactive.

enableFeedback → bool

Optional. Whether to provide platform feedback on interactions.

autofocus → bool

Optional. Whether the header should autofocus.

focusNode → FocusNode?

Optional. Focus node associated with the header.

onFocusChange → ValueChanged<bool>?

Optional. Called when the header's focus state changes.

onHoverChange → ValueChanged<bool>?

Optional. Called when the header's hover state changes.

onPressChange → ValueChanged<bool>?

Optional. Called when the header's pressed state changes.

semanticLabel → String?

Optional. Semantic label announced for the header.

style → AccordionStyler

Optional. The style configuration for the accordion item.

styleSpec → AccordionSpec?

Optional. A raw resolved style spec that bypasses fluent style resolution.

transitionBuilder → Widget Function(Widget, Animation<double>)

Optional. The transition builder for the accordion item.

Style Methods

container(BoxStyler value)

Configures the outer panel: fill, border, radius, and clipping shared by the trigger and the content. Not reachable through the top-level Box shorthand, which forwards to trigger.

containerEffects(RemixBoxEffectsMix value)

Configures layered fills, strokes, and backdrop blur painted with container.

trigger(FlexBoxStyler value)

Configures the trigger row style.

leadingIcon(IconStyler value)

Configures the leading icon style.

title(TextStyler value)

Configures the title text style.

trailingIcon(IconStyler value)

Configures the trailing icon style.

content(BoxStyler value)

Configures the expanded content container style.

The methods below are the top-level Box shorthand. Each forwards to trigger, not to container.

backgroundColor(Color value) / color(Color value)

Sets the trigger background color.

padding(EdgeInsetsGeometryMix value)

Sets trigger padding.

margin(EdgeInsetsGeometryMix value)

Sets trigger margin.

spacing(double value)

Sets spacing between trigger children.

alignment(Alignment value)

Sets trigger alignment.

size(double width, double height)

Sets fixed trigger width and height constraints.

constraints(BoxConstraintsMix value)

Sets trigger constraints.

decoration(DecorationMix value)

Sets trigger decoration.

borderRadius(BorderRadiusGeometryMix radius)

Sets trigger border radius.

foregroundDecoration(DecorationMix value)

Sets trigger foreground decoration.

transform(Matrix4 value, {AlignmentGeometry alignment = Alignment.center})

Applies a matrix transform to the trigger.

flex(FlexStyler value)

Configures trigger flex behavior.

titleColor(Color value)

Sets title color.

titleFontSize(double value)

Sets title font size.

titleFontWeight(FontWeight value)

Sets title font weight.

titleStyle(TextStyleMix value)

Sets title style using a TextStyleMix.

leadingIconColor(Color value)

Sets leading icon color.

leadingIconSize(double value)

Sets leading icon size.

trailingIconColor(Color value)

Sets trailing icon color.

trailingIconSize(double value)

Sets trailing icon size.

contentColor(Color value)

Sets expanded content background color.

contentPadding(EdgeInsetsGeometryMix value)

Sets expanded content padding.

contentDecoration(DecorationMix value)

Sets expanded content decoration.

onExpanded<T>(AccordionStyler value)

Applies a style while the item is expanded.

onCollapsed<T>(AccordionStyler value)

Applies a style while the item is collapsed.

onCanCollapse(AccordionStyler value)

Applies a style while the item can be collapsed.

onCanExpand<T>(AccordionStyler value)

Applies a style while the item can be expanded.

onHovered / onPressed / onFocused / onFocusVisible / onDisabled

Applies a style while the trigger is in that interaction state. The state is resolved once for the whole item, so these reach container and containerEffects as well as the trigger, title, icons, and content.

animate(AnimationConfig value)

Configures implicit animation for style transitions.

variants(List<VariantStyle<AccordionSpec>> value)

Sets custom style variants.

wrap(WidgetModifierConfig value)

Applies widget modifiers such as clipping, opacity, or scaling.

View page source on GitHub

On this page