Fliin
On this page

Components / data-display

Separator

Marks the limit between two rows, two blocks or two columns with a thin or thick line, sharp to the pixel.

16 of 16 variants pass
Separator, example state
Platform
Brightness
Text scale
Direction

Import

In a Flutter app where fliin init has run:

fliin add separatorAlso installs: core

Examples

Example

A horizontal separator between a heading and what follows, and vertical ones between links on a row, through axis.

Separator, example state
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/surface/fl_surface.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 SeparatorExample extends StatelessWidget {
  const SeparatorExample({super.key});

  @override
  Widget build(BuildContext context) {
    final space = FlScope.of(context).space;
    return FlSurface(
      variant: FlSurfaceVariant.secondary,
      child: Column(
        mainAxisSize: MainAxisSize.min,
        crossAxisAlignment: CrossAxisAlignment.start,
        spacing: space.md,
        children: [
          const FlText('Fliin', type: FlTextType.h5),
          const FlText('Flutter components you copy and keep.', type: FlTextType.bodySm, tone: FlTextTone.muted),
          const FlSeparator(),
          FittedBox(
            fit: BoxFit.scaleDown,
            alignment: AlignmentDirectional.centerStart,
            child: IntrinsicHeight(
              child: Row(
                spacing: space.md,
                children: const [
                  FlText('Components'),
                  FlSeparator(axis: Axis.vertical),
                  FlText('Themes'),
                  FlSeparator(axis: Axis.vertical),
                  FlText('Examples'),
                ],
              ),
            ),
          ),
        ],
      ),
    );
  }
}

Variants

Two weights: thin by default between rows, thick between sections of a page.

Separator, variants state
import 'package:your_app/ui/fliin/components/separator/fl_separator.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 SeparatorVariantsExample extends StatelessWidget {
  const SeparatorVariantsExample({super.key});

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: FlScope.of(context).space.md,
      children: const [
        FlText('Thin', type: FlTextType.bodySm, tone: FlTextTone.muted),
        FlSeparator(),
        FlText('Thick', type: FlTextType.bodySm, tone: FlTextTone.muted),
        FlSeparator(variant: FlSeparatorVariant.thick),
      ],
    );
  }
}

With surface

On each surface its separator: separator on the default one, separatorSecondary and separatorTertiary through color on the recessed ones, so the line keeps the same contrast.

Separator, with-surface state
import 'package:your_app/ui/fliin/components/separator/fl_separator.dart';
import 'package:your_app/ui/fliin/components/surface/fl_surface.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 SeparatorWithSurfaceExample extends StatelessWidget {
  const SeparatorWithSurfaceExample({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final colors = theme.colors;
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: theme.space.lg,
      children: [
        for (final (variant, name, line) in [
          (FlSurfaceVariant.primary, 'Default surface', colors.separator),
          (
            FlSurfaceVariant.secondary,
            'Secondary surface',
            colors.separatorSecondary,
          ),
          (
            FlSurfaceVariant.tertiary,
            'Tertiary surface',
            colors.separatorTertiary,
          ),
        ])
          FlSurface(
            variant: variant,
            child: Column(
              mainAxisSize: MainAxisSize.min,
              crossAxisAlignment: CrossAxisAlignment.stretch,
              spacing: theme.space.md,
              children: [
                FlText(name, type: FlTextType.h6),
                FlSeparator(color: line),
                const FlText(
                  'Surface content',
                  type: FlTextType.bodySm,
                  tone: FlTextTone.muted,
                ),
              ],
            ),
          ),
      ],
    );
  }
}

Custom styles

A heading over a separatorSecondary line, then links separated by vertical lines of a given length, the height of the text, which need no IntrinsicHeight around the row.

Separator, custom-styles state
import 'package:your_app/ui/fliin/components/separator/fl_separator.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 SeparatorCustomStylesExample extends StatelessWidget {
  const SeparatorCustomStylesExample({super.key});

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final textHeight = theme.typography.style(FlTextRole.textSm).size;
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      spacing: theme.space.lg,
      children: [
        FlText(
          'Account settings',
          type: FlTextType.bodySm,
          weight: theme.tokens.typography.weight.medium,
        ),
        FlSeparator(color: theme.colors.separatorSecondary),
        Wrap(
          crossAxisAlignment: WrapCrossAlignment.center,
          spacing: theme.space.md,
          runSpacing: theme.space.sm,
          children: [
            for (final (index, link) in const [
              'Profile',
              'Billing',
              'Security',
            ].indexed) ...[
              if (index > 0)
                FlSeparator(axis: Axis.vertical, length: textHeight),
              FlText(link, type: FlTextType.bodySm, tone: FlTextTone.muted),
            ],
          ],
        ),
      ],
    );
  }
}

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.sm,
      children: [
        Text(
          'Dark mode',
          style: TextStyle(color: FlScope.of(context).colors.onSurface),
        ),
        Padding(
          padding: EdgeInsetsDirectional.symmetric(
            vertical: FlScope.of(context).space.sm,
          ),
          child: FlSeparator(),
        ),
        Text(
          'Notifications',
          style: TextStyle(color: FlScope.of(context).colors.onSurface),
        ),
        Padding(
          padding: EdgeInsetsDirectional.symmetric(
            vertical: FlScope.of(context).space.sm,
          ),
          child: FlSeparator(variant: FlSeparatorVariant.thick),
        ),
        SizedBox(
          height: 32,
          child: Row(
            spacing: FlScope.of(context).space.sm,
            children: [
              Flexible(
                child: Text(
                  'Wi-Fi',
                  style: TextStyle(color: FlScope.of(context).colors.onSurface),
                ),
              ),
              FlSeparator(axis: Axis.vertical),
              Flexible(
                child: Text(
                  'Bluetooth',
                  style: TextStyle(color: FlScope.of(context).colors.onSurface),
                ),
              ),
            ],
          ),
        ),
      ],
    ),
  ),
);

