Navigation

Glass Tab Bar

A frosted-glass mobile bottom tab bar React component. Icons pop in one by one on mount, the active pill slides between tabs and the active label expands, with an optional raised centre button and unread badges.

A frosted-glass tab bar for the bottom of a mobile app. On mount each icon pops in turn on a slightly bouncy spring; when you switch tabs, the white active pill slides from the previous tab to the new one while the active tab expands from icon-only to icon + label. With an odd number of items you can turn on raisedCenter to make the middle item a raised round primary button (for example "New"); badges support unread counts (99+ above 99) and a small dot, and scale once whenever the count changes. The active value supports controlled and uncontrolled modes.

Bring this into your project

Run this command in your project folder. The CLI adds the source and dependencies; then follow the usage example below.

npx webberui@latest add glass-tab-bar.en

New project or installation trouble? Start here →

Playground

Tune the props live — the code snippet updates as you go, so you can dial in the look you want before copying it.

18
60
Active edited usageDefault props
<GlassTabBar/>

The complete example includes the preview layout and data. Install the component first, then paste this file into your project.

Installation

npx webberui@latest add https://webberui.com/r/glass-tab-bar.en.json

Or, once registries are configured in components.json, install it as @webberui/glass-tab-bar.

Usage

import { Compass, House, MessageCircle, Plus, User } from "lucide-react";
import { GlassTabBar } from "@/components/ui/glass-tab-bar";

const [tab, setTab] = React.useState("home");

<GlassTabBar
  position="fixed"
  raisedCenter
  value={tab}
  onValueChange={setTab}
  onCenterPress={() => openComposer()}
  items={[
    { id: "home", icon: <House />, label: "Home" },
    { id: "explore", icon: <Compass />, label: "Find" },
    { id: "create", icon: <Plus />, label: "New" },
    { id: "messages", icon: <MessageCircle />, label: "Inbox", badge: 3 },
    { id: "me", icon: <User />, label: "Me" },
  ]}
/>

Props

PropTypeDefaultDescription
itemsGlassTabBarItem[]—The list of tabs
valuestring—Controlled active id; when omitted the component manages it internally (uncontrolled)
defaultValuestringfirst item's idInitial active id in uncontrolled mode
onValueChange(value: string) => void—Called when the active tab changes, with the new id
raisedCenterbooleanfalseWith an odd number of items (at least 3), turn the middle one into a raised round primary button
onCenterPress() => void—Called when the centre button is pressed
centerSelectablebooleanfalseWhether the centre button also changes the value; by default it only fires onCenterPress
blurnumber18Glass backdrop blur (px); falls back to --wb-gtb-blur
staggernumber60Delay between icons popping in on mount (ms); falls back to --wb-gtb-stagger
position"fixed" | "static""static"fixed pins it to the bottom of the viewport with safe-area padding; static keeps it in normal flow
ariaLabelstring"Main navigation"Accessible name of the nav
classNamestring—Forwarded to the outer nav

GlassTabBarItem

FieldTypeDefaultDescription
idstring—Unique id, also used as the value
iconReact.ReactNode—Tab icon; a 20px lucide icon is recommended
labelstring—Tab name; shown beside the icon when active, used as the accessible name otherwise
badgenumber | boolean—A number shows an unread count (99+ above 99); true shows a small dot

CSS variables

FieldTypeDefaultDescription
--wb-gtb-blurlength18pxGlass backdrop blur
--wb-gtb-staggertime60msDelay between icon pops (the stagger prop wins)
--wb-gtb-highlightcolorrgba(255,255,255,0.6)1px inner highlight along the top edge
--wb-gtb-accentcolor#171717 (dark: #f5f5f5)Centre button fill
--wb-gtb-badgecolor#ef4444Badge fill

How it works

  • Glass material: a translucent white fill (neutral-900/55 in dark mode) with backdrop-blur and backdrop-saturate-150, a 1px inner highlight along the top edge, a faint 1px border and a soft outer shadow, all in a fully rounded pill.
  • The active pill slides between tabs with motion's layoutId; the layoutId is prefixed with useId, so several bars on one page never interfere with each other.
  • The active tab's width expands with a layout animation and its label fades in after a 60ms delay while sliding 4px in from the left; on first render it is shown directly, without the expand animation.
  • The centre button is 56px across, floats 18px up and does not take part in the pill slide; it is not a tab, and pressing it only calls onCenterPress unless centerSelectable is on.
  • The mount pop waits until the component has mounted, when the user's motion preference and the CSS variable values are both known, so the SSR first render matches the client.

Accessibility

  • The outer element is a nav (default aria-label "Main navigation") containing a role="tablist"; each tab is a role="tab" with aria-selected
  • Each tab's aria-label is its label, with ", 3 unread" or ", new activity" appended when it has a badge; the visual badge and expanded label are hidden from assistive technology to avoid double announcements
  • Roving tabindex: only the active tab is in the Tab order; ← / → cycle, Home / End jump to the ends, and moving also selects (automatic activation)
  • The centre button is a plain button (outside the tablist) using its item label as its aria-label; with centerSelectable on and selected it sets aria-current="page"
  • Every button is type="button" and has a focus-visible ring
  • When the user has reduce motion enabled at the system level, icons do not pop in, the pill jumps straight into place, badges do not scale and the label appears directly