Fliin
On this page

Components / data-display

Chip

Shows a status, a category or a filter as a short label in a colored capsule, pressable when needed.

16 of 16 variants pass
Chip, filters state
Platform
Brightness
Text scale
Direction

Import

In a Flutter app where fliin init has run:

fliin add chipAlso installs: core

Examples

Filters

Chips as filters: each one toggles with onPressed, and a chosen one switches to the primary variant in the accent color.

Chip, filters state
import 'package:your_app/ui/fliin/components/chip/fl_chip.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 ChipFiltersExample extends StatefulWidget {
  const ChipFiltersExample({super.key});

  @override
  State<ChipFiltersExample> createState() => _ChipFiltersExampleState();
}

class _ChipFiltersExampleState extends State<ChipFiltersExample> {
  final _chosen = {'design'};

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Column(
      mainAxisSize: MainAxisSize.min,
      spacing: space.md,
      children: [
        const FlText('Filter by team', type: FlTextType.bodySm, tone: FlTextTone.muted),
        Wrap(
          alignment: WrapAlignment.center,
          spacing: space.sm,
          runSpacing: space.sm,
          children: [
            for (final (id, label) in [
              ('design', 'Design'),
              ('engineering', 'Engineering'),
              ('marketing', 'Marketing'),
              ('sales', 'Sales'),
            ])
              FlChip(
                key: ValueKey(id),
                label: label,
                variant: _chosen.contains(id) ? FlChipVariant.primary : FlChipVariant.secondary,
                color: _chosen.contains(id) ? FlChipColor.accent : FlChipColor.neutral,
                onPressed: () => setState(() => _chosen.contains(id) ? _chosen.remove(id) : _chosen.add(id)),
              ),
          ],
        ),
      ],
    );
  }
}

Colors

Five colors: accent for what is highlighted, neutral for plain labels, and success, warning and danger for a status.

Chip, colors state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Accent', variant: FlChipVariant.primary),
        FlChip(label: 'Default', variant: FlChipVariant.primary, color: FlChipColor.neutral),
        FlChip(label: 'Success', variant: FlChipVariant.primary, color: FlChipColor.success),
        FlChip(label: 'Warning', variant: FlChipVariant.primary, color: FlChipColor.warning),
        FlChip(label: 'Danger', variant: FlChipVariant.primary, color: FlChipColor.danger),
      ],
    );
  }
}

Statuses

Statuses: dot puts a dot of the label's color before it, on a solid chip; on a quieter one, an icon in leading says the same thing.

Chip, statuses state
import 'package:your_app/ui/fliin/components/chip/fl_chip.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 ChipStatusesExample extends StatelessWidget {
  const ChipStatusesExample({super.key});

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Column(
      mainAxisSize: MainAxisSize.min,
      spacing: space.lg,
      children: [
        Wrap(
          alignment: WrapAlignment.center,
          spacing: space.sm,
          runSpacing: space.sm,
          children: const [
            FlChip(label: 'Default', color: FlChipColor.neutral, dot: true),
            FlChip(label: 'Active', color: FlChipColor.success, dot: true),
            FlChip(label: 'Pending', color: FlChipColor.warning, dot: true),
            FlChip(label: 'Inactive', color: FlChipColor.danger, dot: true),
          ],
        ),
        Wrap(
          alignment: WrapAlignment.center,
          spacing: space.sm,
          runSpacing: space.sm,
          children: [
            FlChip(
              label: 'New feature',
              variant: FlChipVariant.secondary,
              color: FlChipColor.accent,
              leading: FlIcon(FlIcons.circleInfo),
            ),
            FlChip(
              label: 'Available',
              variant: FlChipVariant.secondary,
              color: FlChipColor.success,
              leading: FlIcon(FlIcons.check),
            ),
            FlChip(
              label: 'Beta',
              variant: FlChipVariant.secondary,
              color: FlChipColor.warning,
              leading: FlIcon(FlIcons.triangleExclamation),
            ),
            FlChip(
              label: 'Deprecated',
              variant: FlChipVariant.secondary,
              color: FlChipColor.danger,
              leading: FlIcon(FlIcons.circleXmark),
            ),
          ],
        ),
      ],
    );
  }
}

With icon

An icon before the label in leading, or after it in trailing, tinted with the label and at its size.

Chip, with-icon state
import 'package:your_app/ui/fliin/components/chip/fl_chip.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 ChipWithIconExample extends StatelessWidget {
  const ChipWithIconExample({super.key});

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: [
        FlChip(
          label: 'Completed',
          variant: FlChipVariant.soft,
          color: FlChipColor.success,
          leading: FlIcon(FlIcons.circleCheck),
        ),
        FlChip(
          label: 'Pending',
          variant: FlChipVariant.soft,
          color: FlChipColor.warning,
          leading: FlIcon(FlIcons.clock),
        ),
        FlChip(label: 'Failed', variant: FlChipVariant.soft, color: FlChipColor.danger, leading: FlIcon(FlIcons.xmark)),
        FlChip(
          label: 'Sort by',
          variant: FlChipVariant.secondary,
          color: FlChipColor.neutral,
          trailing: FlIcon(FlIcons.chevronDown),
          onPressed: () {},
        ),
      ],
    );
  }
}