API

FlSeparatorVariant: enum {thin, thick}
FlSeparatorVariant.thin: static final FlSeparatorVariant
FlSeparatorVariant.thick: static final FlSeparatorVariant
FlSeparator: class extends StatelessWidget
FlSeparator.new: const new({super.key, Axis axis, FlSeparatorVariant variant, double? thickness, Color? color, double? length, double indent, double endIndent})
FlSeparator.axis: get Axis
FlSeparator.variant: get FlSeparatorVariant
FlSeparator.thickness: get double?
FlSeparator.color: get Color?
FlSeparator.length: get double?
FlSeparator.indent: get double
FlSeparator.endIndent: get double
FlSeparator.build: Widget(BuildContext context)

Reference renders

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

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

Use it to

  • separate list items with a horizontal line
  • add a divider rule between sections or rows
  • separate two columns with a vertical line
  • split a form into parts
  • show a thick break between two parts of a page
Not for
  • space without a line: use the token spacing (core)

Source

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

separator/fl_separator.dart

// Modified from the Separator 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 thickness of an [FlSeparator].
enum FlSeparatorVariant {
  /// One physical pixel, React Native's `hairlineWidth`.
  thin,

  /// `size.separatorThick`: separates two blocks rather than two rows.
  thick,
}

/// A `separator` line between two pieces of content, horizontal or vertical.
/// It takes the full length its parent offers (nothing when that length is
/// infinite), and snaps to the pixel grid to stay crisp. Purely decorative:
/// screen readers don't see it.
class FlSeparator extends StatelessWidget {
  /// An [axis] separator, thin by default.
  const FlSeparator({
    super.key,
    this.axis = Axis.horizontal,
    this.variant = FlSeparatorVariant.thin,
    this.thickness,
    this.color,
    this.length,
    this.indent = 0,
    this.endIndent = 0,
  }) : assert(length == null || length >= 0, 'length must not be negative'),
       assert(indent >= 0 && endIndent >= 0, 'indents must not be negative');

  /// Horizontal (a line between two rows) or vertical (between two
  /// columns).
  final Axis axis;

  /// Thin or thick.
  final FlSeparatorVariant variant;

  /// A chosen thickness, in logical points, instead of the one from
  /// [variant].
  final double? thickness;

  /// The line's color instead of the variant's: `separatorSecondary` or
  /// `separatorTertiary` on a `secondary` or `tertiary` surface.
  final Color? color;

  /// The line's length; when null, the full length the parent offers. A
  /// vertical separator between two words of a row takes one, the height of
  /// the text, without an `IntrinsicHeight` around the row.
  final double? length;

  /// The space left before the line: at the start of a horizontal
  /// separator, at the top of a vertical one.
  final double indent;

  /// The space left after the line: at the end of a horizontal separator,
  /// at the bottom of a vertical one.
  final double endIndent;

  @override
  Widget build(BuildContext context) {
    final theme = FlScope.of(context);
    final extent =
        thickness ??
        switch (variant) {
          FlSeparatorVariant.thin => 1 / MediaQuery.devicePixelRatioOf(context),
          FlSeparatorVariant.thick => theme.size.separatorThick,
        };
    // The canvas's thick rule is a band of `surfaceSecondary`, the thin one
    // a hairline of `separator`.
    final line = ColoredBox(
      color:
          color ??
          (variant == FlSeparatorVariant.thick && thickness == null
              ? theme.colors.surfaceSecondary
              : theme.colors.separator),
    );
    Widget separator = switch (axis) {
      // Nothing along an infinite length, unless a length is given.
      Axis.horizontal => LimitedBox(
        maxWidth: length ?? 0,
        child: SizedBox(
          width: length ?? double.infinity,
          height: extent,
          child: FlPixelSnap(child: line),
        ),
      ),
      Axis.vertical => LimitedBox(
        maxHeight: length ?? 0,
        child: SizedBox(
          width: extent,
          height: length ?? double.infinity,
          child: FlPixelSnap(child: line),
        ),
      ),
    };
    if (indent > 0 || endIndent > 0) {
      separator = Padding(
        padding: switch (axis) {
          Axis.horizontal => EdgeInsetsDirectional.only(start: indent, end: endIndent),
          Axis.vertical => EdgeInsetsDirectional.only(top: indent, bottom: endIndent),
        },
        child: separator,
      );
    }
    return ExcludeSemantics(child: separator);
  }
}

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