suiss/ui
  • Docs
  • Components
  • Patterns
  • Interactions
  • Charts
  • Blocks
GitHubGet started
DocsComponentsPatternsInteractionsChartsBlocks
Components82
  • Overview
  • Accordion
  • Alert
  • Alert Dialog
  • Alert Sheet
  • Amount Input
  • Aspect Ratio
  • Attachment
  • Avatar
  • Badge
  • Bottom Bar
  • Breadcrumb
  • Bubble
  • Button
  • Button Group
  • Calendar
  • Card
  • Carousel
  • Chart
  • Checkbox
  • Chip
  • Collapsible
  • Combobox
  • Command
  • Context Menu
  • Dialog
  • Direction
  • Drawer
  • Dropdown Menu
  • Empty
  • FAB
  • Field
  • Fullscreen Menu
  • Haptics
  • Header
  • Hover Card
  • Icon
  • Input
  • Input Accessory Bar
  • Input Group
  • Input OTP
  • Item
  • Kbd
  • Keyboard Scroll View
  • Keypad
  • Label
  • Marker
  • Menubar
  • Message
  • Message Scroller
  • Native Select
  • Navigation Menu
  • Page Indicator
  • Pagination
  • Pin Pad
  • Popover
  • Progress
  • QR Scanner
  • Questionnaire
  • Radio Group
  • Resizable
  • Screen
  • Scroll Area
  • Select
  • Separator
  • Sheet
  • Sidebar
  • Signature Pad
  • Skeleton
  • Slider
  • Spinner
  • Switch
  • Tab Bar
  • Table
  • Tabs
  • Text
  • Textarea
  • Toast
  • Toggle
  • Toggle Group
  • Tooltip
  • Wheel Picker
  • Widget

suiss/ui · Component

Tab Bar

A floating bottom navigation bar whose overflow and extra sections expand from a More button.

Tab Bar is an open-source component from suiss/ui for React, React Native, SwiftUI and Jetpack Compose. The @esuiss/ui CLI copies its source into your project, so you own the code.

TabBar renders a fixed nav centered at the bottom of the screen. TabBarList holds the primary TabBarItem buttons. TabBarTrigger is the More button: pressing it expands TabBarContent above the list, with a spring animation, to show TabBarSection columns. Each section has a TabBarSectionTitle and a set of TabBarLink entries.

The list measures itself and moves items that don't fit into the expanded content, where they appear as links. When every item fits and there are no sections, the More button is hidden. Use TabBar for top-level navigation on any screen size; for in-page panels, use Tabs, and for an app's side navigation, use Sidebar.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/tab-bar

Works in any project with a components.json. See Installation for the one-line registry setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add tab-bar

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react @phosphor-icons/react
  1. Add the pieces it builds on: Button.

  2. Copy the source into components/ui/tab-bar.tsx:

  1. Update the import paths to match your project.

Usage

import { TabBar, TabBarList, TabBarItem, TabBarTrigger, TabBarContent, TabBarSection, TabBarSectionTitle, TabBarLink } 
<TabBarExample

Use defaultOpen on TabBar for an uncontrolled bar, or open with onOpenChange to control it. Give the nav a name with aria-label.

Mark the current destination with isActive on a TabBarItem or TabBarLink. A TabBarItem can render as a link or router link. Choosing a TabBarLink or an overflowed item closes the bar. Child components can read the bar's state with the useTabBar hook.

Use the render prop to render a TabBarItem as a link or router link.

Set asChild on a TabBarItem and pass a link or router link as the child.

Pass a render function to a TabBarItem that returns a link or router link with the props it receives.

Examples

Open

Renders with the sections expanded on mount; an outside click closes them.

Fits without more

Hides the More button because every item fits and there are no sections.

Overflow into more

Holds more items than fit, so the extra items move behind More.

Links

Renders each item as a link, with the active one marked as the current page.

Disabled item

Disables one item in the list.

API Reference

Anatomy5 parts
<TabBar>
<TabBarList>
<TabBarItem />
</TabBarList>
<TabBarContent />
<TabBarTrigger />
</TabBar>
01

TabBar

The floating nav container that holds the open state.

PropTypeDefaultDescription
openboolean-Whether the More panel is open, for controlled use.
defaultOpenbooleanfalseWhether the More panel starts open when uncontrolled.
onOpenChange(open: boolean) => void-Called when the More panel opens or closes.

Also accepts the props of React.ComponentProps<"nav">, TabBarProps.

02

TabBarList

The row of primary items; items that don't fit move into the content.

Also accepts the props of React.ComponentProps<"div">.

03

TabBarItem

A primary destination in the list, rendered as a button by default.

PropTypeDefaultDescription
isActivebooleanfalseMarks the item as the current page with aria-current="page".

Also accepts the props of useRender.ComponentProps<"button">, React.ComponentProps<"button">.

04

TabBarTrigger

The More button that expands and collapses the content.

Also accepts the props of React.ComponentProps<typeof Button>.

05

TabBarContent

The expandable area above the list that holds overflowed items and sections.

Also accepts the props of React.ComponentProps<"div">.

06

TabBarSection

A column of related links in the content.

Also accepts the props of React.ComponentProps<"div">.

07

TabBarSectionTitle

The heading of a section, marked with a small dot.

Also accepts the props of React.ComponentProps<"div">.

08

TabBarLink

A link inside a section that closes the bar when chosen.

PropTypeDefaultDescription
isActivebooleanfalseMarks the link as the current page with aria-current="page".

Also accepts the props of useRender.ComponentProps<"a">, React.ComponentProps<"a">.

Accessibility

  • TabBar is a navigation landmark; name it with aria-label.
  • Active items and links set aria-current="page".
  • The More button sets aria-expanded and aria-controls to point to the content.
  • Escape closes the open bar and returns focus to the More button. A pointer press outside the bar also closes it.
  • While open, the list is inert. Items moved into the overflow are hidden from assistive technologies and removed from the tab order in the list, and appear as links in the content instead.
  • When the More button isn't needed, it's hidden and removed from the tab order.

Notes

  • TabBar sets data-open, data-overflowing and data-sections for styling. The trigger sets data-idle when it's hidden, and items and links set data-active.
  • Parts carry data-slot attributes such as tab-bar, tab-bar-list, tab-bar-item, tab-bar-trigger and tab-bar-content.
  • The bar sits above the safe-area inset at the bottom of the screen and uses a card background with a thin border and a shadow. The active item gets a secondary fill and primary text.
  • Animations are removed when reduced motion is preferred.
  • useTabBar throws if it's used outside a TabBar.

