liquid_segmented_bar 0.1.1
liquid_segmented_bar: ^0.1.1 copied to clipboard
An iOS "liquid glass" style segmented control for Flutter: a frosted glass capsule with an animated glass bubble that slides to the selected segment.
liquid_segmented_bar #
An iOS "liquid glass" style segmented control for Flutter: a frosted glass capsule with a glossy bubble that springs to the selected segment.
Table of contents #
- Features
- Installation
- Quick start
- Label modes
- Visuals
- Sizing
- Styling
- Animation and haptics
- Accessibility
- API reference
- Tips and FAQ
- Support
- License
Features #
- Liquid glass look: backdrop blur, gradient glass capsule and an accent-tinted bubble with a top highlight.
- Generic and type-safe:
LiquidSegmentedBar<T>works with enums, strings, ints, nullable values or your own classes. - Show anything: icons, PNG/JPG/WebP/GIF assets, network images, SVG (via your own renderer), Lottie or any widget.
- Three label modes:
always,selectedOnly,never. - Adjustable sizes: icon size, image size, label and selected-label font size, spacing and padding.
- Height that adapts: calculated from the content and the device text
scale, always clamped between
minHeightandmaxHeight. - No overflow errors: labels are cut off with an ellipsis and content scales down when space is tight.
- Accessible: every segment is a semantic button with selected state; hidden labels are still read by screen readers.
- Lightweight: depends only on Flutter.
Installation #
dependencies:
liquid_segmented_bar: ^0.1.0
import 'package:liquid_segmented_bar/liquid_segmented_bar.dart';
Requires Dart >=3.7.0 and Flutter >=3.27.0.
Quick start #
enum Side { t, ct }
class SidePicker extends StatefulWidget {
const SidePicker({super.key});
@override
State<SidePicker> createState() => _SidePickerState();
}
class _SidePickerState extends State<SidePicker> {
Side _side = Side.t;
@override
Widget build(BuildContext context) {
return LiquidSegmentedBar<Side>(
selected: _side,
onChanged: (side) => setState(() => _side = side),
segments: const [
LiquidSegment(value: Side.t, label: 'Terrorists', icon: Icons.shield),
LiquidSegment(value: Side.ct, label: 'Counter-T', icon: Icons.security),
],
);
}
}
Text only #
Segments don't need a visual. Leave out icon, asset and visual for a
clean, text-only tab bar:

