Liquid Glass Easy

Tab bar

Tabs in one glass capsule, no scaffold required.

LiquidGlassTabBar is the bar on its own: you place it, you keep the index, you decide what the page does with it. Like the app bar it is a single lens, so on Skia it wants an ancestor LiquidGlassView whose background is the page it should refract.

Dart
LiquidGlassTabBar(
  items: const [
    LiquidGlassTabBarItem(
      icon: Icons.today_outlined,
      selectedIcon: Icons.today_rounded,
      label: 'Today',
    ),
    LiquidGlassTabBarItem(icon: Icons.bar_chart_rounded, label: 'Charts'),
  ],
  selectedIndex: index,
  onChanged: (i) => setState(() => index = i),
  width: 330,
)

That background does not have to be the page. The tab bar in a card demo centres the bar in a card and hands it the card's own surface, so the glass stops at the card rim — the page around it is never captured and never bends, and there is nothing behind the bar but the card's material. Which pipeline the bar needs is the one decision to make, and resolveGlassPill() answers it for the current renderer: true and the glass pill wants the bar's own dual-view machinery through buildGlassPillBar(body: …); false and the flat tier is a single lens, so one LiquidGlassView over the card is enough.

Dart
final bar = LiquidGlassTabBar(
  items: _items,
  selectedIndex: index,
  onChanged: (i) => setState(() => index = i),
  width: cardWidth - 26,
  // The card is the whole world the bar sits in: dead centre, no gap.
  alignment: Alignment.center,
  margin: EdgeInsets.zero,
);

SizedBox(
  width: cardWidth,
  height: 112,
  child: ClipRRect(
    borderRadius: BorderRadius.circular(26),
    child: bar.resolveGlassPill()
        ? bar.buildGlassPillBar(body: cardSurface)
        : LiquidGlassView(
            backgroundWidget: cardSurface,
            child: Center(child: bar),
          ),
  ),
)
ParameterDefaultWhat it does
itemsLiquidGlassTabBarItems: icon, selected icon, label.
selectedIndex / onChangedYou own the index.
width / height320 / 64The capsule.
margin / itemPadding / alignmentWhere the bar sits and how the items are spaced inside it.
itemStyleLiquidGlassTabItemStyle()Icon and label colours, sizes and weights — see below.
pillStyleLiquidGlassTabPillStyle()Everything about the selection marker — see below.
style / visibilitynull / trueThe bar capsule’s own glass, and the switch that turns it off.

Styling the items#

Colours and sizes are grouped rather than flat, so a bar's look travels as one value. underGlassIconSize and underGlassLabelFontSize are the interesting pair: a glass-pill bar draws each tab twice — once normally and once under the pill — and those two let the copy under the glass be a different size, which is how the magnification is cancelled out.

Dart
itemStyle: const LiquidGlassTabItemStyle(
  selectedColor: Colors.white,
  unselectedColor: Colors.white70,
  iconSize: 24,
  underGlassIconSize: 21,      // cancels the pill's magnification
  labelFontSize: 10.5,
  selectedFontWeight: FontWeight.w600,
)

Custom glyphs#

Tabs are not limited to IconData. Pass an iconBuilder — or a labelBuilder — and an SvgPicture, an Image or a CustomPaint all work.

Dart
LiquidGlassTabBarItem(
  label: 'Home',
  iconBuilder: (context, i) => SvgPicture.asset(
    i.selected ? 'assets/home_fill.svg' : 'assets/home.svg',
    width: i.size,
    height: i.size,
    colorFilter: ColorFilter.mode(i.color, BlendMode.srcIn),
  ),
)

The builder is handed a LiquidGlassGlyph: the colour the bar already resolved for the layer being drawn, the glyph box size, whether that layer is the selected one, and whether it is the copy under the glass. Tint with i.color and your artwork follows the palette and the morph pill's reveal. Multi-colour art can ignore the colour entirely.

The two tiers#

`pillStyle.mode`What the selection is
noneA translucent highlight inside the bar’s single lens. Instant, or a soft slide with animated: true. Cheap everywhere.
impellerOnlyThe glass morph pill where it is free, the highlight on Skia.
both (default)The morph pill on both engines — a second refracting surface that bends the bar itself. On Skia, only under lite glass — see the note below.

The rest of the pill#

LiquidGlassTabPillStyle carries the whole selection: whether it shows at all, its colour and blur, its shape, the glass it wears when lifted and the plain style it wears at rest, how fast it travels, and how it deforms on the way.

FieldDefaultWhat it does
showtrueDraw a selection marker at all.
animatedfalseIn none mode, slide the highlight instead of snapping it.
color / blur0x2EAEAEB2 / —The marker’s fill and its backdrop blur.
growHeight9How much taller than the item row the pill is.
distortion / distortionWidth / magnification0.04 / 12 / 1The pill’s own refraction, over the bar beneath it.
travelStiffness / travelDamping280 / 31.4The spring that carries it between tabs.
glassStyle / rest / shapenullFull overrides for the lifted look, the resting look and the outline.

The moving pill deforms as it travels, on the same model the slider's thumb uses: pillStyle.motion is a LiquidGlassLensMotionSpec, tuned here to a gentler ceiling for tab-scale travel. See Motion.

Standalone bar on Impeller#

Dart
Stack(
  children: [
    MyPage(),
    LiquidGlassTabBar.withImpeller(
      items: items,
      selectedIndex: index,
      onChanged: (i) => setState(() => index = i),
    ),
  ],
)

.withImpeller is the bodyless bar: no scaffold, no page handed in. It expands to fill, so drop it as the last child of a Stack. On Skia it falls back to a frosted bar that still shows content through it.

Comments

Comments are GitHub Discussions — reply from either place.