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

Tabs

A set of tabs that switches between panels of related content in the same place.

Tabs 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.

Tabs wraps the tabs primitive of the chosen base. TabsList holds the TabsTrigger buttons, and each TabsContent panel is shown when its value matches the selected tab. An indicator slides to the active tab: a raised pill in the default variant, or an underline in the line variant.

When there are more tabs than fit, the list scrolls horizontally and keeps the active tab in view. On small screens, a horizontal list bleeds to the edges of its scroll container. Use Tabs to switch views within a page. For app-level navigation, use TabBar or Sidebar; for a compact single-choice control, consider a segmented ToggleGroup.

Installation

CLI

$ pnpm dlx shadcn@latest add @esuiss/tabs

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 tabs

Manual

  1. Install the dependencies and add the cn helper:
$ pnpm add @base-ui/react class-variance-authority
  1. Copy the source into components/ui/tabs.tsx:
  1. Update the import paths to match your project.

Usage

import { Tabs, TabsList, TabsTrigger, TabsContent } 
<Tabs
















Use defaultValue on Tabs for an uncontrolled set, or value with onValueChange to control it. Give each TabsTrigger and its TabsContent the same value. Set orientation="vertical" on Tabs to stack the triggers beside the panel.

Examples

Line

Uses the line variant, which underlines the active tab.

Vertical

Stacks the tabs vertically beside the panel.

With icons

Places an icon before each tab's label.

Disabled tab

Disables one tab so it can't be selected.

Many tabs

Holds more tabs than fit, so the list scrolls.

Many tabs line

Combines a scrolling list with the line variant.

API Reference

Anatomy4 parts
<Tabs>
<TabsList>
<TabsTrigger />
</TabsList>
<TabsContent />
</Tabs>
01

Tabs

The root that holds the selected value and orientation.

Also accepts the props of the primitive part it wraps.

02

TabsList

The row or column of tabs, with the sliding indicator.

PropTypeDefaultDescription
variant"default" | "line""default"Chooses a filled default list with a raised pill, or a line list with an underline.

Also accepts the props of the primitive part it wraps.

03

TabsTrigger

A tab that selects its panel.

Also accepts the props of the primitive part it wraps.

04

TabsContent

The panel shown when its tab is selected.

Also accepts the props of the primitive part it wraps.

Accessibility

  • The list has the tablist role with aria-orientation; triggers are tab elements with aria-selected, and panels are tabpanel elements.
  • Arrow keys move focus between tabs, wrapping at the ends; Enter or Space selects the focused tab. Vertical tabs use the up and down arrow keys.
  • Home and End move focus to the first and last tabs.
  • Disabled tabs set aria-disabled and can't be selected by click or keyboard.

Notes

  • Parts carry data-slot attributes: tabs, tabs-list, tabs-list-content, tabs-indicator, tabs-trigger and tabs-content. The list and indicator set data-variant, and the active trigger sets data-active.
  • The edge bleed on small screens is set through the --tabs-list-bleed-start and --tabs-list-bleed-end properties, which the list measures itself.
  • className on TabsList styles the inner tabs-list-content element, not the scrolling list.
  • Indicator and scroll animations are removed when reduced motion is preferred.

Tabs groups a TabsList of TabsTrigger buttons with matching TabsContent panels. The active tab is marked by an indicator that slides between tabs with a Reanimated layout transition.

TabsList has two variants: default, a segmented control with a raised indicator on a filled track, and line, a transparent list with a thin underline. Both work in horizontal and vertical orientation.

Installation

$ pnpm dlx shadcn@latest add @esuiss-rn/tabs

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

$ pnpm dlx @esuiss/ui@latest add tabs
$ pnpm dlx @esuiss/ui@latest add tabs --platform swift
NextTextPreviousTable

On this page

  1. 01Installation
    • CLI
    • Manual
  2. 02Usage
  3. 03Examples
    • Line
    • Vertical
    • With icons
    • Disabled tab
    • Many tabs
    • Many tabs line
  4. 04API Reference
    • Tabs
    • TabsList
    • TabsTrigger
    • TabsContent
  5. 05Accessibility
  6. 06Notes
  7. 07Installation
    • Manual
  8. 08Usage
  9. 09Examples
    • With icons
    • Disabled tab
    • Line
    • Vertical
    • Vertical line
    • With icons line
    • Disabled tab line
  10. 10API Reference
    • Tabs
    • TabsList
    • TabsTrigger
    • TabsContent
  11. 11Accessibility
  12. 12Notes
from
"@/components/ui/tabs"
defaultValue
=
"account"
orientation
=
"horizontal"
>
<TabsList variant="default">
{tabs.map((tab) => (
<TabsTrigger key={tab.value} value={tab.value}>
{tab.label}
</TabsTrigger>
))}
</TabsList>
{tabs.map((tab) => (
<TabsContent
key={tab.value}
value={tab.value}
className="text-muted-foreground"
>
{tab.content}
</TabsContent>
))}
</Tabs>
$ pnpm dlx @esuiss/ui@latest add tabs --platform kotlin

The CLI also adds what it builds on: Text.

Manual

  1. Install the dependencies:
