Fliin
On this page

Components / actions

Close button

Closes a dialog, a window, a popover or a menu: a round button with a cross, named for screen readers.

16 of 16 variants pass
Close button, enabled state
Platform
Brightness
Text scale
Direction

Import

In a Flutter app where fliin init has run:

fliin add close-buttonAlso installs: core, icons, button

Examples

Enabled

A close button at the end of a title, to dismiss a sheet, a card or a notice. label is what the screen reader says.

Close button, enabled state
import 'package:your_app/ui/fliin/components/close_button/fl_close_button.dart';
import 'package:your_app/ui/fliin/components/text/fl_text.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return Row(
      children: [
        const Expanded(child: FlText('Notifications', type: FlTextType.h5)),
        FlCloseButton(label: 'Close', onPressed: () {}),
      ],
    );
  }
}

Bare

filled: false leaves the cross bare and muted until the pointer or a press fills its disc: quieter, for a notice or a chip of text.

Close button, bare state
import 'package:your_app/ui/fliin/components/close_button/fl_close_button.dart';
import 'package:your_app/ui/fliin/components/text/fl_text.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return Row(
      spacing: FlScope.of(context).space.sm,
      children: [
        const Expanded(
          child: FlText('Your trial ends in 3 days.', type: FlTextType.bodySm, tone: FlTextTone.muted),
        ),
        FlCloseButton(label: 'Dismiss', filled: false, onPressed: () {}),
      ],
    );
  }
}

Custom icon

icon replaces the cross, at its size and in its color: a chevron down to lower a sheet, a circled cross for a search field.

Close button, custom-icon state
import 'package:your_app/ui/fliin/components/close_button/fl_close_button.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:your_app/ui/fliin/icons/fl_icons.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return Row(
      mainAxisAlignment: MainAxisAlignment.center,
      spacing: FlScope.of(context).space.lg,
      children: [
        FlCloseButton(label: 'Close', onPressed: () {}),
        FlCloseButton(label: 'Lower', icon: FlIcon(FlIcons.chevronDown), onPressed: () {}),
        FlCloseButton(label: 'Clear', icon: FlIcon(FlIcons.circleXmark), filled: false, onPressed: () {}),
      ],
    );
  }
}

Disabled

onPressed: null disables the close button, for a step that must be finished first.

Close button, disabled state
import 'package:your_app/ui/fliin/components/close_button/fl_close_button.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    return const Center(child: FlCloseButton(label: 'Close', onPressed: null));
  }
}

Usage

The code the reference images render:

final example = ColoredBox(
  color: FlScope.of(context).colors.surface,
  child: Padding(
    padding: EdgeInsetsDirectional.all(FlScope.of(context).space.lg),
    child: Column(
      crossAxisAlignment: CrossAxisAlignment.start,
      spacing: FlScope.of(context).space.sm,
      children: [
        Row(
          spacing: FlScope.of(context).space.lg,
          children: [
            FlCloseButton(label: 'Close', onPressed: () {}),
            FlCloseButton(label: 'Close', onPressed: null),
          ],
        ),
      ],
    ),
  ),
);

API

FlCloseButton: class extends StatelessWidget
FlCloseButton.new: const new({super.key, required String label, required VoidCallback? onPressed, WidgetStatesController? statesController, bool filled, Widget? icon})
FlCloseButton.label: get String
FlCloseButton.onPressed: get VoidCallback?
FlCloseButton.statesController: get WidgetStatesController?
FlCloseButton.filled: get bool
FlCloseButton.icon: get Widget?
FlCloseButton.build: Widget(BuildContext context)

Reference renders

The 16 images published with version 0.2.5, rendered by the shared harness on macOS with Flutter 3.47.7. fliin verify close-button compares your copy to them.

iOS · Light
Close button, iOS, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Close button, iOS, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Close button, iOS, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Close button, iOS, Light, text 2.0×, Right to left
Text 2.0× · Right to left
iOS · Dark
Close button, iOS, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Close button, iOS, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Close button, iOS, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Close button, iOS, Dark, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Light
Close button, Android, Light, text 1.0×, Left to right
Text 1.0× · Left to right
Close button, Android, Light, text 1.0×, Right to left
Text 1.0× · Right to left
Close button, Android, Light, text 2.0×, Left to right
Text 2.0× · Left to right
Close button, Android, Light, text 2.0×, Right to left
Text 2.0× · Right to left
Android · Dark
Close button, Android, Dark, text 1.0×, Left to right
Text 1.0× · Left to right
Close button, Android, Dark, text 1.0×, Right to left
Text 1.0× · Right to left
Close button, Android, Dark, text 2.0×, Left to right
Text 2.0× · Left to right
Close button, Android, Dark, text 2.0×, Right to left
Text 2.0× · Right to left