Sizes

Three sizes: sm inside a row or a card, md by default, lg when the chip is the main content.

Chip, sizes state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      crossAxisAlignment: WrapCrossAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Small', size: FlChipSize.sm),
        FlChip(label: 'Medium', size: FlChipSize.md),
        FlChip(label: 'Large', size: FlChipSize.lg),
      ],
    );
  }
}

Primary

The primary variant, the default: the color's solid background with its foreground text, the strongest emphasis.

Chip, primary state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Accent', variant: FlChipVariant.primary),
        FlChip(label: 'Default', variant: FlChipVariant.primary, color: FlChipColor.neutral),
        FlChip(label: 'Success', variant: FlChipVariant.primary, color: FlChipColor.success),
        FlChip(label: 'Warning', variant: FlChipVariant.primary, color: FlChipColor.warning),
        FlChip(label: 'Danger', variant: FlChipVariant.primary, color: FlChipColor.danger),
      ],
    );
  }
}

Secondary

The secondary variant: the control background with the color's soft text, quieter next to other content.

Chip, secondary state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Accent', variant: FlChipVariant.secondary),
        FlChip(label: 'Default', variant: FlChipVariant.secondary, color: FlChipColor.neutral),
        FlChip(label: 'Success', variant: FlChipVariant.secondary, color: FlChipColor.success),
        FlChip(label: 'Warning', variant: FlChipVariant.secondary, color: FlChipColor.warning),
        FlChip(label: 'Danger', variant: FlChipVariant.secondary, color: FlChipColor.danger),
      ],
    );
  }
}

Tertiary

The tertiary variant: no background, only the color's soft text, for a status inside a dense row.

Chip, tertiary state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Accent', variant: FlChipVariant.tertiary),
        FlChip(label: 'Default', variant: FlChipVariant.tertiary, color: FlChipColor.neutral),
        FlChip(label: 'Success', variant: FlChipVariant.tertiary, color: FlChipColor.success),
        FlChip(label: 'Warning', variant: FlChipVariant.tertiary, color: FlChipColor.warning),
        FlChip(label: 'Danger', variant: FlChipVariant.tertiary, color: FlChipColor.danger),
      ],
    );
  }
}

Soft

The soft variant: a light tint of the color behind its soft text, a status that stands out without shouting.

Chip, soft state
import 'package:your_app/ui/fliin/components/chip/fl_chip.dart';
import 'package:your_app/ui/fliin/core/fliin.dart';
import 'package:flutter/widgets.dart';

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

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return Wrap(
      alignment: WrapAlignment.center,
      spacing: space.sm,
      runSpacing: space.sm,
      children: const [
        FlChip(label: 'Accent', variant: FlChipVariant.soft),
        FlChip(label: 'Default', variant: FlChipVariant.soft, color: FlChipColor.neutral),
        FlChip(label: 'Success', variant: FlChipVariant.soft, color: FlChipColor.success),
        FlChip(label: 'Warning', variant: FlChipVariant.soft, color: FlChipColor.warning),
        FlChip(label: 'Danger', variant: FlChipVariant.soft, color: FlChipColor.danger),
      ],
    );
  }
}

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.stretch,
      spacing: FlScope.of(context).space.md,
      children: [
        for (final v in FlChipVariant.values)
          Wrap(
            spacing: FlScope.of(context).space.sm,
            runSpacing: FlScope.of(context).space.sm,
            children: [
              for (final c in FlChipColor.values)
                FlChip(
                  label: (variant.dir == TextDirection.rtl
                      ? const [
                          'جديد',
                          'مسودة',
                          'مدفوع',
                          'قيد الانتظار',
                          'مرفوض',
                        ]
                      : const [
                          'New',
                          'Draft',
                          'Paid',
                          'Pending',
                          'Declined',
                        ])[c.index],
                  variant: v,
                  color: c,
                  size: FlChipSize.lg,
                ),
            ],
          ),
        Wrap(
          spacing: FlScope.of(context).space.sm,
          runSpacing: FlScope.of(context).space.sm,
          crossAxisAlignment: WrapCrossAlignment.center,
          children: [
            for (final s in FlChipSize.values)
              FlChip(
                label: (variant.dir == TextDirection.rtl
                    ? const ['جديد', 'مسودة', 'مدفوع', 'قيد الانتظار', 'مرفوض']
                    : const ['New', 'Draft', 'Paid', 'Pending', 'Declined'])[1],
                variant: FlChipVariant.secondary,
                color: FlChipColor.neutral,
                size: s,
                onPressed: () {},
              ),
          ],
        ),
      ],
    ),
  ),
);