enum Sort { hot, latest, top }
LiquidSegmentedBar<Sort>(
selected: _sort,
onChanged: (sort) => setState(() => _sort = sort),
segments: const [
LiquidSegment(value: Sort.hot, label: 'Hot'),
LiquidSegment(value: Sort.latest, label: 'New'),
LiquidSegment(value: Sort.top, label: 'Top'),
],
)
The blur affects whatever is behind the bar. It looks best on top of images, gradients or scrolling content, for example in a
Stack.
Label modes #
Choose when labels are visible with labelBehavior:
LiquidSegmentedBar<Grenade>(
labelBehavior: LiquidLabelBehavior.selectedOnly,
// ...
)
| Value | Behavior |
|---|---|
always |
Every segment shows its label under the visual. Default. |
selectedOnly |
Only the selected segment shows its label, with a fade and size animation. |
never |
Only visuals are shown. Labels are still read by screen readers. |
A segment without any visual always shows its label, whatever the mode, so it never renders empty.
Visuals #
Every segment can show one visual above its label. If several are set, the first one in this order wins:
visualBuilder → visual → asset → icon
icon is also the fallback when an asset fails to load.
| Field | Use it for |
|---|---|
icon |
Icons.*, CupertinoIcons.*, custom icon fonts |
asset |
Asset path or http(s) URL; PNG/JPG/WebP/GIF work out of the box |
visual |
Any widget: SvgPicture, Lottie, CachedNetworkImage, emoji… |
visualBuilder |
A widget that changes with the selection state |
Recommended image formats: PNG (with transparency) or SVG. Other formats still render; in debug mode a one-time warning is printed for each such asset. Nothing is ever blocked.
Icons #
LiquidSegment(value: Mode.grid, label: 'Grid', icon: Icons.grid_view_rounded)
Selected icons use the accent color; unselected icons use a dimmed
foregroundColor.
Image assets and URLs #
// Local asset (remember to declare it in pubspec.yaml)
LiquidSegment(value: Grenade.smoke, label: 'Smoke', asset: 'assets/smoke.png')
// Network image
LiquidSegment(value: Grenade.flash, label: 'Flash', asset: 'https://example.com/flash.png')
// With a fallback icon if loading fails
LiquidSegment(
value: Grenade.he,
label: 'HE',
asset: 'assets/he.png',
icon: Icons.flare,
)
Without a custom builder, paths use Image.asset and http(s) URLs use
Image.network.
SVG: bring your own package #
This package has no SVG dependency to stay small. Add
flutter_svg (or any renderer) to
your app and register it once, for example in main():
import 'package:flutter/material.dart';
import 'package:flutter_svg/flutter_svg.dart';
import 'package:liquid_segmented_bar/liquid_segmented_bar.dart';
void main() {
LiquidSegmentedBar.defaultAssetBuilder = (context, source, state) {
final isNetwork = source.startsWith('http');
if (source.endsWith('.svg')) {
final tint = ColorFilter.mode(state.color, BlendMode.srcIn);
return isNetwork
? SvgPicture.network(source, width: state.size, colorFilter: tint)
: SvgPicture.asset(source, width: state.size, colorFilter: tint);
}
return isNetwork
? Image.network(source, height: state.size)
: Image.asset(source, height: state.size);
};
runApp(const MyApp());
}
Now any segment can simply use an SVG path:
LiquidSegment(value: Grenade.smoke, label: 'Smoke', asset: 'assets/smoke.svg')
To use a different renderer for one bar only, pass assetBuilder: to that
bar; it overrides defaultAssetBuilder.
If an
.svgis used and no builder is registered, the segment falls back to itsiconand a debug message explains how to enable SVG.
Custom widgets #
Pass any widget with visual. It is laid out in an imageSize × imageSize
box.
LiquidSegment(value: Rank.gold, label: 'Gold', visual: Text('🏆', style: TextStyle(fontSize: 18)))
LiquidSegment(value: Map.mirage, label: 'Mirage', visual: CachedNetworkImage(imageUrl: url))
State-aware visuals #
Use visualBuilder when the visual should change with the selection:
LiquidSegment(
value: Grenade.molotov,
label: 'Molotov',
visualBuilder: (context, state) => Lottie.asset(
'assets/fire.json',
animate: state.selected, // plays only while selected
),
)
The builder receives a LiquidVisualState:
| Property | Description |
|---|---|
selected |
Whether the segment is selected |
size |
Box size for the visual (the bar's imageSize) |
color |
Suggested tint: accent if selected, dimmed foreground otherwise |
accent |
The bar's accent color |
Sizing #
Visual and text size #
LiquidSegmentedBar<Grenade>(
iconSize: 24, // icons
imageSize: 28, // assets, `visual`, `visualBuilder`
labelFontSize: 11, // all labels
selectedLabelFontSize: 12, // selected label only
labelSpacing: 4, // gap between visual and label
labelAlign: TextAlign.start, // default: TextAlign.center
contentPadding: const EdgeInsets.symmetric(horizontal: 6, vertical: 10),
// ...
)
By default labels are 10.5 when the segment has a visual and 13 when it
is text-only.
Height: auto, fixed, min and max #
| Setup | Result |
|---|---|
height: null (default) |
Calculated from visual size, font size, labelMaxLines, spacing, padding and the device text scale |
height: 64 |
Fixed height |
| Any of the above | Always clamped to minHeight..maxHeight |
LiquidSegmentedBar<Grenade>(
minHeight: 48, // default 44, the recommended minimum touch target
maxHeight: 80, // default 120
// ...
)
The auto height reserves room for the tallest possible segment, so the bar
does not jump when the selection changes, even in selectedOnly mode.
Overflow handling #
The bar is built not to throw RenderFlex overflowed errors:
- Long labels are cut off with an ellipsis at the segment width. Allow
more lines with
labelMaxLines. - Too little height or width (a tight parent, large text scale, big icons): the segment content scales down to fit.
- Wide images are fitted inside the visual box with
BoxFit.contain.
Styling #
LiquidSegmentedBar<Grenade>(
accent: const Color(0xFF7CF28F), // bubble + selected icon
foregroundColor: Colors.white, // labels/icons (unselected = 55% opacity)
labelStyle: GoogleFonts.poppins(), // base text style (font family etc.)
blurSigma: 24, // glass blur strength
// ...
)
accentdefaults toTheme.of(context).colorScheme.primary, so the bar follows your theme automatically.labelStyleis a base style; its color, weight and size are set by the bar (useforegroundColor,labelFontSize,selectedLabelFontSize).
Animation and haptics #
LiquidSegmentedBar<Grenade>(
animationDuration: const Duration(milliseconds: 300),
animationCurve: Curves.easeOutCubic, // default: Curves.easeOutBack (springy)
haptics: false, // default: true (selection click)
// ...
)
Segments also scale slightly and fade when they lose selection; tapping the
already selected segment does nothing and does not call onChanged.
Accessibility #
- Each segment is exposed as a button with its
labeland selected state. - Labels hidden by
selectedOnlyorneverare still announced by screen readers. - Auto height follows the system text scale.
minHeightdefaults to 44, the recommended minimum touch target.
API reference #
LiquidSegmentedBar<T> #
| Parameter | Type | Default | Description |
|---|---|---|---|
segments |
List<LiquidSegment<T>> |
required | Options, at least one |
selected |
T |
required | Currently selected value |
onChanged |
ValueChanged<T> |
required | Called with the tapped value |
accent |
Color? |
ColorScheme.primary |
Bubble and selected icon color |
foregroundColor |
Color |
Colors.white |
Label/icon color (unselected = 55% opacity) |
height |
double? |
null (auto) |
Fixed height, still clamped |
minHeight |
double |
44 |
Minimum height |
maxHeight |
double |
120 |
Maximum height |
labelBehavior |
LiquidLabelBehavior |
always |
When labels are shown |
iconSize |
double |
20 |
Size of icon |
imageSize |
double |
22 |
Size of asset / visual / visualBuilder |
labelFontSize |
double? |
10.5 / 13 |
Label font size |
selectedLabelFontSize |
double? |
labelFontSize |
Selected label font size |
labelStyle |
TextStyle? |
TextTheme.labelSmall |
Base text style |
labelMaxLines |
int |
1 |
Lines before ellipsis |
labelAlign |
TextAlign |
TextAlign.center |
Horizontal label alignment |
labelSpacing |
double |
3 |
Gap between visual and label |
contentPadding |
EdgeInsets |
h: 4, v: 8 |
Inner padding of each segment |
assetBuilder |
LiquidAssetBuilder? |
defaultAssetBuilder |
Asset renderer for this bar |
blurSigma |
double |
20 |
Backdrop blur strength |
haptics |
bool |
true |
Selection click on tap |
animationDuration |
Duration |
380ms |
Bubble slide duration |
animationCurve |
Curve |
Curves.easeOutBack |
Bubble slide curve |
Static
| Member | Type | Description |
|---|---|---|
defaultAssetBuilder |
LiquidAssetBuilder? |
App-wide asset renderer (e.g. for SVG support) |
LiquidSegment<T> #
| Field | Type | Description |
|---|---|---|
value |
T |
Required. Value passed to onChanged |
label |
String |
Required. Label and semantics text |
icon |
IconData? |
Icon, also the fallback for failed assets |
asset |
String? |
Asset path or http(s) URL |
visual |
Widget? |
Any custom widget |
visualBuilder |
LiquidVisualBuilder? |
Custom widget that reacts to selection state |
LiquidLabelBehavior #
always · selectedOnly · never
Typedefs #
typedef LiquidAssetBuilder =
Widget Function(BuildContext context, String source, LiquidVisualState state);
typedef LiquidVisualBuilder =
Widget Function(BuildContext context, LiquidVisualState state);
Tips and FAQ #
How many segments should I use? All segments share the same width, so 2–6 options work best. For more options, consider a scrollable chip list.
Can the selected value be nullable (for an "All" option)?
Yes, use a nullable type such as LiquidSegmentedBar<Category?> with a
segment whose value is null.
What if selected matches no segment?
The bubble rests on the first segment.
Why don't I see the blur?
There is nothing behind the bar. Place it over an image, gradient or list
(e.g. in a Stack).
My SVG shows an icon instead. No asset builder is registered. See SVG: bring your own package.
I get "recommended image formats" in the console. It is only a debug hint for formats other than PNG/SVG. The image still renders and the message is not shown in release builds.
Support #
If this package saves you time, you can support its development:
Stars, issues and pull requests are also very welcome.
License #
MIT. See LICENSE.
