Skip to content
Gx
GitHub

Tabs

Exclusive panels behind a list of tab buttons.

<tabs.Tabs>
  <tabs.TabsList variant={tabs.Default} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Installation

Run the command in the app module.

Terminal
gx add tabs

The command writes these files.

File Path in the app
Tabs.gx ui/tabs/Tabs.gx
TabsList.gx ui/tabs/TabsList.gx
TabsTrigger.gx ui/tabs/TabsTrigger.gx
TabsContent.gx ui/tabs/TabsContent.gx
styles.go ui/tabs/styles.go
Tabs.fixtures.go ui/tabs/Tabs.fixtures.go
TabsList.fixtures.go ui/tabs/TabsList.fixtures.go
TabsTrigger.fixtures.go ui/tabs/TabsTrigger.fixtures.go
TabsContent.fixtures.go ui/tabs/TabsContent.fixtures.go

Copy each file to its path in the app. Change each import of a registry package to the path of that package in the app.

ui/tabs/Tabs.gx
package tabs

props {
  // Orientation sets the layout direction: Horizontal or Vertical. Vertical puts the list beside
  // the panels.
  Orientation Orientation = Horizontal
  // Sync is a key that tab groups share. Groups with the same key keep one selection and remember
  // it per viewer.
  Sync        string = ""
  // Default is the label of the tab that is selected first. An empty value selects the first tab.
  Default     string = ""
  // Class adds classes to the root element.
  Class       string = ""
  // Children is the tab list and the panels.
  Children    gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs       gx.Attrs = nil
}

<div class={gx.Cx("group/tabs flex gap-2 data-[orientation=horizontal]:flex-col", p.Class)} data-gx-tabs data-orientation={p.orientation()} data-sync={p.Sync} data-default={p.Default} {...p.Attrs}>{p.Children}</div>
ui/tabs/TabsList.gx
package tabs

