Bottom Tab Bar
Mobile app tab bar with an indicator that slides and stretches between tabs, an icon that springs up on selection, and a notification badge that pops.
About This Component
The bottom navigation of a mobile app or an installable web app: five tabs of icon and label, with an indicator that stretches toward the tab it is travelling to and springs back to that tab's width when it lands. The selected icon comes up on an elastic ease, its label fades in beneath it, and a notification badge pops on the tab that has unread items.
Read the full effect overview
It is a plain nav of links, so it works with no JavaScript and reads correctly to a screen reader, and it pads itself against the home indicator with env(safe-area-inset-bottom). Ships in three looks, vault, soft and brutal, switched by one data-variant attribute. One HTML file, one stylesheet, one script, core GSAP only.
What's Included
- Indicator slides and stretches between tabs, then springs back to width
- Selected icon rises on an elastic ease and its label fades in under it
- Notification badge pops in on a back ease and clears when its tab is opened
- Measured offsets, so four tabs or six need no code change
- Safe-area padding keeps the bar clear of the home indicator
- Built from a nav of links: no JavaScript still navigates
- Left and right arrow keys walk the bar, with a visible focus ring
- Three shipped variants: vault, soft, brutal
Perfect For
- Installable web apps and mobile-first products
- Bottom navigation inside a phone-width app shell
- Section switching in a mobile dashboard
- Prototypes and design reviews that need a real tab bar rather than a picture of one
How It Works
The stretch is the morph
Selecting a tab starts a short timeline. The indicator's x tweens to the measured offset on power3, its width overshoots by a fraction of the distance travelled, and then settles back to the target tab's own width on elastic.out. A long jump therefore stretches further than a jump to the next tab along, which is what makes the movement read as one shape moving rather than a box being repositioned.
Measured, never offset twice
The indicator keeps left: 0 in CSS and takes its whole horizontal position from getBoundingClientRect, with the bar's own border width subtracted once because the rect is a border box and the indicator sits against the padding box. It re-measures on resize and after web fonts land, so labels of any width stay covered.
Spring and badge
The selected icon is tweened with fromTo, from slightly low and small to slightly high and large on elastic.out, so the bounce happens every time rather than only on the first selection. The previous tab's icon returns on a plain power2 ease. Start states are set by GSAP rather than CSS, so nothing carries a transform the tweens would add to.