TabBar is built on the tabs primitive and sits at the bottom of a screen. TabBarList is a rounded capsule with a shadow, inset from the screen edges, so the bar floats above the page. The root adds bottom padding for the safe area, minus any inset an enclosing Screen already handles, and never less than 12 points. TabBarItem stacks an icon over a short label; the active tab sits on a bg-secondary pill and uses text-primary and a filled icon weight.

Use it for switching between the top-level sections of an app. For switching views within a screen, use Tabs.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/tab-bar

See Installation for the components.json setup. Or use the suiss/ui CLI:

$ pnpm dlx @esuiss/ui@latest add tab-bar
$ pnpm dlx @esuiss/ui@latest add tab-bar --platform swift
NextTablePreviousSwitch

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Open
    • Fits without more
    • Overflow into more
    • Links
    • Disabled item
  4. 04API Reference
    • TabBar
    • TabBarList
    • TabBarItem
    • TabBarTrigger
    • TabBarContent
    • TabBarSection
    • TabBarSectionTitle
    • TabBarLink
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • Without badges
    • Large badge
    • Disabled item
  10. 10API Reference
    • TabBar
    • TabBarList
    • TabBarItem
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/tab-bar"
/>
$ pnpm dlx @esuiss/ui@latest add tab-bar --platform kotlin

The CLI also adds what it builds on: Icon, Screen, Text.

Manual

  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-safe-area-context react-native-svg
  1. Add the pieces it builds on: Icon, Screen, Text, and the primitives components/primitives/haptics.tsx, components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tab-bar.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install cn phosphor-react-native react-native-safe-area-context react-native-svg
  1. Add the pieces it builds on: Icon, Screen, Text, and the primitives components/primitives/haptics.tsx, components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tab-bar.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install phosphor-react-native react-native-safe-area-context react-native-svg
  1. Add the pieces it builds on: Icon, Screen, Text, and the primitives components/primitives/haptics.tsx, components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tab-bar.tsx:

  1. Update the import paths to match your project.
  1. Target iOS 17 or later.

  2. Add the Swift package to your app:

Package.swift
.package(url: "https://github.com/phosphor-icons/swift", from: "2.1.0")
  1. Copy the theme files every view uses:
swift/Theme/Theme.swift
import SwiftUI
import UIKit

struct RGBA: Sendable {
  let red: CGFloat
  let green: CGFloat
  let blue: CGFloat
  let alpha: CGFloat

  init(_ hex: UInt32, _ alpha: CGFloat = 1) {
    red = CGFloat((hex >> 16) & 0xFF) / 255
    green = CGFloat((hex >> 8) & 0xFF) / 255
    blue = CGFloat(hex & 0xFF) / 255
    self.alpha = alpha
  }
swift/Theme/Tokens.swift
import SwiftUI

public enum EsuissColor {
  public static let background = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x000000))
  public static let foreground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let card = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let cardForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let popover = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0x1D1D1F))
  public static let popoverForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let primary = Color.adaptive(light: .init(0x0071E3), dark: .init(0x2997FF))
  public static let primaryForeground = Color.adaptive(light: .init(0xFFFFFF), dark: .init(0xFFFFFF))
  public static let secondary = Color.adaptive(light: .init(0x000000, 0.04), dark: .init(0xFFFFFF, 0.1))
  public static let secondaryForeground = Color.adaptive(light: .init(0x1D1D1F), dark: .init(0xF5F5F7))
  public static let muted = Color.adaptive(light: .init(0xF5F5F7), dark: .init(0x161617))
  public static let mutedForeground = Color.adaptive(light: .init(0x6E6E73), dark: .init(0xA1A1A6))
  public static let accent = Color.adaptive(light: .init(0x000000, 0.06), dark: .init(0xFFFFFF, 0.1))
  1. Copy the source and every file it uses:
swift/Components/Button.swift
import SwiftUI

public struct VariantButtonStyle: ButtonStyle {
  public enum Variant: CaseIterable, Sendable {
    case `default`, outline, secondary, tinted, ghost, plain, destructive, link
  }

  public enum Size: CaseIterable, Sendable {
    case `default`, xs, sm, lg, xl, icon, iconXs, iconSm, iconLg, iconXl

