Fliin
On this page

Components / forms

Color field

Types a color as a hex code, with its swatch at the start of the field; a code that is not a color gives the previous one back on leaving.

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

Import

In a Flutter app where fliin init has run:

fliin add color-fieldAlso installs: core, color-swatch, text-field

Examples

Example

A field for a hexadecimal color, with its swatch beside the text. It hands you a Color once the six digits are valid.

Color field, example state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldExample> createState() => _ColorFieldExampleState();
}

class _ColorFieldExampleState extends State<ColorFieldExample> {
  Color? _color = const Color(0xFF0485F7);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlColorField(
          label: 'Color',
          value: _color,
          placeholder: '#0485F7',
          onChanged: (color) => setState(() => _color = color),
        ),
      ],
    );
  }
}

Filled

The field filled with a color your app already has.

Color field, filled state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldFilledExample> createState() => _ColorFieldFilledExampleState();
}

class _ColorFieldFilledExampleState extends State<ColorFieldFilledExample> {
  Color? _color = const HSVColor.fromAHSV(1, 217, 0.76, 0.96).toColor();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlColorField(
          label: 'Color',
          value: _color,
          placeholder: '#0485F7',
          onChanged: (color) => setState(() => _color = color),
        ),
      ],
    );
  }
}

Empty

Empty, the placeholder shows the format and helper says what to type.

Color field, empty state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldEmptyExample> createState() => _ColorFieldEmptyExampleState();
}

class _ColorFieldEmptyExampleState extends State<ColorFieldEmptyExample> {
  Color? _color;

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlColorField(
          label: 'Color',
          value: _color,
          placeholder: '#0485F7',
          helper: 'Six hexadecimal digits',
          onChanged: (color) => setState(() => _color = color),
        ),
      ],
    );
  }
}

Invalid

error replaces the helper and marks the field when the color does not suit.

Color field, invalid state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldInvalidExample> createState() => _ColorFieldInvalidExampleState();
}

class _ColorFieldInvalidExampleState extends State<ColorFieldInvalidExample> {
  Color? _color = const Color(0xFF000000);

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlColorField(
          label: 'Color',
          value: _color,
          placeholder: '#0485F7',
          error: 'Pick a lighter color',
          onChanged: (color) => setState(() => _color = color),
        ),
      ],
    );
  }
}

Disabled

enabled: false keeps the color readable but not editable.

Color field, disabled state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldDisabledExample> createState() => _ColorFieldDisabledExampleState();
}

class _ColorFieldDisabledExampleState extends State<ColorFieldDisabledExample> {
  Color? _color = const HSVColor.fromAHSV(1, 217, 0.76, 0.96).toColor();

  @override
  Widget build(BuildContext context) {
    return Column(
      mainAxisSize: MainAxisSize.min,
      crossAxisAlignment: CrossAxisAlignment.stretch,
      children: [
        FlColorField(
          label: 'Color',
          value: _color,
          placeholder: '#0485F7',
          enabled: false,
          onChanged: (color) => setState(() => _color = color),
        ),
      ],
    );
  }
}

On surface

On a surface, FlInputVariant.secondary; alpha reads and writes the opacity too (#RRGGBBAA).

Color field, on-surface state
import 'package:your_app/ui/fliin/components/color_field/fl_color_field.dart';
import 'package:your_app/ui/fliin/components/surface/fl_surface.dart';
import 'package:your_app/ui/fliin/components/text_field/fl_text_field.dart';
import 'package:flutter/widgets.dart';

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

  @override
  State<ColorFieldOnSurfaceExample> createState() => _ColorFieldOnSurfaceExampleState();
}

class _ColorFieldOnSurfaceExampleState extends State<ColorFieldOnSurfaceExample> {
  Color? _color = const Color(0xCC3B82F6);

  @override
  Widget build(BuildContext context) {
    return FlSurface(
      child: FlColorField(
        label: 'Overlay color',
        value: _color,
        alpha: true,
        variant: FlInputVariant.secondary,
        helper: 'Hex with opacity, like #3B82F6CC',
        onChanged: (color) => setState(() => _color = color),
      ),
    );
  }
}

Usage

The code the reference images render:

final example = Builder(
  builder: (context) {
    final rtl = variant.dir == TextDirection.rtl;
    final theme = FlScope.of(context);
    String tr(String fr, String ar) => rtl ? ar : fr;
    return ColoredBox(
      color: theme.colors.surface,
      child: SingleChildScrollView(
        padding: EdgeInsetsDirectional.all(theme.space.lg),
        child: Column(
          crossAxisAlignment: CrossAxisAlignment.stretch,
          spacing: theme.space.lg,
          children: [
            FlColorField(
              label: tr('Color', 'اللون'),
              value: HSVColor.fromAHSV(1, 208, 0.98, 0.97).toColor(),
              onChanged: (_) {},
            ),
            FlColorField(
              label: tr('Background color', 'لون الخلفية'),
              value: null,
              placeholder: '#FF5733',
              helper: tr('Six hexadecimal digits', 'ستة أرقام سداسية عشرية'),
              onChanged: (_) {},
            ),
            FlColorField(
              label: tr('Brand color', 'لون العلامة'),
              value: HSVColor.fromAHSV(1, 0, 0, 0).toColor(),
              required: true,
              error: tr('Choose a lighter color', 'اختر لونًا أفتح'),
              onChanged: (_) {},
            ),
            FlColorField(
              label: tr('Overlay', 'الطبقة'),
              value: HSVColor.fromAHSV(0.5, 217, 0.76, 0.96).toColor(),
              alpha: true,
              onChanged: (_) {},
            ),
            FlColorField(
              label: tr('Theme color', 'لون السمة'),
              value: HSVColor.fromAHSV(1, 160, 0.9, 0.73).toColor(),
              enabled: false,
              onChanged: (_) {},
            ),
          ],
        ),
      ),
    );
  },
);

API

FlColorField: class extends StatefulWidget
FlColorField.new: const new({super.key, required String label, required Color? value, required ValueChanged<Color?> onChanged, String? placeholder, String? helper, String? error, bool alpha, bool required, bool enabled, FlInputVariant variant, FocusNode? focusNode, bool autofocus})
FlColorField.label: get String
FlColorField.value: get Color?
FlColorField.onChanged: get ValueChanged<Color?>
FlColorField.placeholder: get String?
FlColorField.helper: get String?
FlColorField.error: get String?
FlColorField.alpha: get bool
FlColorField.required: get bool
FlColorField.enabled: get bool
FlColorField.variant: get FlInputVariant
FlColorField.focusNode: get FocusNode?
FlColorField.autofocus: get bool
FlColorField.createState: State<FlColorField>()

Reference renders

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

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

Use it to

  • edit a colour as a hex code
  • change a brand colour written as #RRGGBB
  • show a swatch of the typed colour in the field
  • show an error under an invalid hex colour
  • accept a colour with its opacity as eight hex digits
Not for
  • free text: use a text field (text-field)
  • picking a color by eye: use a color picker (color-picker)
  • a number with minus and plus: use a number field (number-field)
States
  • Filled
  • Empty
  • Invalid
  • Disabled

Source

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

color_field/fl_color_field.dart

// Modified from the ColorField and ColorInputGroup of HeroUI React v3,
// Copyright 2025 NextUI Inc., licensed under Apache 2.0:
// https://www.apache.org/licenses/LICENSE-2.0
import 'package:flutter/services.dart';
import 'package:flutter/widgets.dart';
import 'package:fliin_ui/components/color_swatch/fl_color_swatch.dart';
import 'package:fliin_ui/components/text_field/fl_text_field.dart';

/// A hexadecimal color field: the label, an input that writes the color as
/// `#RRGGBB` with a 16 swatch of it at its start, then the help or the
/// error. It has the frame of a `FlTextField`: white, lifted by the
/// `field` shadow, the accent ring on focus, the `danger` outline when
/// [error] is given.
///
/// Typed text counts as soon as it is a color (`#RGB` or `#RRGGBB`, the
/// `#` optional; with [alpha], `#RGBA` and `#RRGGBBAA` too): [onChanged]
/// receives it at once, and the swatch follows. On leaving the field or on
/// the action key, a color is rewritten in full capitals, and text that is
/// not one gives the previous color back, as React Aria's `ColorField`
/// does; an emptied field gives null, unless [required].
///
/// The hex is written left to right on an Arabic page too. The color is a
/// value of the app, never a theme color.
class FlColorField extends StatefulWidget {
  /// A field named [label] that shows [value] and offers the colors typed to
  /// [onChanged].
  const FlColorField({
    super.key,
    required this.label,
    required this.value,
    required this.onChanged,
    this.placeholder,
    this.helper,
    this.error,
    this.alpha = false,
    this.required = false,
    this.enabled = true,
    this.variant = FlInputVariant.primary,
    this.focusNode,
    this.autofocus = false,
  });