API

FlChipVariant: enum {primary, secondary, tertiary, soft}
FlChipVariant.primary: static final FlChipVariant
FlChipVariant.secondary: static final FlChipVariant
FlChipVariant.tertiary: static final FlChipVariant
FlChipVariant.soft: static final FlChipVariant
FlChipColor: enum {accent, neutral, success, warning, danger}
FlChipColor.accent: static final FlChipColor
FlChipColor.neutral: static final FlChipColor
FlChipColor.success: static final FlChipColor
FlChipColor.warning: static final FlChipColor
FlChipColor.danger: static final FlChipColor
FlChipSize: enum {sm, md, lg}
FlChipSize.sm: static final FlChipSize
FlChipSize.md: static final FlChipSize
FlChipSize.lg: static final FlChipSize
FlChip: class extends StatelessWidget
FlChip.new: const new({super.key, required String label, FlChipVariant variant, FlChipColor color, FlChipSize size, Widget? leading, Widget? trailing, VoidCallback? onPressed, bool enabled, WidgetStatesController? statesController, bool dot})
FlChip.label: get String
FlChip.variant: get FlChipVariant
FlChip.color: get FlChipColor
FlChip.size: get FlChipSize
FlChip.leading: get Widget?
FlChip.trailing: get Widget?
FlChip.dot: get bool
FlChip.onPressed: get VoidCallback?
FlChip.enabled: get bool
FlChip.statesController: get WidgetStatesController?
FlChip.build: Widget(BuildContext context)

Reference renders

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

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

Use it to

  • show an order status as a colored tag
  • let people see that an item is new
  • show the topic of an article as a tag
  • show a notification count at the end of a row
  • pick an active filter among tags
Not for
  • a main action: use a button (button)
  • a message about the page's state: use an inline alert (alert)
States
  • Primary
  • Secondary
  • Tertiary
  • Soft

Source

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

chip/fl_chip.dart

// Modified from the Chip of HeroUI Native 1.0.10, Copyright 2025 NextUI Inc.,
// licensed under Apache 2.0: https://www.apache.org/licenses/LICENSE-2.0
import 'package:flutter/widgets.dart';
import 'package:fliin_ui/core/fliin.dart';

/// The fill of a [FlChip].
enum FlChipVariant {
  /// The solid background of the color, its foreground text.
  primary,

  /// The `control` background, the color's soft text.
  secondary,

  /// No background, the color's soft text.
  tertiary,

  /// The color's tint at 15 %, its soft text.
  soft,
}

/// The color of a [FlChip].
enum FlChipColor {
  /// The highlighted color: solid, `selected` with
  /// `onSelected` text (the accent is reserved for the primary action);
  /// soft, `accentSoft` with `onAccentSoft` text.
  accent,

  /// The neutral.
  neutral,

  /// Green.
  success,

  /// Orange.
  warning,

  /// Red.
  danger,
}

/// The size of a [FlChip]: its padding, its radius and its text.
enum FlChipSize {
  /// 20 high, `textXs` text.
  sm,

  /// 28 high, `textSm` text.
  md,

  /// 36 high, `textBase` text.
  lg,
}

/// A chip: a short medium-weight label in a capsule,
/// between two optional pieces of content (an icon, a remove button). It is
/// pressable when [onPressed] is given, without changing appearance.
class FlChip extends StatelessWidget {
  /// A `primary` `accent` `md` chip that shows [label].
  const FlChip({
    super.key,
    required this.label,
    this.variant = FlChipVariant.primary,
    this.color = FlChipColor.accent,
    this.size = FlChipSize.md,
    this.leading,
    this.trailing,
    this.onPressed,
    this.enabled = true,
    this.statesController,
    this.dot = false,
  });

  /// The label.
  final String label;

  /// The fill.
  final FlChipVariant variant;

  /// The color.
  final FlChipColor color;

  /// The size.
  final FlChipSize size;

  /// Content before the label, tinted with its color and at its size.
  final Widget? leading;

  /// Content after the label, tinted likewise.
  final Widget? trailing;

  /// True: a dot of the label's color before it, a status chip
  /// ("Active", "Pending"), 6 across with the default tokens. It comes
  /// before [leading], and is not read: the label says the status.
  final bool dot;

  /// When null: the chip is not pressable.
  final VoidCallback? onPressed;

  /// False: a pressable chip that cannot be pressed now, faded to
  /// `size.disabledOpacity` and announced as disabled. No effect without
  /// [onPressed].
  final bool enabled;