    var height: CGFloat {
      switch self {
      case .xs, .iconXs: 24
      case .sm, .iconSm: 32
      case .default, .icon: 36
      case .lg, .iconLg: 44
swift/Components/ButtonGroup.swift
import SwiftUI

extension EnvironmentValues {
  @Entry var buttonGroup: Axis? = nil
  @Entry var buttonGroupSize: CGSize = .zero
}

enum ButtonGroupSpace: Hashable {
  case group
}

struct ButtonGroupFrame {
  let bounds: Anchor<CGRect>
  let outlined: Bool
}
swift/Components/DropdownMenu.swift
import PhosphorSwift
import SwiftUI

enum MenuPresentation: Sendable {
  case system, popup
}

extension EnvironmentValues {
  @Entry var menuPresentation: MenuPresentation = .system
  @Entry var menuRadio = MenuRadioContext()
}

struct MenuRadioContext: @unchecked Sendable {
  var selected: AnyHashable?
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}
swift/Components/Field.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var isInvalid = false
  @Entry var fieldInset = false
  @Entry var fieldGroupInset = false
  @Entry var fieldSetInset = false
}

extension View {
  public func invalid(_ isInvalid: Bool = true) -> some View {
    environment(\.isInvalid, isInvalid)
  }
}

public enum FieldOrientation: CaseIterable, Sendable {
swift/Components/Haptics.swift
import SwiftUI

extension EnvironmentValues {
  @Entry public var hapticsEnabled = true
}

extension View {
  public func haptics(_ enabled: Bool) -> some View {
    environment(\.hapticsEnabled, enabled)
  }

  func hapticFeedback<Trigger: Equatable>(_ feedback: SensoryFeedback, trigger: Trigger) -> some View {
    modifier(HapticFeedbackModifier(feedback: feedback, trigger: trigger) { _, _ in true })
  }

  func hapticFeedback<Trigger: Equatable>(
swift/Components/Overlay.swift
import PhosphorSwift
import Observation
import SwiftUI

public enum PopupAlignment: Sendable, CaseIterable {
  case start, center, end
}

struct OverlayDismiss: Sendable {
  let action: @MainActor @Sendable () -> Void

  @MainActor func callAsFunction() {
    action()
  }
}
swift/Components/Select.swift
import PhosphorSwift
import SwiftUI

struct SelectContext: @unchecked Sendable {
  var selected: AnyHashable?
  var registering = false
  var select: @MainActor @Sendable (AnyHashable) -> Void = { _ in }
}

extension EnvironmentValues {
  @Entry var select = SelectContext()
}

struct SelectLabelsKey: PreferenceKey {
  static var defaultValue: [AnyHashable: Text] { [:] }
swift/Components/Separator.swift
import SwiftUI

public struct Separator: View {
  let orientation: Axis

  public init(_ orientation: Axis = .horizontal) {
    self.orientation = orientation
  }

  public var body: some View {
    Rectangle()
      .fill(EsuissColor.border)
      .frame(width: orientation == .vertical ? 1 : nil, height: orientation == .horizontal ? 1 : nil)
      .frame(maxWidth: orientation == .horizontal ? .infinity : nil, maxHeight: orientation == .vertical ? .infinity : nil)
      .accessibilityHidden(true)
  }
swift/Components/TextVariant.swift
import SwiftUI

public enum TextVariant: CaseIterable, Sendable {
  case `default`, h1, h2, h3, h4, p, blockquote, code, lead, large, small, muted

  var style: TextStyle {
    switch self {
    case .h1: .xl3
    case .h2: .xl2
    case .h3: .xl
    case .h4, .lead, .large: .lg
    case .code, .small, .muted: .sm
    default: .base
    }
  }
  1. Add the files to your app target.
  1. Set minSdk to 24 or higher and add the dependencies:
build.gradle.kts
implementation(platform("androidx.compose:compose-bom:2026.09.00"))
implementation("androidx.compose.foundation:foundation")
implementation("androidx.compose.ui:ui")
implementation("com.adamglin:phosphor-icon-android:1.0.0")
  1. Copy the theme files every view uses:
kotlin/theme/Theme.kt
package com.esuiss.ui.theme

import androidx.compose.foundation.isSystemInDarkTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.Immutable
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.dropShadow
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.Shape
import androidx.compose.ui.graphics.shadow.Shadow as DropShadow
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.text.style.LineHeightStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.DpOffset
kotlin/theme/Tokens.kt
package com.esuiss.ui.theme

import androidx.compose.runtime.Immutable
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.unit.dp

@Immutable
data class Colors(
    val background: Color,
    val foreground: Color,
    val card: Color,
    val cardForeground: Color,
    val popover: Color,
    val popoverForeground: Color,
    val primary: Color,
    val primaryForeground: Color,
  1. Copy the source and every file it uses:
kotlin/components/Button.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.clickable
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.size
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.RoundedCornerShape
kotlin/components/ButtonGroup.kt
package com.esuiss.ui.components

import androidx.compose.foundation.background
import androidx.compose.foundation.gestures.Orientation
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.shape.CircleShape
import androidx.compose.foundation.shape.CornerSize
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.Immutable
kotlin/components/Content.kt
package com.esuiss.ui.components

import androidx.compose.runtime.compositionLocalOf
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.text.TextStyle
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.dp

val LocalContentStyle = compositionLocalOf { TextStyle.Default }

val LocalContentColor = compositionLocalOf { Color.Unspecified }

val LocalIconSize = compositionLocalOf { 20.dp }
kotlin/components/Haptics.kt
package com.esuiss.ui.components

import androidx.compose.runtime.Composable
import androidx.compose.runtime.CompositionLocalProvider
import androidx.compose.runtime.remember
import androidx.compose.runtime.staticCompositionLocalOf
import androidx.compose.ui.hapticfeedback.HapticFeedback
import androidx.compose.ui.hapticfeedback.HapticFeedbackType
import androidx.compose.ui.platform.LocalHapticFeedback

val LocalHapticsEnabled = staticCompositionLocalOf { true }

@Composable
fun ProvideHaptics(enabled: Boolean, content: @Composable () -> Unit) {
    CompositionLocalProvider(LocalHapticsEnabled provides enabled, content = content)
}
kotlin/components/Icon.kt
package com.esuiss.ui.components

import androidx.compose.foundation.Image
import androidx.compose.foundation.layout.size
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.Color
import androidx.compose.ui.graphics.ColorFilter
import androidx.compose.ui.graphics.takeOrElse
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.Dp
import com.esuiss.ui.theme.EsuissTheme

@Composable
fun Icon(
    imageVector: ImageVector,
kotlin/components/Overlay.kt
package com.esuiss.ui.components

import android.os.Build
import android.view.Window
import android.view.WindowManager
import androidx.compose.animation.AnimatedVisibility
import androidx.compose.animation.AnimatedVisibilityScope
import androidx.compose.animation.EnterExitState
import androidx.compose.animation.core.MutableTransitionState
import androidx.compose.animation.core.animateFloat
import androidx.compose.animation.core.tween
import androidx.compose.animation.fadeIn
import androidx.compose.animation.fadeOut
import androidx.compose.animation.scaleIn
import androidx.compose.animation.scaleOut
import androidx.compose.animation.slideIn
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { TabBar, TabBarList, TabBarItem } from "@/components/ui/tab-bar"
<TabBarExample />

Put TabBarItem entries inside a TabBarList, inside TabBar. Give each item a value, an icon and a label as children. Control the active tab with value and onValueChange, or set defaultValue.

The badge prop shows a count, text or a dot: numbers above 99 show as 99+, 0 and empty strings show nothing, and true shows a small dot. A selection haptic plays whenever the tab changes, when a haptics adapter is provided through HapticsContext.

The TabBarBadgeValue type is also exported.

Examples

Without badges

The tab bar without badges.

iOS
Tab Bar (without badges) on iOS in light modeTab Bar (without badges) on iOS in dark mode
import { ChatCircleIcon, HouseIcon, MagnifyingGlassIcon, UserIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"

const items = [
  { value: "home", label: "Home", icon: HouseIcon },
  { value: "search", label: "Search", icon: MagnifyingGlassIcon },
  { value: "messages", label: "Messages", icon: ChatCircleIcon, badge: 3 },
  { value: "profile", label: "Profile", icon: UserIcon, badge: true },
]

function Page({
  title,
  children,
}: {
  title: string
  children: React.ReactNode
}) {
  return (
    <View className="-m-4 flex-1 bg-background">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">{title}</Text>
        <Text variant="muted">Tap a tab to switch screens.</Text>
      </View>
      {children}
    </View>
  )
}

function TabBarExample({
  badges = true,
  disabled,
}: {
  badges?: boolean
  disabled?: string
}) {
  const [value, setValue] = React.useState("home")
  const current = items.find((item) => item.value === value)
  return (
    <Page title={current?.label ?? ""}>
      <TabBar value={value} onValueChange={setValue}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={badges ? item.badge : undefined}
              disabled={item.value === disabled}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Page>
  )
}

export function TabBarWithoutBadgesExample() {
  return (
    <TabBarExample badges={false} />
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabBarWithoutBadgesExample: View {
  var body: some View {
    TabBarExample(badges: false)
  }
}

struct TabBarEntry {
  let value: String
  let label: String
  let icon: Image
  let activeIcon: Image
  let badge: TabBarBadge?
}

let tabBarEntries = [
  TabBarEntry(value: "home", label: "Home", icon: Ph.house.regular, activeIcon: Ph.house.fill, badge: nil),
  TabBarEntry(value: "search", label: "Search", icon: Ph.magnifyingGlass.regular, activeIcon: Ph.magnifyingGlass.fill, badge: nil),
  TabBarEntry(value: "messages", label: "Messages", icon: Ph.chatCircle.regular, activeIcon: Ph.chatCircle.fill, badge: 3),
  TabBarEntry(value: "profile", label: "Profile", icon: Ph.user.regular, activeIcon: Ph.user.fill, badge: .dot),
]

struct TabBarPage<Bar: View>: View {
  let title: String
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text(verbatim: title).textVariant(.h3)
        Text("Tap a tab to switch screens.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
    .background(EsuissColor.background.ignoresSafeArea())
  }
}

struct TabBarExample: View {
  var badges = true
  var disabled: String?

  @State private var value = "home"

  var body: some View {
    TabBarPage(title: tabBarEntries.first { $0.value == value }?.label ?? "") {
      TabBar(selection: $value) {
        ForEach(tabBarEntries, id: \.value) { item in
          TabBarItem(
            verbatim: item.label,
            value: item.value,
            icon: item.icon,
            activeIcon: item.activeIcon,
            badge: badges ? item.badge : nil
          )
          .disabled(item.value == disabled)
        }
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.House
import com.adamglin.phosphoricons.fill.MagnifyingGlass
import com.adamglin.phosphoricons.fill.User
import com.adamglin.phosphoricons.regular.ChatCircle
import com.adamglin.phosphoricons.regular.House
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun TabBarWithoutBadgesExample() {
    TabBarExample(badges = false)
}

class TabBarEntry(val value: String, val label: String, val icon: ImageVector, val selectedIcon: ImageVector, val badge: String? = null)

val tabBarEntries = listOf(
    TabBarEntry("home", "Home", PhosphorIcons.Regular.House, PhosphorIcons.Fill.House),
    TabBarEntry("search", "Search", PhosphorIcons.Regular.MagnifyingGlass, PhosphorIcons.Fill.MagnifyingGlass),
    TabBarEntry("messages", "Messages", PhosphorIcons.Regular.ChatCircle, PhosphorIcons.Fill.ChatCircle, "3"),
    TabBarEntry("profile", "Profile", PhosphorIcons.Regular.User, PhosphorIcons.Fill.User, ""),
)

@Composable
fun TabBarPage(title: String, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Text(title, variant = TextVariant.H3)
                Text("Tap a tab to switch screens.", variant = TextVariant.Muted)
            }
            bar()
        }
    }
}

@Composable
fun TabBarExample(badges: Boolean = true, disabled: String? = null) {
    var value by rememberSaveable { mutableStateOf("home") }
    val current = tabBarEntries.firstOrNull { it.value == value }
    TabBarPage(current?.label ?: "") {
        TabBar(value = value, onValueChange = { value = it }) {
            tabBarEntries.forEach { entry ->
                TabBarItem(
                    value = entry.value,
                    label = entry.label,
                    icon = entry.icon,
                    selectedIcon = entry.selectedIcon,
                    badge = if (badges) entry.badge else null,
                    enabled = entry.value != disabled,
                )
            }
        }
    }
}

Large badge

A count above 99 shown as 99+ and a text badge.

iOS
Tab Bar (large badge) on iOS in light modeTab Bar (large badge) on iOS in dark mode
import { ChatCircleIcon, GearSixIcon, HouseIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"

function Page({
  title,
  children,
}: {
  title: string
  children: React.ReactNode
}) {
  return (
    <View className="-m-4 flex-1 bg-background">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">{title}</Text>
        <Text variant="muted">Tap a tab to switch screens.</Text>
      </View>
      {children}
    </View>
  )
}

export function TabBarLargeBadgeExample() {
  return (
    <Page title="Home">
      <TabBar defaultValue="home">
        <TabBarList>
          <TabBarItem value="home" icon={HouseIcon}>
            Home
          </TabBarItem>
          <TabBarItem value="messages" icon={ChatCircleIcon} badge={128}>
            Messages
          </TabBarItem>
          <TabBarItem value="settings" icon={GearSixIcon} badge="New">
            Settings
          </TabBarItem>
        </TabBarList>
      </TabBar>
    </Page>
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabBarPage<Bar: View>: View {
  let title: String
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text(verbatim: title).textVariant(.h3)
        Text("Tap a tab to switch screens.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
    .background(EsuissColor.background.ignoresSafeArea())
  }
}

struct LargeBadgeDemo: View {
  @State private var value = "home"

  var body: some View {
    TabBarPage(title: "Home") {
      TabBar(selection: $value) {
        TabBarItem("Home", value: "home", icon: Ph.house.regular, activeIcon: Ph.house.fill)
        TabBarItem("Messages", value: "messages", icon: Ph.chatCircle.regular, activeIcon: Ph.chatCircle.fill, badge: 128)
        TabBarItem("Settings", value: "settings", icon: Ph.gearSix.regular, activeIcon: Ph.gearSix.fill, badge: "New")
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.GearSix
import com.adamglin.phosphoricons.fill.House
import com.adamglin.phosphoricons.regular.ChatCircle
import com.adamglin.phosphoricons.regular.GearSix
import com.adamglin.phosphoricons.regular.House
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun TabBarPage(title: String, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Text(title, variant = TextVariant.H3)
                Text("Tap a tab to switch screens.", variant = TextVariant.Muted)
            }
            bar()
        }
    }
}

@Composable
fun TabBarLargeBadge() {
    var value by rememberSaveable { mutableStateOf("home") }
    TabBarPage("Home") {
        TabBar(value = value, onValueChange = { value = it }) {
            TabBarItem(value = "home", label = "Home", icon = PhosphorIcons.Regular.House, selectedIcon = PhosphorIcons.Fill.House)
            TabBarItem(value = "messages", label = "Messages", icon = PhosphorIcons.Regular.ChatCircle, selectedIcon = PhosphorIcons.Fill.ChatCircle, badge = "128")
            TabBarItem(value = "settings", label = "Settings", icon = PhosphorIcons.Regular.GearSix, selectedIcon = PhosphorIcons.Fill.GearSix, badge = "New")
        }
    }
}

Disabled item

A tab bar with one disabled tab.

iOS
Tab Bar (disabled item) on iOS in light modeTab Bar (disabled item) on iOS in dark mode
import { ChatCircleIcon, HouseIcon, MagnifyingGlassIcon, UserIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"

const items = [
  { value: "home", label: "Home", icon: HouseIcon },
  { value: "search", label: "Search", icon: MagnifyingGlassIcon },
  { value: "messages", label: "Messages", icon: ChatCircleIcon, badge: 3 },
  { value: "profile", label: "Profile", icon: UserIcon, badge: true },
]

function Page({
  title,
  children,
}: {
  title: string
  children: React.ReactNode
}) {
  return (
    <View className="-m-4 flex-1 bg-background">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">{title}</Text>
        <Text variant="muted">Tap a tab to switch screens.</Text>
      </View>
      {children}
    </View>
  )
}

function TabBarExample({
  badges = true,
  disabled,
}: {
  badges?: boolean
  disabled?: string
}) {
  const [value, setValue] = React.useState("home")
  const current = items.find((item) => item.value === value)
  return (
    <Page title={current?.label ?? ""}>
      <TabBar value={value} onValueChange={setValue}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={badges ? item.badge : undefined}
              disabled={item.value === disabled}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Page>
  )
}

export function TabBarDisabledItemExample() {
  return (
    <TabBarExample disabled="search" />
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabBarDisabledItemExample: View {
  var body: some View {
    TabBarExample(disabled: "search")
  }
}

struct TabBarEntry {
  let value: String
  let label: String
  let icon: Image
  let activeIcon: Image
  let badge: TabBarBadge?
}

let tabBarEntries = [
  TabBarEntry(value: "home", label: "Home", icon: Ph.house.regular, activeIcon: Ph.house.fill, badge: nil),
  TabBarEntry(value: "search", label: "Search", icon: Ph.magnifyingGlass.regular, activeIcon: Ph.magnifyingGlass.fill, badge: nil),
  TabBarEntry(value: "messages", label: "Messages", icon: Ph.chatCircle.regular, activeIcon: Ph.chatCircle.fill, badge: 3),
  TabBarEntry(value: "profile", label: "Profile", icon: Ph.user.regular, activeIcon: Ph.user.fill, badge: .dot),
]

struct TabBarPage<Bar: View>: View {
  let title: String
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text(verbatim: title).textVariant(.h3)
        Text("Tap a tab to switch screens.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
    .background(EsuissColor.background.ignoresSafeArea())
  }
}

struct TabBarExample: View {
  var badges = true
  var disabled: String?

  @State private var value = "home"

  var body: some View {
    TabBarPage(title: tabBarEntries.first { $0.value == value }?.label ?? "") {
      TabBar(selection: $value) {
        ForEach(tabBarEntries, id: \.value) { item in
          TabBarItem(
            verbatim: item.label,
            value: item.value,
            icon: item.icon,
            activeIcon: item.activeIcon,
            badge: badges ? item.badge : nil
          )
          .disabled(item.value == disabled)
        }
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.House
import com.adamglin.phosphoricons.fill.MagnifyingGlass
import com.adamglin.phosphoricons.fill.User
import com.adamglin.phosphoricons.regular.ChatCircle
import com.adamglin.phosphoricons.regular.House
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

@Composable
fun TabBarDisabledItemExample() {
    TabBarExample(disabled = "search")
}

class TabBarEntry(val value: String, val label: String, val icon: ImageVector, val selectedIcon: ImageVector, val badge: String? = null)

val tabBarEntries = listOf(
    TabBarEntry("home", "Home", PhosphorIcons.Regular.House, PhosphorIcons.Fill.House),
    TabBarEntry("search", "Search", PhosphorIcons.Regular.MagnifyingGlass, PhosphorIcons.Fill.MagnifyingGlass),
    TabBarEntry("messages", "Messages", PhosphorIcons.Regular.ChatCircle, PhosphorIcons.Fill.ChatCircle, "3"),
    TabBarEntry("profile", "Profile", PhosphorIcons.Regular.User, PhosphorIcons.Fill.User, ""),
)

@Composable
fun TabBarPage(title: String, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Text(title, variant = TextVariant.H3)
                Text("Tap a tab to switch screens.", variant = TextVariant.Muted)
            }
            bar()
        }
    }
}

@Composable
fun TabBarExample(badges: Boolean = true, disabled: String? = null) {
    var value by rememberSaveable { mutableStateOf("home") }
    val current = tabBarEntries.firstOrNull { it.value == value }
    TabBarPage(current?.label ?: "") {
        TabBar(value = value, onValueChange = { value = it }) {
            tabBarEntries.forEach { entry ->
                TabBarItem(
                    value = entry.value,
                    label = entry.label,
                    icon = entry.icon,
                    selectedIcon = entry.selectedIcon,
                    badge = if (badges) entry.badge else null,
                    enabled = entry.value != disabled,
                )
            }
        }
    }
}

API Reference

01

TabBar

Root that holds the active tab, the side inset and the safe area padding.

Also accepts the props of React.ComponentProps<typeof TabsPrimitive.Root>.

02

TabBarList

Capsule that holds the tabs.

Also accepts the props of React.ComponentProps<typeof TabsPrimitive.List>.

03

TabBarItem

One tab with an icon, a label and an optional badge.

PropTypeDefaultDescription
iconPhosphorIcon-Phosphor icon shown above the label. Uses the fill weight when active.
badgeTabBarBadgeValue-Number, text or true for a dot, shown on the icon's corner.

Also accepts the props of React.ComponentProps<typeof TabsPrimitive.Tab>.

Accessibility

  • The list has the tablist role and each item has the tab role, with selected and disabled states.
  • A badge with a count or text is exposed as the tab's accessibility value, so VoiceOver and TalkBack read it after the label.
  • A dot badge is visual only and is not announced.

Notes

  • The capsule uses bg-card with a border-foreground/5 border (border-foreground/10 in dark mode) and a large shadow. The active tab uses bg-secondary and text-primary, and badges use bg-destructive; colors follow the system appearance.
  • The bottom padding is the remaining safe area inset minus 8 points, and never less than 12 points.
iOS
Tab Bar on iOS in light modeTab Bar on iOS in dark mode
import { ChatCircleIcon, HouseIcon, MagnifyingGlassIcon, UserIcon } from "phosphor-react-native"
import * as React from "react"
import { View } from "react-native"
import { TabBar, TabBarItem, TabBarList } from "@/components/ui/tab-bar"
import { Text } from "@/components/ui/text"

const items = [
  { value: "home", label: "Home", icon: HouseIcon },
  { value: "search", label: "Search", icon: MagnifyingGlassIcon },
  { value: "messages", label: "Messages", icon: ChatCircleIcon, badge: 3 },
  { value: "profile", label: "Profile", icon: UserIcon, badge: true },
]

function Page({
  title,
  children,
}: {
  title: string
  children: React.ReactNode
}) {
  return (
    <View className="-m-4 flex-1 bg-background">
      <View className="flex-1 gap-2 p-4">
        <Text variant="h3">{title}</Text>
        <Text variant="muted">Tap a tab to switch screens.</Text>
      </View>
      {children}
    </View>
  )
}

function TabBarExample({
  badges = true,
  disabled,
}: {
  badges?: boolean
  disabled?: string
}) {
  const [value, setValue] = React.useState("home")
  const current = items.find((item) => item.value === value)
  return (
    <Page title={current?.label ?? ""}>
      <TabBar value={value} onValueChange={setValue}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item.value}
              value={item.value}
              icon={item.icon}
              badge={badges ? item.badge : undefined}
              disabled={item.value === disabled}
            >
              {item.label}
            </TabBarItem>
          ))}
        </TabBarList>
      </TabBar>
    </Page>
  )
}

export function TabBarExampleDemo() {
  return (
    <TabBarExample />
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabBarEntry {
  let value: String
  let label: String
  let icon: Image
  let activeIcon: Image
  let badge: TabBarBadge?
}

let tabBarEntries = [
  TabBarEntry(value: "home", label: "Home", icon: Ph.house.regular, activeIcon: Ph.house.fill, badge: nil),
  TabBarEntry(value: "search", label: "Search", icon: Ph.magnifyingGlass.regular, activeIcon: Ph.magnifyingGlass.fill, badge: nil),
  TabBarEntry(value: "messages", label: "Messages", icon: Ph.chatCircle.regular, activeIcon: Ph.chatCircle.fill, badge: 3),
  TabBarEntry(value: "profile", label: "Profile", icon: Ph.user.regular, activeIcon: Ph.user.fill, badge: .dot),
]

struct TabBarPage<Bar: View>: View {
  let title: String
  @ViewBuilder let bar: () -> Bar

  var body: some View {
    VStack(spacing: 0) {
      VStack(alignment: .leading, spacing: 8) {
        Text(verbatim: title).textVariant(.h3)
        Text("Tap a tab to switch screens.").textVariant(.muted)
      }
      .padding(16)
      .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .topLeading)
      bar()
    }
    .background(EsuissColor.background.ignoresSafeArea())
  }
}

struct TabBarExample: View {
  var badges = true
  var disabled: String?

  @State private var value = "home"

  var body: some View {
    TabBarPage(title: tabBarEntries.first { $0.value == value }?.label ?? "") {
      TabBar(selection: $value) {
        ForEach(tabBarEntries, id: \.value) { item in
          TabBarItem(
            verbatim: item.label,
            value: item.value,
            icon: item.icon,
            activeIcon: item.activeIcon,
            badge: badges ? item.badge : nil
          )
          .disabled(item.value == disabled)
        }
      }
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.saveable.rememberSaveable
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.graphics.vector.ImageVector
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Fill
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.fill.ChatCircle
import com.adamglin.phosphoricons.fill.House
import com.adamglin.phosphoricons.fill.MagnifyingGlass
import com.adamglin.phosphoricons.fill.User
import com.adamglin.phosphoricons.regular.ChatCircle
import com.adamglin.phosphoricons.regular.House
import com.adamglin.phosphoricons.regular.MagnifyingGlass
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.Screen
import com.esuiss.ui.components.ScreenEdge
import com.esuiss.ui.components.TabBar
import com.esuiss.ui.components.TabBarItem
import com.esuiss.ui.components.Text
import com.esuiss.ui.components.TextVariant

class TabBarEntry(val value: String, val label: String, val icon: ImageVector, val selectedIcon: ImageVector, val badge: String? = null)

val tabBarEntries = listOf(
    TabBarEntry("home", "Home", PhosphorIcons.Regular.House, PhosphorIcons.Fill.House),
    TabBarEntry("search", "Search", PhosphorIcons.Regular.MagnifyingGlass, PhosphorIcons.Fill.MagnifyingGlass),
    TabBarEntry("messages", "Messages", PhosphorIcons.Regular.ChatCircle, PhosphorIcons.Fill.ChatCircle, "3"),
    TabBarEntry("profile", "Profile", PhosphorIcons.Regular.User, PhosphorIcons.Fill.User, ""),
)

@Composable
fun TabBarPage(title: String, bar: @Composable () -> Unit) {
    Screen(edges = setOf(ScreenEdge.Top, ScreenEdge.Left, ScreenEdge.Right)) {
        Column(Modifier.fillMaxSize()) {
            Column(
                Modifier.weight(1f).fillMaxWidth().padding(16.dp),
                verticalArrangement = Arrangement.spacedBy(8.dp),
            ) {
                Text(title, variant = TextVariant.H3)
                Text("Tap a tab to switch screens.", variant = TextVariant.Muted)
            }
            bar()
        }
    }
}

@Composable
fun TabBarExample(badges: Boolean = true, disabled: String? = null) {
    var value by rememberSaveable { mutableStateOf("home") }
    val current = tabBarEntries.firstOrNull { it.value == value }
    TabBarPage(current?.label ?: "") {
        TabBar(value = value, onValueChange = { value = it }) {
            tabBarEntries.forEach { entry ->
                TabBarItem(
                    value = entry.value,
                    label = entry.label,
                    icon = entry.icon,
                    selectedIcon = entry.selectedIcon,
                    badge = if (badges) entry.badge else null,
                    enabled = entry.value != disabled,
                )
            }
        }
    }
}
import * as React from "react"
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarLink,
  TabBarList,
  TabBarSection,
  TabBarSectionTitle,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

const defaultItems = ["Home", "Nominees", "Directory", "Collections"]

const sections = [
  { title: "Awards", links: ["Winners", "Site of the Day", "Nominees"] },
  { title: "Inspiration", links: ["Collections", "Elements", "Resources"] },
  { title: "Directory", links: ["Professionals", "Agencies", "Freelancers"] },
  { title: "Market", links: ["Jobs", "New Events", "Products"] },
]

function TabBarExample({
  defaultOpen,
  items = defaultItems,
  withSections = true,
}: {
  defaultOpen?: boolean
  items?: string[]
  withSections?: boolean
}) {
  const [value, setValue] = React.useState("Home")

  return (
    <div className="h-full min-h-svh bg-background">
      <Backdrop />
      <TabBar aria-label="Main" defaultOpen={defaultOpen}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item}
              isActive={value === item}
              onClick={() => setValue(item)}
            >
              {item}
            </TabBarItem>
          ))}
        </TabBarList>
        <TabBarContent>
          {withSections &&
            sections.map((section) => (
              <TabBarSection key={section.title}>
                <TabBarSectionTitle>{section.title}</TabBarSectionTitle>
                {section.links.map((link) => (
                  <TabBarLink
                    key={link}
                    href={`#${link.toLowerCase().replaceAll(" ", "-")}`}
                    isActive={value === link}
                    onClick={() => setValue(link)}
                  >
                    {link}
                  </TabBarLink>
                ))}
              </TabBarSection>
            ))}
        </TabBarContent>
        <TabBarTrigger>More</TabBarTrigger>
      </TabBar>
    </div>
  )
}

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarDemo() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <TabBarExample />
    </div>
  )
}
import * as React from "react"
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarLink,
  TabBarList,
  TabBarSection,
  TabBarSectionTitle,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

const defaultItems = ["Home", "Nominees", "Directory", "Collections"]

const sections = [
  { title: "Awards", links: ["Winners", "Site of the Day", "Nominees"] },
  { title: "Inspiration", links: ["Collections", "Elements", "Resources"] },
  { title: "Directory", links: ["Professionals", "Agencies", "Freelancers"] },
  { title: "Market", links: ["Jobs", "New Events", "Products"] },
]

function TabBarExample({
  defaultOpen,
  items = defaultItems,
  withSections = true,
}: {
  defaultOpen?: boolean
  items?: string[]
  withSections?: boolean
}) {
  const [value, setValue] = React.useState("Home")

  return (
    <div className="h-full min-h-svh bg-background">
      <Backdrop />
      <TabBar aria-label="Main" defaultOpen={defaultOpen}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item}
              isActive={value === item}
              onClick={() => setValue(item)}
            >
              {item}
            </TabBarItem>
          ))}
        </TabBarList>
        <TabBarContent>
          {withSections &&
            sections.map((section) => (
              <TabBarSection key={section.title}>
                <TabBarSectionTitle>{section.title}</TabBarSectionTitle>
                {section.links.map((link) => (
                  <TabBarLink
                    key={link}
                    href={`#${link.toLowerCase().replaceAll(" ", "-")}`}
                    isActive={value === link}
                    onClick={() => setValue(link)}
                  >
                    {link}
                  </TabBarLink>
                ))}
              </TabBarSection>
            ))}
        </TabBarContent>
        <TabBarTrigger>More</TabBarTrigger>
      </TabBar>
    </div>
  )
}

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarOpen() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <TabBarExample defaultOpen />
    </div>
  )
}
import * as React from "react"
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarLink,
  TabBarList,
  TabBarSection,
  TabBarSectionTitle,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

const defaultItems = ["Home", "Nominees", "Directory", "Collections"]

const sections = [
  { title: "Awards", links: ["Winners", "Site of the Day", "Nominees"] },
  { title: "Inspiration", links: ["Collections", "Elements", "Resources"] },
  { title: "Directory", links: ["Professionals", "Agencies", "Freelancers"] },
  { title: "Market", links: ["Jobs", "New Events", "Products"] },
]

function TabBarExample({
  defaultOpen,
  items = defaultItems,
  withSections = true,
}: {
  defaultOpen?: boolean
  items?: string[]
  withSections?: boolean
}) {
  const [value, setValue] = React.useState("Home")

  return (
    <div className="h-full min-h-svh bg-background">
      <Backdrop />
      <TabBar aria-label="Main" defaultOpen={defaultOpen}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item}
              isActive={value === item}
              onClick={() => setValue(item)}
            >
              {item}
            </TabBarItem>
          ))}
        </TabBarList>
        <TabBarContent>
          {withSections &&
            sections.map((section) => (
              <TabBarSection key={section.title}>
                <TabBarSectionTitle>{section.title}</TabBarSectionTitle>
                {section.links.map((link) => (
                  <TabBarLink
                    key={link}
                    href={`#${link.toLowerCase().replaceAll(" ", "-")}`}
                    isActive={value === link}
                    onClick={() => setValue(link)}
                  >
                    {link}
                  </TabBarLink>
                ))}
              </TabBarSection>
            ))}
        </TabBarContent>
        <TabBarTrigger>More</TabBarTrigger>
      </TabBar>
    </div>
  )
}

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarFitsWithoutMore() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <TabBarExample items={defaultItems.slice(0, 3)} withSections={false} />
    </div>
  )
}
import * as React from "react"
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarLink,
  TabBarList,
  TabBarSection,
  TabBarSectionTitle,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

const defaultItems = ["Home", "Nominees", "Directory", "Collections"]

const manyItems = [...defaultItems, "Market", "Academy", "Conferences"]

const sections = [
  { title: "Awards", links: ["Winners", "Site of the Day", "Nominees"] },
  { title: "Inspiration", links: ["Collections", "Elements", "Resources"] },
  { title: "Directory", links: ["Professionals", "Agencies", "Freelancers"] },
  { title: "Market", links: ["Jobs", "New Events", "Products"] },
]

function TabBarExample({
  defaultOpen,
  items = defaultItems,
  withSections = true,
}: {
  defaultOpen?: boolean
  items?: string[]
  withSections?: boolean
}) {
  const [value, setValue] = React.useState("Home")

  return (
    <div className="h-full min-h-svh bg-background">
      <Backdrop />
      <TabBar aria-label="Main" defaultOpen={defaultOpen}>
        <TabBarList>
          {items.map((item) => (
            <TabBarItem
              key={item}
              isActive={value === item}
              onClick={() => setValue(item)}
            >
              {item}
            </TabBarItem>
          ))}
        </TabBarList>
        <TabBarContent>
          {withSections &&
            sections.map((section) => (
              <TabBarSection key={section.title}>
                <TabBarSectionTitle>{section.title}</TabBarSectionTitle>
                {section.links.map((link) => (
                  <TabBarLink
                    key={link}
                    href={`#${link.toLowerCase().replaceAll(" ", "-")}`}
                    isActive={value === link}
                    onClick={() => setValue(link)}
                  >
                    {link}
                  </TabBarLink>
                ))}
              </TabBarSection>
            ))}
        </TabBarContent>
        <TabBarTrigger>More</TabBarTrigger>
      </TabBar>
    </div>
  )
}

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarOverflowIntoMore() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <TabBarExample items={manyItems} withSections={false} />
    </div>
  )
}
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarList,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

const defaultItems = ["Home", "Nominees", "Directory", "Collections"]

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarLinks() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <div className="h-full min-h-svh bg-background">
        <Backdrop />
        <TabBar aria-label="Main">
          <TabBarList>
            {defaultItems.map((item, index) => (
              <TabBarItem
                key={item}
                isActive={index === 0}
                render={<a href={`#${item.toLowerCase()}`} />}
              >
                {item}
              </TabBarItem>
            ))}
          </TabBarList>
          <TabBarContent />
          <TabBarTrigger>More</TabBarTrigger>
        </TabBar>
      </div>
    </div>
  )
}
import {
  TabBar,
  TabBarContent,
  TabBarItem,
  TabBarList,
  TabBarTrigger,
} from "@/components/ui/tab-bar"

function Backdrop() {
  return (
    <div className="grid grid-cols-2 gap-3 p-4 md:grid-cols-4">
      {Array.from({ length: 16 }, (_, index) => `Card ${index + 1}`).map(
        (card) => (
          <div
            key={card}
            className="flex aspect-[4/3] items-end rounded-2xl bg-primary/15 p-4 text-sm font-semibold text-primary odd:bg-secondary odd:text-foreground"
          >
            {card}
          </div>
        )
      )}
    </div>
  )
}

export default function TabBarDisabledItem() {
  return (
    <div className="h-[32rem] transform-gpu overflow-hidden">
      <div className="h-full min-h-svh bg-background">
        <Backdrop />
        <TabBar aria-label="Main">
          <TabBarList>
            <TabBarItem isActive>Home</TabBarItem>
            <TabBarItem>Nominees</TabBarItem>
            <TabBarItem disabled>Directory</TabBarItem>
          </TabBarList>
          <TabBarContent />
          <TabBarTrigger>More</TabBarTrigger>
        </TabBar>
      </div>
    </div>
  )
}
components/ui/tab-bar.tsx
"use client"

import { mergeProps } from "@base-ui/react/merge-props"
import { useRender } from "@base-ui/react/use-render"
import { ListIcon, XIcon } from "@phosphor-icons/react"
import * as React from "react"
import { flushSync } from "react-dom"
import { Button, buttonVariants } from "@/components/ui/button"
import { cn } from "@/lib/utils"

type TabBarProps = {
  open?: boolean
  defaultOpen?: boolean
  onOpenChange?: (open: boolean) => void
}
kotlin/components/TabBar.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.Spring
import androidx.compose.animation.core.spring
import androidx.compose.foundation.background
import androidx.compose.foundation.border
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.interaction.collectIsPressedAsState
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.WindowInsetsSides
swift/Components/TabBar.swift
import SwiftUI

public enum TabBarBadge: Equatable, Sendable, ExpressibleByIntegerLiteral, ExpressibleByStringLiteral {
  case dot
  case count(Int)
  case text(String)