  /// The field's name, shown above and read by the screen reader.
  final String label;

  /// The color shown; null, an empty field. A value different from the one
  /// being typed replaces the text: the parent has the last word.
  final Color? value;

  /// Receives each color typed, and null when the field is emptied (unless
  /// [required]); up to the parent to give it back in [value].
  final ValueChanged<Color?> onChanged;

  /// The example text of an empty field ("#0485F7").
  final String? placeholder;

  /// The help under the field, read as a hint. Hidden while [error] is
  /// non-null.
  final String? helper;

  /// Non-null: the field is invalid, and this text replaces [helper].
  final String? error;

  /// True: the field reads and writes the opacity (`#RRGGBBAA`).
  final bool alpha;

  /// True: an asterisk follows the label, and an emptied field gives the
  /// previous color back.
  final bool required;

  /// False: the whole field is half opaque, without touch or focus.
  final bool enabled;

  /// The background: `primary` by default, `secondary` on a surface.
  final FlInputVariant variant;

  /// The input's focus node; null, the field creates its own.
  final FocusNode? focusNode;

  /// Takes focus on the first frame if nothing else has it within its scope.
  final bool autofocus;

  @override
  State<FlColorField> createState() => _FlColorFieldState();
}

class _FlColorFieldState extends State<FlColorField> {
  final TextEditingController _text = TextEditingController();
  FocusNode? _ownFocus;
  FocusNode get _focus => widget.focusNode ?? (_ownFocus ??= FocusNode());

  /// The last color the input gave the parent: given back in
  /// [FlColorField.value], it does not rewrite the text being typed.
  Color? _typed;

  @override
  void initState() {
    super.initState();
    _text.text = _format(widget.value);
    _focus.addListener(_onFocusChange);
  }

  @override
  void didUpdateWidget(FlColorField oldWidget) {
    super.didUpdateWidget(oldWidget);
    if (oldWidget.focusNode != widget.focusNode) {
      (oldWidget.focusNode ?? _ownFocus)?.removeListener(_onFocusChange);
      _focus.addListener(_onFocusChange);
    }
    if (oldWidget.value != widget.value && !(_focus.hasFocus && widget.value == _typed)) {
      _show(widget.value);
    }
  }

  @override
  void dispose() {
    _focus.removeListener(_onFocusChange);
    _ownFocus?.dispose();
    _text.dispose();
    super.dispose();
  }

  String _format(Color? color) => color == null ? '' : flColorHex(color, alpha: widget.alpha);

  void _show(Color? color) {
    final text = _format(color);
    if (_text.text == text) return;
    _text.value = TextEditingValue(
      text: text,
      selection: TextSelection.collapsed(offset: text.length),
    );
  }

  void _onFocusChange() {
    if (!_focus.hasFocus) _settle();
  }

  void _onChanged(String text) {
    if (text.trim().isEmpty) {
      if (!widget.required && widget.value != null) {
        _typed = null;
        widget.onChanged(null);
      }
      return;
    }
    final color = flParseColorHex(text, alpha: widget.alpha);
    if (color == null || color == widget.value) return;
    _typed = color;
    widget.onChanged(color);
  }

  /// On leaving: a color is rewritten in full, anything else gives the
  /// previous color back.
  void _settle() {
    final color = flParseColorHex(_text.text, alpha: widget.alpha);
    if (color == null && _text.text.trim().isEmpty && !widget.required) {
      _show(null);
      return;
    }
    _show(color ?? widget.value);
  }

  @override
  Widget build(BuildContext context) {
    final shown = widget.value;
    return FlTextField(
      label: widget.label,
      controller: _text,
      focusNode: _focus,
      placeholder: widget.placeholder,
      helper: widget.helper,
      error: widget.error,
      required: widget.required,
      enabled: widget.enabled,
      variant: widget.variant,
      autofocus: widget.autofocus,
      keyboardType: TextInputType.visiblePassword,
      textInputAction: TextInputAction.done,
      textDirection: TextDirection.ltr,
      prefix: shown == null ? null : FlColorSwatch(color: shown, label: null, size: FlColorSwatchSize.xs),
      onChanged: _onChanged,
      onSubmitted: (_) => _settle(),
    );
  }
}

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