npx expo install class-variance-authority cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: Text, and the primitives components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tabs.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install class-variance-authority cn react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: Text, and the primitives components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tabs.tsx:

  1. Update the import paths to match your project.
  1. Install the dependencies:
npx expo install react-native-reanimated react-native-worklets
  1. Add the pieces it builds on: Text, and the primitives components/primitives/tabs.tsx, components/primitives/use-render.tsx.

  2. Copy the source into components/ui/tabs.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/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/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")
  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/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 }
  1. Update the com.esuiss.ui package names to match your project.

Usage

import { Tabs, TabsList, TabsTrigger, TabsContent } from "@/components/ui/tabs"
<TabsDemo />

Give each TabsTrigger and its TabsContent the same value. Use defaultValue for an uncontrolled tab set, or value with onValueChange to control it.

Set orientation="vertical" on Tabs to stack the list beside the panels. Put an Icon next to the Text in a trigger for icon tabs.

Inactive panels are unmounted unless you set keepMounted on TabsContent. tabsListVariants is exported for reuse.

Examples

With icons

Tabs with an icon beside each label.

iOS
Tabs (with icons) on iOS in light modeTabs (with icons) on iOS in dark mode
import { GearIcon, UserIcon } from "phosphor-react-native"
import * as React from "react"
import { Icon } from "@/components/ui/icon"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function WithIconsDemo({ variant }: { variant?: ListVariant }) {
  const [value, setValue] = React.useState("profile")
  return (
    <Tabs value={value} onValueChange={setValue}>
      <TabsList variant={variant}>
        <TabsTrigger value="profile">
          <Icon as={UserIcon} className="size-4" />
          <Text>Profile</Text>
        </TabsTrigger>
        <TabsTrigger value="settings">
          <Icon as={GearIcon} className="size-4" />
          <Text>Settings</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="profile">
        <Text className="text-sm text-muted-foreground">
          Your public profile information.
        </Text>
      </TabsContent>
      <TabsContent value="settings">
        <Text className="text-sm text-muted-foreground">
          Manage your preferences.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsWithIconsExample() {
  return (
    <WithIconsDemo />
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsWithIconsDemo: View {
  var variant: TabsListVariant = .default

  @State private var value = "profile"

  var body: some View {
    Tabs(selection: $value) {
      TabsList(variant: variant) {
        TabsTrigger(value: "profile") {
          Label {
            Text("Profile")
          } icon: {
            Ph.user.regular
          }
        }
        TabsTrigger(value: "settings") {
          Label {
            Text("Settings")
          } icon: {
            Ph.gear.regular
          }
        }
      }
      TabsContent(value: "profile") {
        TabsPanelText(text: "Your public profile information.")
      }
      TabsContent(value: "settings") {
        TabsPanelText(text: "Manage your preferences.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}
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.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Gear
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsListVariant
import com.esuiss.ui.components.TabsTrigger
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TabsWithIconsExample() {
    TabsWithIcons(TabsListVariant.Default)
}

@Composable
fun TabsSecondaryText(text: String) {
    Text(text, style = Typography.sm, color = EsuissTheme.colors.mutedForeground)
}

@Composable
fun TabsWithIcons(variant: TabsListVariant) {
    var value by rememberSaveable { mutableStateOf("profile") }
    Tabs(value, { value = it }) {
        TabsList(variant = variant) {
            TabsTrigger("profile") {
                Icon(PhosphorIcons.Regular.User, contentDescription = null, size = 16.dp)
                Text("Profile")
            }
            TabsTrigger("settings") {
                Icon(PhosphorIcons.Regular.Gear, contentDescription = null, size = 16.dp)
                Text("Settings")
            }
        }
        TabsContent("profile") { TabsSecondaryText("Your public profile information.") }
        TabsContent("settings") { TabsSecondaryText("Manage your preferences.") }
    }
}

Disabled tab

A tab set with one disabled tab.

iOS
Tabs (disabled tab) on iOS in light modeTabs (disabled tab) on iOS in dark mode
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function DisabledTabDemo({ variant }: { variant?: ListVariant }) {
  const [value, setValue] = React.useState("overview")
  return (
    <Tabs value={value} onValueChange={setValue}>
      <TabsList variant={variant}>
        <TabsTrigger value="overview">
          <Text>Overview</Text>
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <Text>Analytics</Text>
        </TabsTrigger>
        <TabsTrigger value="reports" disabled>
          <Text>Reports</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text className="text-sm text-muted-foreground">
          A summary of your activity.
        </Text>
      </TabsContent>
      <TabsContent value="analytics">
        <Text className="text-sm text-muted-foreground">
          Detailed metrics and trends.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsDisabledTabExample() {
  return (
    <DisabledTabDemo />
  )
}
import EsuissUI
import SwiftUI

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsDisabledDemo: View {
  var variant: TabsListVariant = .default

  @State private var value = "overview"

  var body: some View {
    Tabs(selection: $value) {
      TabsList(variant: variant) {
        TabsTrigger("Overview", value: "overview")
        TabsTrigger("Analytics", value: "analytics")
        TabsTrigger("Reports", value: "reports")
          .disabled(true)
      }
      TabsContent(value: "overview") {
        TabsPanelText(text: "A summary of your activity.")
      }
      TabsContent(value: "analytics") {
        TabsPanelText(text: "Detailed metrics and trends.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}
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 com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsListVariant
import com.esuiss.ui.components.TabsTrigger
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TabsDisabledTabExample() {
    TabsDisabledTab(TabsListVariant.Default)
}

@Composable
fun TabsSecondaryText(text: String) {
    Text(text, style = Typography.sm, color = EsuissTheme.colors.mutedForeground)
}

@Composable
fun TabsDisabledTab(variant: TabsListVariant) {
    var value by rememberSaveable { mutableStateOf("overview") }
    Tabs(value, { value = it }) {
        TabsList(variant = variant) {
            TabsTrigger("overview", "Overview")
            TabsTrigger("analytics", "Analytics")
            TabsTrigger("reports", "Reports", enabled = false)
        }
        TabsContent("overview") { TabsSecondaryText("A summary of your activity.") }
        TabsContent("analytics") { TabsSecondaryText("Detailed metrics and trends.") }
    }
}

Line

The line variant with an underline indicator.

iOS
Tabs (line) on iOS in light modeTabs (line) on iOS in dark mode
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function SectionsDemo({
  variant,
  orientation,
}: {
  variant?: ListVariant
  orientation?: "horizontal" | "vertical"
}) {
  const [value, setValue] = React.useState("overview")
  return (
    <Tabs value={value} onValueChange={setValue} orientation={orientation}>
      <TabsList variant={variant}>
        <TabsTrigger value="overview">
          <Text>Overview</Text>
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <Text>Analytics</Text>
        </TabsTrigger>
        <TabsTrigger value="reports">
          <Text>Reports</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text className="text-sm text-muted-foreground">
          A summary of your activity.
        </Text>
      </TabsContent>
      <TabsContent value="analytics">
        <Text className="text-sm text-muted-foreground">
          Detailed metrics and trends.
        </Text>
      </TabsContent>
      <TabsContent value="reports">
        <Text className="text-sm text-muted-foreground">
          Exported reports and schedules.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsLineExample() {
  return (
    <SectionsDemo variant="line" />
  )
}
import EsuissUI
import SwiftUI

struct TabsLineExample: View {
  var body: some View {
    TabsSectionsDemo(variant: .line)
  }
}

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsSectionsDemo: View {
  var variant: TabsListVariant = .default
  var orientation: Axis = .horizontal

  @State private var value = "overview"

  var body: some View {
    Tabs(selection: $value, orientation: orientation) {
      TabsList(variant: variant) {
        TabsTrigger("Overview", value: "overview")
        TabsTrigger("Analytics", value: "analytics")
        TabsTrigger("Reports", value: "reports")
      }
      TabsContent(value: "overview") {
        TabsPanelText(text: "A summary of your activity.")
      }
      TabsContent(value: "analytics") {
        TabsPanelText(text: "Detailed metrics and trends.")
      }
      TabsContent(value: "reports") {
        TabsPanelText(text: "Exported reports and schedules.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}
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 com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsListVariant
import com.esuiss.ui.components.TabsTrigger
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TabsLineExample() {
    TabsSections(TabsListVariant.Line)
}

@Composable
fun TabsSecondaryText(text: String) {
    Text(text, style = Typography.sm, color = EsuissTheme.colors.mutedForeground)
}

@Composable
fun TabsSections(variant: TabsListVariant) {
    var value by rememberSaveable { mutableStateOf("overview") }
    Tabs(value, { value = it }) {
        TabsList(variant = variant) {
            TabsTrigger("overview", "Overview")
            TabsTrigger("analytics", "Analytics")
            TabsTrigger("reports", "Reports")
        }
        TabsContent("overview") { TabsSecondaryText("A summary of your activity.") }
        TabsContent("analytics") { TabsSecondaryText("Detailed metrics and trends.") }
        TabsContent("reports") { TabsSecondaryText("Exported reports and schedules.") }
    }
}

Vertical

A vertical tab list next to its panels.

iOS
Tabs (vertical) on iOS in light modeTabs (vertical) on iOS in dark mode
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function SectionsDemo({
  variant,
  orientation,
}: {
  variant?: ListVariant
  orientation?: "horizontal" | "vertical"
}) {
  const [value, setValue] = React.useState("overview")
  return (
    <Tabs value={value} onValueChange={setValue} orientation={orientation}>
      <TabsList variant={variant}>
        <TabsTrigger value="overview">
          <Text>Overview</Text>
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <Text>Analytics</Text>
        </TabsTrigger>
        <TabsTrigger value="reports">
          <Text>Reports</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text className="text-sm text-muted-foreground">
          A summary of your activity.
        </Text>
      </TabsContent>
      <TabsContent value="analytics">
        <Text className="text-sm text-muted-foreground">
          Detailed metrics and trends.
        </Text>
      </TabsContent>
      <TabsContent value="reports">
        <Text className="text-sm text-muted-foreground">
          Exported reports and schedules.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsVerticalExample() {
  return (
    <SectionsDemo orientation="vertical" />
  )
}
import EsuissUI
import SwiftUI

struct TabsVerticalExample: View {
  var body: some View {
    TabsSectionsDemo(orientation: .vertical)
  }
}

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsSectionsDemo: View {
  var variant: TabsListVariant = .default
  var orientation: Axis = .horizontal

  @State private var value = "overview"

  var body: some View {
    Tabs(selection: $value, orientation: orientation) {
      TabsList(variant: variant) {
        TabsTrigger("Overview", value: "overview")
        TabsTrigger("Analytics", value: "analytics")
        TabsTrigger("Reports", value: "reports")
      }
      TabsContent(value: "overview") {
        TabsPanelText(text: "A summary of your activity.")
      }
      TabsContent(value: "analytics") {
        TabsPanelText(text: "Detailed metrics and trends.")
      }
      TabsContent(value: "reports") {
        TabsPanelText(text: "Exported reports and schedules.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}

Vertical line

A vertical line list with a side indicator.

iOS
Tabs (vertical line) on iOS in light modeTabs (vertical line) on iOS in dark mode
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function SectionsDemo({
  variant,
  orientation,
}: {
  variant?: ListVariant
  orientation?: "horizontal" | "vertical"
}) {
  const [value, setValue] = React.useState("overview")
  return (
    <Tabs value={value} onValueChange={setValue} orientation={orientation}>
      <TabsList variant={variant}>
        <TabsTrigger value="overview">
          <Text>Overview</Text>
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <Text>Analytics</Text>
        </TabsTrigger>
        <TabsTrigger value="reports">
          <Text>Reports</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text className="text-sm text-muted-foreground">
          A summary of your activity.
        </Text>
      </TabsContent>
      <TabsContent value="analytics">
        <Text className="text-sm text-muted-foreground">
          Detailed metrics and trends.
        </Text>
      </TabsContent>
      <TabsContent value="reports">
        <Text className="text-sm text-muted-foreground">
          Exported reports and schedules.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsVerticalLineExample() {
  return (
    <SectionsDemo variant="line" orientation="vertical" />
  )
}
import EsuissUI
import SwiftUI

struct TabsVerticalLineExample: View {
  var body: some View {
    TabsSectionsDemo(variant: .line, orientation: .vertical)
  }
}

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsSectionsDemo: View {
  var variant: TabsListVariant = .default
  var orientation: Axis = .horizontal

  @State private var value = "overview"

  var body: some View {
    Tabs(selection: $value, orientation: orientation) {
      TabsList(variant: variant) {
        TabsTrigger("Overview", value: "overview")
        TabsTrigger("Analytics", value: "analytics")
        TabsTrigger("Reports", value: "reports")
      }
      TabsContent(value: "overview") {
        TabsPanelText(text: "A summary of your activity.")
      }
      TabsContent(value: "analytics") {
        TabsPanelText(text: "Detailed metrics and trends.")
      }
      TabsContent(value: "reports") {
        TabsPanelText(text: "Exported reports and schedules.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}

With icons line

Icon tabs in the line variant.

iOS
Tabs (with icons line) on iOS in light modeTabs (with icons line) on iOS in dark mode
import { GearIcon, UserIcon } from "phosphor-react-native"
import * as React from "react"
import { Icon } from "@/components/ui/icon"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function WithIconsDemo({ variant }: { variant?: ListVariant }) {
  const [value, setValue] = React.useState("profile")
  return (
    <Tabs value={value} onValueChange={setValue}>
      <TabsList variant={variant}>
        <TabsTrigger value="profile">
          <Icon as={UserIcon} className="size-4" />
          <Text>Profile</Text>
        </TabsTrigger>
        <TabsTrigger value="settings">
          <Icon as={GearIcon} className="size-4" />
          <Text>Settings</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="profile">
        <Text className="text-sm text-muted-foreground">
          Your public profile information.
        </Text>
      </TabsContent>
      <TabsContent value="settings">
        <Text className="text-sm text-muted-foreground">
          Manage your preferences.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsWithIconsLineExample() {
  return (
    <WithIconsDemo variant="line" />
  )
}
import EsuissUI
import PhosphorSwift
import SwiftUI

struct TabsWithIconsLineExample: View {
  var body: some View {
    TabsWithIconsDemo(variant: .line)
  }
}

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsWithIconsDemo: View {
  var variant: TabsListVariant = .default

  @State private var value = "profile"

  var body: some View {
    Tabs(selection: $value) {
      TabsList(variant: variant) {
        TabsTrigger(value: "profile") {
          Label {
            Text("Profile")
          } icon: {
            Ph.user.regular
          }
        }
        TabsTrigger(value: "settings") {
          Label {
            Text("Settings")
          } icon: {
            Ph.gear.regular
          }
        }
      }
      TabsContent(value: "profile") {
        TabsPanelText(text: "Your public profile information.")
      }
      TabsContent(value: "settings") {
        TabsPanelText(text: "Manage your preferences.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}
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.semantics.contentDescription
import androidx.compose.ui.unit.dp
import com.adamglin.PhosphorIcons
import com.adamglin.phosphoricons.Regular
import com.adamglin.phosphoricons.regular.Gear
import com.adamglin.phosphoricons.regular.User
import com.esuiss.ui.components.Icon
import com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsListVariant
import com.esuiss.ui.components.TabsTrigger
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TabsWithIconsLineExample() {
    TabsWithIcons(TabsListVariant.Line)
}

@Composable
fun TabsSecondaryText(text: String) {
    Text(text, style = Typography.sm, color = EsuissTheme.colors.mutedForeground)
}

@Composable
fun TabsWithIcons(variant: TabsListVariant) {
    var value by rememberSaveable { mutableStateOf("profile") }
    Tabs(value, { value = it }) {
        TabsList(variant = variant) {
            TabsTrigger("profile") {
                Icon(PhosphorIcons.Regular.User, contentDescription = null, size = 16.dp)
                Text("Profile")
            }
            TabsTrigger("settings") {
                Icon(PhosphorIcons.Regular.Gear, contentDescription = null, size = 16.dp)
                Text("Settings")
            }
        }
        TabsContent("profile") { TabsSecondaryText("Your public profile information.") }
        TabsContent("settings") { TabsSecondaryText("Manage your preferences.") }
    }
}

Disabled tab line

A disabled tab in the line variant.

iOS
Tabs (disabled tab line) on iOS in light modeTabs (disabled tab line) on iOS in dark mode
import * as React from "react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

type ListVariant = "default" | "line"

function DisabledTabDemo({ variant }: { variant?: ListVariant }) {
  const [value, setValue] = React.useState("overview")
  return (
    <Tabs value={value} onValueChange={setValue}>
      <TabsList variant={variant}>
        <TabsTrigger value="overview">
          <Text>Overview</Text>
        </TabsTrigger>
        <TabsTrigger value="analytics">
          <Text>Analytics</Text>
        </TabsTrigger>
        <TabsTrigger value="reports" disabled>
          <Text>Reports</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="overview">
        <Text className="text-sm text-muted-foreground">
          A summary of your activity.
        </Text>
      </TabsContent>
      <TabsContent value="analytics">
        <Text className="text-sm text-muted-foreground">
          Detailed metrics and trends.
        </Text>
      </TabsContent>
    </Tabs>
  )
}

export function TabsDisabledTabLineExample() {
  return (
    <DisabledTabDemo variant="line" />
  )
}
import EsuissUI
import SwiftUI

struct TabsDisabledTabLineExample: View {
  var body: some View {
    TabsDisabledDemo(variant: .line)
  }
}

struct TabsPanelText: View {
  let text: LocalizedStringKey

  var body: some View {
    Text(text)
      .textStyle(.sm)
      .foregroundStyle(EsuissColor.mutedForeground)
  }
}

struct TabsDisabledDemo: View {
  var variant: TabsListVariant = .default

  @State private var value = "overview"

  var body: some View {
    Tabs(selection: $value) {
      TabsList(variant: variant) {
        TabsTrigger("Overview", value: "overview")
        TabsTrigger("Analytics", value: "analytics")
        TabsTrigger("Reports", value: "reports")
          .disabled(true)
      }
      TabsContent(value: "overview") {
        TabsPanelText(text: "A summary of your activity.")
      }
      TabsContent(value: "analytics") {
        TabsPanelText(text: "Detailed metrics and trends.")
      }
    }
    .frame(maxWidth: .infinity)
  }
}
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 com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsListVariant
import com.esuiss.ui.components.TabsTrigger
import com.esuiss.ui.components.Text
import com.esuiss.ui.theme.EsuissTheme
import com.esuiss.ui.theme.Typography

@Composable
fun TabsDisabledTabLineExample() {
    TabsDisabledTab(TabsListVariant.Line)
}

@Composable
fun TabsSecondaryText(text: String) {
    Text(text, style = Typography.sm, color = EsuissTheme.colors.mutedForeground)
}

@Composable
fun TabsDisabledTab(variant: TabsListVariant) {
    var value by rememberSaveable { mutableStateOf("overview") }
    Tabs(value, { value = it }) {
        TabsList(variant = variant) {
            TabsTrigger("overview", "Overview")
            TabsTrigger("analytics", "Analytics")
            TabsTrigger("reports", "Reports", enabled = false)
        }
        TabsContent("overview") { TabsSecondaryText("A summary of your activity.") }
        TabsContent("analytics") { TabsSecondaryText("Detailed metrics and trends.") }
    }
}

API Reference

01

Tabs

Root that holds the active value and orientation.

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

02

TabsList

Row or column of triggers with the animated indicator.

PropTypeDefaultDescription
variant"default" | "line""default"default for a filled segmented track, or line for an underline.

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

03

TabsTrigger

Pressable tab that activates its panel.

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

04

TabsContent

Panel shown when its value is active.

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

Accessibility

  • TabsList has the tablist role and each trigger has the tab role.
  • Triggers report selected and disabled in their accessibility state, so VoiceOver and TalkBack announce the current tab.
  • Hidden panels kept with keepMounted are removed from the accessibility tree.
  • The indicator animation follows the system reduce motion setting.

Notes

  • The default track uses bg-secondary with a bg-card indicator (bg-border in dark mode); the line indicator uses bg-foreground.
  • Labels use text-foreground when active, text-muted-foreground when inactive and opacity-50 when disabled.
  • Light and dark colors follow the system appearance.
iOS
Tabs on iOS in light modeTabs on iOS in dark mode
import * as React from "react"
import { View } from "react-native"
import { Button } from "@/components/ui/button"
import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"
import { Text } from "@/components/ui/text"

function TabsDemo() {
  const [value, setValue] = React.useState("account")
  return (
    <Tabs value={value} onValueChange={setValue} className="w-full">
      <TabsList>
        <TabsTrigger value="account">
          <Text>Account</Text>
        </TabsTrigger>
        <TabsTrigger value="password">
          <Text>Password</Text>
        </TabsTrigger>
      </TabsList>
      <TabsContent value="account">
        <Card>
          <CardHeader>
            <CardTitle>Account</CardTitle>
            <CardDescription>
              Make changes to your account here.
            </CardDescription>
          </CardHeader>
          <CardContent className="gap-4">
            <View className="gap-2">
              <Label nativeID="tabs-name">Name</Label>
              <Input
                accessibilityLabelledBy="tabs-name"
                defaultValue="Pedro Duarte"
              />
            </View>
            <View className="gap-2">
              <Label nativeID="tabs-username">Username</Label>
              <Input
                accessibilityLabelledBy="tabs-username"
                defaultValue="@peduarte"
                autoCapitalize="none"
              />
            </View>
          </CardContent>
          <CardFooter>
            <Button>
              <Text>Save changes</Text>
            </Button>
          </CardFooter>
        </Card>
      </TabsContent>
      <TabsContent value="password">
        <Card>
          <CardHeader>
            <CardTitle>Password</CardTitle>
            <CardDescription>
              Change your password here. You will be logged out after saving.
            </CardDescription>
          </CardHeader>
          <CardContent className="gap-4">
            <View className="gap-2">
              <Label nativeID="tabs-current">Current password</Label>
              <Input accessibilityLabelledBy="tabs-current" secureTextEntry />
            </View>
            <View className="gap-2">
              <Label nativeID="tabs-new">New password</Label>
              <Input accessibilityLabelledBy="tabs-new" secureTextEntry />
            </View>
          </CardContent>
          <CardFooter>
            <Button>
              <Text>Save password</Text>
            </Button>
          </CardFooter>
        </Card>
      </TabsContent>
    </Tabs>
  )
}

export function TabsExample() {
  return (
    <TabsDemo />
  )
}
import EsuissUI
import SwiftUI

struct TabsDemo: View {
  @State private var value = "account"
  @State private var name = "Pedro Duarte"
  @State private var username = "@peduarte"
  @State private var current = ""
  @State private var next = ""

  var body: some View {
    Tabs(selection: $value) {
      TabsList {
        TabsTrigger("Account", value: "account")
        TabsTrigger("Password", value: "password")
      }
      TabsContent(value: "account") {
        Card {
          CardHeader("Account", description: "Make changes to your account here.")
          CardContent {
            VStack(alignment: .leading, spacing: 16) {
              TabsLabeledField(label: "Name") {
                TextField("Name", text: $name).textFieldStyle(.input)
              }
              TabsLabeledField(label: "Username") {
                TextField("Username", text: $username)
                  .textFieldStyle(.input)
                  .textInputAutocapitalization(.never)
              }
            }
          }
          CardFooter {
            Button("Save changes") {}.buttonStyle(.variant())
          }
        }
      }
      TabsContent(value: "password") {
        Card {
          CardHeader("Password", description: "Change your password here. You will be logged out after saving.")
          CardContent {
            VStack(alignment: .leading, spacing: 16) {
              TabsLabeledField(label: "Current password") {
                SecureField("Current password", text: $current).textFieldStyle(.input)
              }
              TabsLabeledField(label: "New password") {
                SecureField("New password", text: $next).textFieldStyle(.input)
              }
            }
          }
          CardFooter {
            Button("Save password") {}.buttonStyle(.variant())
          }
        }
      }
    }
    .frame(maxWidth: .infinity)
  }
}

struct TabsLabeledField<Content: View>: View {
  let label: LocalizedStringKey
  @ViewBuilder let content: () -> Content

  var body: some View {
    VStack(alignment: .leading, spacing: 8) {
      FieldLabel(label)
        .accessibilityHidden(true)
      content()
        .frame(maxWidth: .infinity)
    }
  }
}
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.text.KeyboardOptions
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.semantics.contentDescription
import androidx.compose.ui.semantics.semantics
import androidx.compose.ui.text.input.KeyboardCapitalization
import androidx.compose.ui.text.input.KeyboardType
import androidx.compose.ui.text.input.PasswordVisualTransformation
import androidx.compose.ui.text.input.VisualTransformation
import androidx.compose.ui.unit.dp
import com.esuiss.ui.components.Button
import com.esuiss.ui.components.Card
import com.esuiss.ui.components.CardContent
import com.esuiss.ui.components.CardFooter
import com.esuiss.ui.components.CardHeader
import com.esuiss.ui.components.Input
import com.esuiss.ui.components.Label
import com.esuiss.ui.components.Tabs
import com.esuiss.ui.components.TabsContent
import com.esuiss.ui.components.TabsList
import com.esuiss.ui.components.TabsTrigger

@Composable
fun TabsField(label: String, initial: String, password: Boolean = false, capitalize: Boolean = true) {
    var value by rememberSaveable(label) { mutableStateOf(initial) }
    Column(verticalArrangement = Arrangement.spacedBy(8.dp)) {
        Label(label)
        Input(
            value,
            { value = it },
            Modifier.semantics { contentDescription = label },
            keyboardOptions = when {
                password -> KeyboardOptions(keyboardType = KeyboardType.Password)
                capitalize -> KeyboardOptions.Default
                else -> KeyboardOptions(capitalization = KeyboardCapitalization.None, autoCorrectEnabled = false)
            },
            visualTransformation = if (password) PasswordVisualTransformation() else VisualTransformation.None,
        )
    }
}

@Composable
fun TabsDemo() {
    var value by rememberSaveable { mutableStateOf("account") }
    Tabs(value, { value = it }, Modifier.fillMaxWidth()) {
        TabsList {
            TabsTrigger("account", "Account")
            TabsTrigger("password", "Password")
        }
        TabsContent("account") {
            Card(Modifier.fillMaxWidth()) {
                CardHeader("Account", description = "Make changes to your account here.")
                CardContent {
                    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                        TabsField("Name", "Pedro Duarte")
                        TabsField("Username", "@peduarte", capitalize = false)
                    }
                }
                CardFooter {
                    Button("Save changes", onClick = {})
                }
            }
        }
        TabsContent("password") {
            Card(Modifier.fillMaxWidth()) {
                CardHeader("Password", description = "Change your password here. You will be logged out after saving.")
                CardContent {
                    Column(verticalArrangement = Arrangement.spacedBy(16.dp)) {
                        TabsField("Current password", "", password = true)
                        TabsField("New password", "", password = true)
                    }
                }
                CardFooter {
                    Button("Save password", onClick = {})
                }
            }
        }
    }
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "account",
    label: "Account",
    content: "Update your name, email address, and profile photo.",
  },
  {
    value: "notifications",
    label: "Notifications",
    content: "Choose which updates you receive by email and push.",
  },
  {
    value: "settings",
    label: "Settings",
    content: "Manage language, time zone, and privacy preferences.",
  },
] as const

export default function TabsDemo() {
  return (
    <div className="w-full max-w-96">
      <Tabs defaultValue="account" orientation="horizontal">
        <TabsList variant="default">
          {tabs.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground"
          >
            {tab.content}
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
components/ui/tabs.tsx
"use client"

import { Tabs as TabsPrimitive } from "@base-ui/react/tabs"
import { cva, type VariantProps } from "class-variance-authority"
import * as React from "react"
import { cn } from "@/lib/utils"

function Tabs({
  className,
  orientation = "horizontal",
  ...props
}: TabsPrimitive.Root.Props) {
  return (
    <TabsPrimitive.Root
      data-slot="tabs"
      orientation={orientation}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "account",
    label: "Account",
    content: "Update your name, email address, and profile photo.",
  },
  {
    value: "notifications",
    label: "Notifications",
    content: "Choose which updates you receive by email and push.",
  },
  {
    value: "settings",
    label: "Settings",
    content: "Manage language, time zone, and privacy preferences.",
  },
] as const

export default function TabsLine() {
  return (
    <div className="w-full max-w-96">
      <Tabs defaultValue="account" orientation="horizontal">
        <TabsList variant="line">
          {tabs.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground"
          >
            {tab.content}
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "account",
    label: "Account",
    content: "Update your name, email address, and profile photo.",
  },
  {
    value: "notifications",
    label: "Notifications",
    content: "Choose which updates you receive by email and push.",
  },
  {
    value: "settings",
    label: "Settings",
    content: "Manage language, time zone, and privacy preferences.",
  },
] as const

export default function TabsVertical() {
  return (
    <div className="w-full max-w-96">
      <Tabs defaultValue="account" orientation="vertical">
        <TabsList variant="default">
          {tabs.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground"
          >
            {tab.content}
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
import { BellIcon, GearIcon, UserIcon } from "@phosphor-icons/react"
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "account",
    label: "Account",
    icon: (
      <UserIcon data-icon="inline-start" />
    ),
    content: "Update your name, email address, and profile photo.",
  },
  {
    value: "notifications",
    label: "Notifications",
    icon: (
      <BellIcon data-icon="inline-start" />
    ),
    content: "Choose which updates you receive by email and push.",
  },
  {
    value: "settings",
    label: "Settings",
    icon: (
      <GearIcon data-icon="inline-start" />
    ),
    content: "Manage language, time zone, and privacy preferences.",
  },
] as const

export default function TabsWithIcons() {
  return (
    <div className="w-full max-w-96">
      <Tabs defaultValue="account" orientation="horizontal">
        <TabsList variant="default">
          {tabs.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.icon}
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {tabs.map((tab) => (
          <TabsContent
            key={tab.value}
            value={tab.value}
            className="text-muted-foreground"
          >
            {tab.content}
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const tabs = [
  {
    value: "account",
    label: "Account",
    content: "Update your name, email address, and profile photo.",
  },
  {
    value: "notifications",
    label: "Notifications",
    content: "Choose which updates you receive by email and push.",
  },
  {
    value: "settings",
    label: "Settings",
    content: "Manage language, time zone, and privacy preferences.",
  },
] as const

export default function TabsDisabledTab() {
  return (
    <div className="w-full max-w-96">
      <Tabs defaultValue="account" orientation="horizontal">
        <TabsList variant="default">
          <TabsTrigger value="account">Account</TabsTrigger>
          <TabsTrigger value="notifications">Notifications</TabsTrigger>
          <TabsTrigger value="billing" disabled>
            Billing
          </TabsTrigger>
        </TabsList>
        <TabsContent value="account" className="text-muted-foreground">
          {tabs[0].content}
        </TabsContent>
        <TabsContent value="notifications" className="text-muted-foreground">
          {tabs[1].content}
        </TabsContent>
      </Tabs>
    </div>
  )
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const settings = [
  { value: "general", label: "General" },
  { value: "account", label: "Account" },
  { value: "notifications", label: "Notifications" },
  { value: "privacy", label: "Privacy" },
  { value: "appearance", label: "Appearance" },
  { value: "language", label: "Language" },
  { value: "billing", label: "Billing" },
]

export default function TabsManyTabs() {
  return (
    <div className="w-full">
      <Tabs
        orientation="horizontal"
        defaultValue="general"
        className="w-full max-w-3xl"
      >
        <TabsList variant="default">
          {settings.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {settings.map((tab) => (
          <TabsContent key={tab.value} value={tab.value}>
            <p className="text-muted-foreground">{tab.label} settings</p>
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs"

const settings = [
  { value: "general", label: "General" },
  { value: "account", label: "Account" },
  { value: "notifications", label: "Notifications" },
  { value: "privacy", label: "Privacy" },
  { value: "appearance", label: "Appearance" },
  { value: "language", label: "Language" },
  { value: "billing", label: "Billing" },
]

export default function TabsManyTabsLine() {
  return (
    <div className="w-full">
      <Tabs
        orientation="horizontal"
        defaultValue="general"
        className="w-full max-w-3xl"
      >
        <TabsList variant="line">
          {settings.map((tab) => (
            <TabsTrigger key={tab.value} value={tab.value}>
              {tab.label}
            </TabsTrigger>
          ))}
        </TabsList>
        {settings.map((tab) => (
          <TabsContent key={tab.value} value={tab.value}>
            <p className="text-muted-foreground">{tab.label} settings</p>
          </TabsContent>
        ))}
      </Tabs>
    </div>
  )
}
swift/Components/Tabs.swift
import SwiftUI

public struct Tabs<Content: View>: View {
  let selection: Binding<AnyHashable>
  let orientation: Axis
  let content: Content

  @Environment(\.accessibilityReduceMotion) private var reduceMotion

  public init<Value: Hashable & Sendable>(
    selection: Binding<Value>,
    orientation: Axis = .horizontal,
    @ViewBuilder content: () -> Content
  ) {
    self.selection = Binding(
      get: { AnyHashable(selection.wrappedValue) },
uniwind/components/ui/tabs.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import Animated, {
  Easing,
  LinearTransition,
  ReduceMotion,
} from "react-native-reanimated"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { resolveClassName } from "@/components/primitives/use-render"
import { TextClassContext } from "@/components/ui/text"

type TabsListVariant = NonNullable<
  VariantProps<typeof tabsListVariants>["variant"]
>
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/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)
  }
kotlin/components/Tabs.kt
package com.esuiss.ui.components

import androidx.compose.animation.core.Animatable
import androidx.compose.animation.core.CubicBezierEasing
import androidx.compose.animation.core.tween
import androidx.compose.foundation.background
import androidx.compose.foundation.interaction.MutableInteractionSource
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.ColumnScope
import androidx.compose.foundation.layout.IntrinsicSize
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.RowScope
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
nativewind/components/ui/tabs.tsx
import { cva, type VariantProps } from "class-variance-authority"
import { cn } from "cn"
import * as React from "react"
import Animated, {
  Easing,
  LinearTransition,
  ReduceMotion,
} from "react-native-reanimated"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { resolveClassName } from "@/components/primitives/use-render"
import { TextClassContext } from "@/components/ui/text"

type TabsListVariant = NonNullable<
  VariantProps<typeof tabsListVariants>["variant"]
>
stylesheet/components/ui/tabs.tsx
import * as React from "react"
import { StyleSheet } from "react-native"
import Animated, {
  Easing,
  LinearTransition,
  ReduceMotion,
} from "react-native-reanimated"
import * as TabsPrimitive from "@/components/primitives/tabs"
import { TextStyleContext } from "@/components/ui/text"
import { fontSize, radius, shadow, useTheme } from "@/components/ui/theme"

type TabsListVariant = "default" | "line"

const TabsListContext = React.createContext<TabsListVariant>("default")

const INDICATOR_TRANSITION = LinearTransition.duration(300)