  public init(integerLiteral value: Int) {
    self = .count(value)
  }

  public init(stringLiteral value: String) {
    self = .text(value)
  }

  var label: String? {
uniwind/components/ui/tab-bar.tsx
import { cn } from "cn"
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useHaptics } from "@/components/primitives/haptics"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { ScreenContext } from "@/components/ui/screen"
import { Text, TextClassContext } from "@/components/ui/text"

type TabBarBadgeValue = number | string | boolean

function badgeText(badge: TabBarBadgeValue | undefined) {
  if (badge === undefined || badge === false || badge === true) return undefined
nativewind/components/ui/tab-bar.tsx
import { cn } from "cn"
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, View } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useHaptics } from "@/components/primitives/haptics"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { resolveClassName } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { ScreenContext } from "@/components/ui/screen"
import { Text, TextClassContext } from "@/components/ui/text"

type TabBarBadgeValue = number | string | boolean

function badgeText(badge: TabBarBadgeValue | undefined) {
  if (badge === undefined || badge === false || badge === true) return undefined
stylesheet/components/ui/tab-bar.tsx
import type { Icon as PhosphorIcon } from "phosphor-react-native"
import * as React from "react"
import { Pressable, StyleSheet, View, type ViewStyle } from "react-native"
import { useSafeAreaInsets } from "react-native-safe-area-context"
import { useHaptics } from "@/components/primitives/haptics"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { resolveStyle } from "@/components/primitives/use-render"
import { Icon } from "@/components/ui/icon"
import { ScreenContext } from "@/components/ui/screen"
import { Text, TextStyleContext } from "@/components/ui/text"
import { fontSize, radius, shadow, useTheme } from "@/components/ui/theme"

type TabBarBadgeValue = number | string | boolean

const styles = StyleSheet.create({
  root: { paddingHorizontal: 16, paddingTop: 8 },