Use it to

  • close a dialog
  • add an x button to dismiss a window
  • close a window shown over the page
  • close a popover or a menu
  • offer an accessible close button
Not for
  • cancelling an action in progress: use a button (button)
States
  • Enabled
  • Disabled

Source

The files fliin add close-button copies into your app, before your prefix and imports are applied.

close_button/fl_close_button.dart

// Modified from the CloseButton of HeroUI Native 1.0.10, Copyright 2025 NextUI
// Inc., licensed under Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0
import 'dart:math' as math;

import 'package:flutter/widgets.dart';
import 'package:fliin_ui/core/fliin.dart';
import 'package:fliin_ui/icons/fl_icons.dart';

/// The button that closes a dialog, a sheet, a popover or a menu: a round
/// button of `size.closeButton` with a cross of `size.closeIcon`.
///
/// When [filled], the cross sits on a `control` disc, like a `FlAppBar`'s
/// back chevron, which steps to `controlPressed` under the pointer and when
/// pressed. Otherwise the disc is Izydesk's `.close-btn`: no fill at rest,
/// `control` under the pointer and when pressed, and an `onSurfaceMuted`
/// cross.
/// The touch target stays the platform's.
class FlCloseButton extends StatelessWidget {
  /// A close button named [label] for the screen reader.
  const FlCloseButton({
    super.key,
    required this.label,
    required this.onPressed,
    this.statesController,
    this.filled = true,
    this.icon,
  });

  /// What the screen reader reads, usually "Close".
  final String label;

  /// Called on tap; when null, the button is disabled.
  final VoidCallback? onPressed;

  /// The button's states; see `FlPressable.statesController`.
  final WidgetStatesController? statesController;

  /// True: the cross sits on a `control` disc; false, bare until the pointer
  /// or a press fills it.
  final bool filled;

  /// The glyph in place of the cross (a circled cross,
  /// a chevron down for a sheet), sized and tinted like it through the
  /// `IconTheme`; null, `FlIcons.xmark`.
  final Widget? icon;

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final side = theme.size.closeButton;
    // A disc by default; the theme's `radiusXxl` squares it (`square` preset).
    final radius = BorderRadius.circular(math.min(theme.shape.radiusXxl, side / 2));
    // Not a FlButton: `.close-btn` fills when pressed without shrinking,
    // where the button shrinks.
    final colors = theme.colors;
    final rest = filled ? colors.control : colors.control.withValues(alpha: 0);
    final active = filled ? colors.controlPressed : colors.control;
    return FlPressable(
      onPressed: onPressed,
      statesController: statesController,
      label: label,
      borderRadius: radius,
      scaleOnPress: false,
      builder: (context, states) => Opacity(
        opacity: states.contains(WidgetState.disabled) ? theme.size.disabledOpacity : 1,
        child: FlPixelSnap(
          child: SizedBox.square(
            dimension: side,
            child: Stack(
              alignment: Alignment.center,
              children: [
                Positioned.fill(
                  child: ClipRRect(
                    borderRadius: radius,
                    // Under the pointer the disc fades to its pressed fill.
                    child: TweenAnimationBuilder<Color?>(
                      tween: ColorTween(
                        end: states.contains(WidgetState.hovered) && !states.contains(WidgetState.disabled)
                            ? active
                            : rest,
                      ),
                      duration: FlMotion.of(context).reduced ? Duration.zero : theme.motion.short2,
                      builder: (context, fill, child) => ColoredBox(color: fill!, child: child),
                      child: FlPressHighlight(color: active),
                    ),
                  ),
                ),
                _CloseIcon(color: filled ? colors.onControl : colors.onSurfaceMuted, icon: icon),
              ],
            ),
          ),
        ),
      ),
    );
  }
}

class _CloseIcon extends StatelessWidget {
  const _CloseIcon({required this.color, required this.icon});

  final Color color;
  final Widget? icon;

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final side = MediaQuery.textScalerOf(context)
        .clamp(maxScaleFactor: theme.size.iconMaxScale)
        .scale(theme.size.closeIcon);
    final icon = this.icon;
    if (icon == null) return FlIcon(FlIcons.xmark, size: side, color: color);
    return IconTheme.merge(
      data: IconThemeData(color: color, size: side),
      child: icon,
    );
  }
}

Arrows to move, Enter to open, Escape to close.