Components / actions
Close button
Closes a dialog, a window, a popover or a menu: a round button with a cross, named for screen readers.
Import
In a Flutter app where fliin init has run:
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.
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.
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.
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.
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.
















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
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,
);
}
}