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.
gx add tabsThe 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.
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>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>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>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>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
}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.")}),
)},
}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.")}),
)})
}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.")}),
)})
}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
Labelthat names the group. - Use
Syncwhen 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. |