  /// The chip's states, which the app can read or force; see
  /// `FlPressable.statesController`. Hovered or pressed, a `control` chip
  /// steps to `controlPressed`, Izydesk's state matrix.
  final WidgetStatesController? statesController;

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final colors = theme.colors;
    final xs = theme.space.xs;
    final scale = theme.typography;
    final soft = variant != FlChipVariant.primary;
    final fill = switch ((variant, color)) {
      (FlChipVariant.tertiary, _) => null,
      (FlChipVariant.secondary, _) || (_, FlChipColor.neutral) => colors.control,
      (FlChipVariant.primary, FlChipColor.accent) => colors.selected,
      (FlChipVariant.primary, FlChipColor.success) => colors.success,
      (FlChipVariant.primary, FlChipColor.warning) => colors.warning,
      (FlChipVariant.primary, FlChipColor.danger) => colors.dangerFill,
      (FlChipVariant.soft, FlChipColor.accent) => colors.accentSoft,
      (FlChipVariant.soft, FlChipColor.success) => colors.successSoft,
      (FlChipVariant.soft, FlChipColor.warning) => colors.warningSoft,
      (FlChipVariant.soft, FlChipColor.danger) => colors.dangerSoft,
    };
    final ink = switch (color) {
      FlChipColor.accent => soft ? colors.onAccentSoft : colors.onSelected,
      FlChipColor.neutral => colors.onControl,
      FlChipColor.success => soft ? colors.onSuccessSoft : colors.onSuccess,
      FlChipColor.warning => soft ? colors.onWarningSoft : colors.onWarning,
      // `danger` holds 4.5:1 on `control`; `onDangerSoft` is darker only
      // for the soft tint.
      FlChipColor.danger => switch (variant) {
        FlChipVariant.primary => colors.onDanger,
        FlChipVariant.secondary || FlChipVariant.tertiary => colors.danger,
        FlChipVariant.soft => colors.onDangerSoft,
      },
    };
    // px-2 py-0.5 rounded-xl, px-3 py-1 rounded-2xl, px-4 py-1.5 rounded-3xl.
    final (role, horizontal, vertical, radius) = switch (size) {
      FlChipSize.sm => (FlTextRole.textXs, xs * 2, xs / 2, theme.shape.radiusXl),
      FlChipSize.md => (FlTextRole.textSm, xs * 3, xs, theme.shape.radiusXxl),
      FlChipSize.lg => (FlTextRole.textBase, xs * 4, xs * 1.5, theme.shape.radiusCard),
    };
    final style = scale.resolve(role).copyWith(color: ink, fontWeight: theme.tokens.typography.weight.medium);
    final leading = this.leading;
    final trailing = this.trailing;
    final shape = FlContinuousBorder(radius: radius);
    Widget chip({bool hovered = false}) => FlPixelSnap(
      child: DecoratedBox(
        decoration: ShapeDecoration(
          color: hovered && fill == colors.control ? colors.controlPressed : fill,
          shape: shape,
        ),
        child: Padding(
          padding: EdgeInsetsDirectional.symmetric(horizontal: horizontal, vertical: vertical),
          child: IconTheme.merge(
            data: IconThemeData(color: ink, size: scale.style(role).size),
            child: Row(
              mainAxisSize: MainAxisSize.min,
              // gap-1: calc(var(--spacing) * 1).
              spacing: xs,
              children: [
                if (dot)
                  SizedBox.square(
                    dimension: xs * 1.5,
                    child: DecoratedBox(
                      decoration: ShapeDecoration(color: ink, shape: const CircleBorder()),
                    ),
                  ),
                ?leading,
                Flexible(
                  child: Text(
                    label,
                    style: style,
                    strutStyle: scale.strut(role, inheritedFamily: DefaultTextStyle.of(context).style.fontFamily),
                  ),
                ),
                ?trailing,
              ],
            ),
          ),
        ),
      ),
    );
    final onPressed = this.onPressed;
    // align-self: flex-start: stretched by a column, the chip keeps its
    // width, at the start of the line.
    if (onPressed == null) return _start(chip());
    return _start(
      FlPressable(
        onPressed: enabled ? onPressed : null,
        statesController: statesController,
        borderRadius: BorderRadius.all(Radius.circular(radius)),
        scaleOnPress: false,
        overlay: FlPressOverlay.none,
        builder: (context, states) => states.contains(WidgetState.disabled)
            ? Opacity(opacity: theme.size.disabledOpacity, child: chip())
            : chip(hovered: states.contains(WidgetState.hovered) || states.contains(WidgetState.pressed)),
      ),
    );
  }
}

Widget _start(Widget child) =>
    Align(alignment: AlignmentDirectional.centerStart, widthFactor: 1, heightFactor: 1, child: child);

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