props {
  // Variant sets the visual style: Default, a muted pill, or Line, an underline.
  Variant  Variant = Default
  // Label is the accessible name of the tab list.
  Label    string = ""
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the tab triggers.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<div role="tablist" aria-label={p.Label} data-variant={p.variant()} class={gx.Cx("group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col", variantClass[p.variant()], p.Class)} {...p.Attrs}>{p.Children}</div>
ui/tabs/TabsTrigger.gx
package tabs

props {
  // Label is the key that pairs the trigger with its panel. It is also the text when Children is
  // nil.
  Label    string
  // Disabled disables the trigger.
  Disabled bool = false
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the trigger. It replaces the label text.
  Children gx.Node = nil
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<button type="button" role="tab" aria-selected="false" tabindex="-1" data-gx-tab={p.Label} disabled={p.Disabled} class={gx.Cx("relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[selected=true]:shadow-sm group-data-[variant=line]/tabs-list:data-[selected=true]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[selected=true]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[selected=true]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[selected=true]:bg-transparent data-[selected=true]:bg-background data-[selected=true]:text-foreground dark:data-[selected=true]:border-input dark:data-[selected=true]:bg-input/30 dark:data-[selected=true]:text-foreground after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[selected=true]:after:opacity-100 motion-reduce:transition-none motion-reduce:after:transition-none", p.Class)} {...p.Attrs}>
  if p.Children != nil {
    {p.Children}
  } else {
    {p.Label}
  }
</button>
ui/tabs/TabsContent.gx
package tabs

props {
  // Label is the label of the trigger that shows this panel.
  Label    string
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the panel.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<div role="tabpanel" tabindex="0" data-gx-tab-panel={p.Label} class={gx.Cx("flex-1 outline-none", p.Class)} {...p.Attrs}>{p.Children}</div>
ui/tabs/styles.go
package tabs

import "github.com/alternayte/gx"

// Orientation is the layout direction of a tab group.
type Orientation string

// The orientations of tabs.Tabs.
const (
	Horizontal Orientation = "horizontal"
	Vertical   Orientation = "vertical"
)

// orientation returns the data-orientation value; a zero value is
// Horizontal. The list, the triggers and the behaviour runtime read it.
func (p TabsProps) orientation() string {
	if p.Orientation == "" {
		return string(Horizontal)
	}
	return string(p.Orientation)
}

// Variant is the visual style of a tab list.
type Variant string

// The variants of tabs.TabsList.
const (
	Default Variant = "default"
	Line    Variant = "line"
)

var variantClass = gx.Enum[Variant]{
	Default: "bg-muted",
	Line:    "gap-1 rounded-none bg-transparent",
}

// variant returns the variant of one list; a zero value is Default.
func (p TabsListProps) variant() Variant {
	if p.Variant == "" {
		return Default
	}
	return p.Variant
}
ui/tabs/Tabs.fixtures.go
package tabs

import "github.com/alternayte/gx"

// demo returns a list of two triggers and their two panels.
func demo(variant Variant, first, second string) gx.Node {
	return gx.Frag(
		TabsList(TabsListProps{Variant: variant, Label: "Settings", Children: gx.Frag(
			TabsTrigger(TabsTriggerProps{Label: first}),
			TabsTrigger(TabsTriggerProps{Label: second}),
		)}),
		TabsContent(TabsContentProps{Label: first, Children: gx.Text(first + " settings.")}),
		TabsContent(TabsContentProps{Label: second, Children: gx.Text(second + " settings.")}),
	)
}

var TabsFixtures = gx.Fixtures[TabsProps]{
	"Two":          {Children: demo(Default, "Account", "Password")},
	"Line":         {Children: demo(Line, "Account", "Password")},
	"Vertical":     {Orientation: Vertical, Children: demo(Default, "Account", "Password")},
	"VerticalLine": {Orientation: Vertical, Children: demo(Line, "Account", "Password")},
	"Default":      {Default: "Password", Children: demo(Default, "Account", "Password")},
	"Synced":       {Sync: "demo-tabs", Children: demo(Default, "First", "Second")},
	"Disabled": {Children: gx.Frag(
		TabsList(TabsListProps{Label: "Settings", Children: gx.Frag(
			TabsTrigger(TabsTriggerProps{Label: "Account"}),
			TabsTrigger(TabsTriggerProps{Label: "Billing", Disabled: true}),
			TabsTrigger(TabsTriggerProps{Label: "Password"}),
		)}),
		TabsContent(TabsContentProps{Label: "Account", Children: gx.Text("Account settings.")}),
		TabsContent(TabsContentProps{Label: "Billing", Children: gx.Text("Billing settings.")}),
		TabsContent(TabsContentProps{Label: "Password", Children: gx.Text("Password settings.")}),
	)},
}
ui/tabs/TabsList.fixtures.go
package tabs

import "github.com/alternayte/gx"

var TabsListFixtures = gx.Fixtures[TabsListProps]{
	"Default": {Label: "Settings", Children: gx.Frag(
		TabsTrigger(TabsTriggerProps{Label: "Account"}),
		TabsTrigger(TabsTriggerProps{Label: "Password"}),
	)},
	"Line": {Variant: Line, Label: "Settings", Children: gx.Frag(
		TabsTrigger(TabsTriggerProps{Label: "Account"}),
		TabsTrigger(TabsTriggerProps{Label: "Password"}),
	)},
}

// TabsListWrap renders the list inside a tab group with the panels of its
// triggers, as a page uses it.
func TabsListWrap(n gx.Node) gx.Node {
	return Tabs(TabsProps{Children: gx.Frag(
		n,
		TabsContent(TabsContentProps{Label: "Account", Children: gx.Text("Account settings.")}),
		TabsContent(TabsContentProps{Label: "Password", Children: gx.Text("Password settings.")}),
	)})
}
ui/tabs/TabsTrigger.fixtures.go
package tabs

import "github.com/alternayte/gx"

var TabsTriggerFixtures = gx.Fixtures[TabsTriggerProps]{
	"Trigger":  {Label: "Account"},
	"Disabled": {Label: "Account", Disabled: true},
}

// TabsTriggerWrap renders the trigger inside a tab group with its panel, as
// a page uses it.
func TabsTriggerWrap(n gx.Node) gx.Node {
	return Tabs(TabsProps{Children: gx.Frag(
		TabsList(TabsListProps{Label: "Settings", Children: n}),
		TabsContent(TabsContentProps{Label: "Account", Children: gx.Text("Account settings.")}),
	)})
}
ui/tabs/TabsContent.fixtures.go
package tabs

import "github.com/alternayte/gx"

var TabsContentFixtures = gx.Fixtures[TabsContentProps]{
	"Panel": {Label: "Account", Children: gx.Text("Account settings.")},
}

// TabsContentWrap renders the panel inside a tab group with its trigger, as
// a page uses it.
func TabsContentWrap(n gx.Node) gx.Node {
	return Tabs(TabsProps{Children: gx.Frag(
		TabsList(TabsListProps{Label: "Settings", Children: TabsTrigger(TabsTriggerProps{Label: "Account"})}),
		n,
	)})
}

The theme must define these tokens: --background, --foreground, --muted, --muted-foreground, --input, --ring.

Usage

<tabs.Tabs default="Account">
  <tabs.TabsList label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

A trigger and its panel share one Label. TabsList has the variants tabs.Default (a muted pill) and tabs.Line (an underline). Set Orientation to tabs.Vertical to stack the list beside the panels. Two tab groups with the same Sync key keep one selection and remember it per viewer. The behaviour runtime selects the tab. Without JavaScript, every panel shows.

Examples

Tabs: Two

<tabs.Tabs>
  <tabs.TabsList variant={tabs.Default} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Line

<tabs.Tabs>
  <tabs.TabsList variant={tabs.Line} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Vertical

<tabs.Tabs orientation={tabs.Vertical}>
  <tabs.TabsList variant={tabs.Default} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Vertical line

<tabs.Tabs orientation={tabs.Vertical}>
  <tabs.TabsList variant={tabs.Line} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Default

<tabs.Tabs default="Password">
  <tabs.TabsList variant={tabs.Default} label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Synced

<tabs.Tabs sync="demo-tabs">
  <tabs.TabsList variant={tabs.Default} label="Settings">
    <tabs.TabsTrigger label="First" />
    <tabs.TabsTrigger label="Second" />
  </tabs.TabsList>
  <tabs.TabsContent label="First">First settings.</tabs.TabsContent>
  <tabs.TabsContent label="Second">Second settings.</tabs.TabsContent>
</tabs.Tabs>

Tabs: Disabled

<tabs.Tabs>
  <tabs.TabsList label="Settings">
    <tabs.TabsTrigger label="Account" />
    <tabs.TabsTrigger label="Billing" disabled />
    <tabs.TabsTrigger label="Password" />
  </tabs.TabsList>
  <tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>
  <tabs.TabsContent label="Billing">Billing settings.</tabs.TabsContent>
  <tabs.TabsContent label="Password">Password settings.</tabs.TabsContent>
</tabs.Tabs>

TabsContent: Panel

<tabs.TabsContent label="Account">Account settings.</tabs.TabsContent>

TabsList: Default

<tabs.TabsList label="Settings">
  <tabs.TabsTrigger label="Account" />
  <tabs.TabsTrigger label="Password" />
</tabs.TabsList>

TabsList: Line

<tabs.TabsList variant={tabs.Line} label="Settings">
  <tabs.TabsTrigger label="Account" />
  <tabs.TabsTrigger label="Password" />
</tabs.TabsList>

TabsTrigger: Trigger

<tabs.TabsTrigger label="Account" />

TabsTrigger: Disabled

<tabs.TabsTrigger label="Account" disabled />

API reference

A tag sets a prop by its name with a lower-case first letter: Class is class.

tabs.Tabs

Prop Type Default Description
Orientation Orientation Horizontal Orientation sets the layout direction: Horizontal or Vertical. Vertical puts the list beside the panels.
Sync string "" Sync is a key that tab groups share. Groups with the same key keep one selection and remember it per viewer.
Default string "" Default is the label of the tab that is selected first. An empty value selects the first tab.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the tab list and the panels.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

tabs.TabsContent

Prop Type Default Description
Label string Required Label is the label of the trigger that shows this panel.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the content of the panel.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

tabs.TabsList

Prop Type Default Description
Variant Variant Default Variant sets the visual style: Default, a muted pill, or Line, an underline.
Label string "" Label is the accessible name of the tab list.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the tab triggers.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

tabs.TabsTrigger

Prop Type Default Description
Label string Required Label is the key that pairs the trigger with its panel. It is also the text when Children is nil.
Disabled bool false Disabled disables the trigger.
Class string "" Class adds classes to the root element.
Children gx.Node nil Children is the content of the trigger. It replaces the label text.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Put a short label on each tab.
  • Give the list a Label that names the group.
  • Use Sync when two groups must move together.

Don't

  • Do not hide primary navigation in tabs.
  • Do not use tabs for steps of a process. Use a page or a form.

Keyboard

Key Action
Tab Enters the tab list at the selected tab. Then moves to the panel.
Arrow Right, Arrow Left Selects the next or previous tab of a horizontal list.
Arrow Down, Arrow Up Selects the next or previous tab of a vertical list.
Home, End Selects the first or last tab.
Enter, Space Selects the focused tab.