Skip to content
Gx
GitHub

Button Group

A row of related buttons.

<buttongroup.ButtonGroup>
  <button.Button variant={button.Outline}>One</button.Button>
  <button.Button variant={button.Outline}>Two</button.Button>
  <button.Button variant={button.Outline}>Three</button.Button>
</buttongroup.ButtonGroup>

Installation

Run the command in the app module.

Terminal
gx add button-group

The command also installs button and icons.

The command writes these files.

File Path in the app
ButtonGroup.gx ui/button-group/ButtonGroup.gx
ButtonGroup.fixtures.go ui/button-group/ButtonGroup.fixtures.go
ButtonGroupText.gx ui/button-group/ButtonGroupText.gx
ButtonGroupText.fixtures.go ui/button-group/ButtonGroupText.fixtures.go
ButtonGroupSeparator.gx ui/button-group/ButtonGroupSeparator.gx
ButtonGroupSeparator.fixtures.go ui/button-group/ButtonGroupSeparator.fixtures.go
styles.go ui/button-group/styles.go

Install button and icons first.

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/button-group/ButtonGroup.gx
package buttongroup

props {
  // Orientation sets the direction of the group: Horizontal for a row or Vertical for a column.
  Orientation Orientation = Horizontal
  // Children is the content of the group: the buttons, inputs, separators and text.
  Children    gx.Node
  // Class adds classes to the root element.
  Class       string = ""
  // Attrs adds HTML attributes to the root element.
  Attrs       gx.Attrs = nil
}

<div role="group" data-slot="button-group" data-orientation={p.orientation()} class={gx.Cx("flex w-fit items-stretch has-[>[data-slot=button-group]]:gap-2 [&>*]:focus-visible:relative [&>*]:focus-visible:z-10 [&>[data-slot=select-trigger]:not([class*='w-'])]:w-fit [&>input]:flex-1", orientationClass[Orientation(p.orientation())], p.Class)} {...p.Attrs}>{p.Children}</div>
ui/button-group/ButtonGroup.fixtures.go
package buttongroup

import (
	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/button"
	"github.com/alternayte/gx/registry/icons"
)

var ButtonGroupFixtures = gx.Fixtures[ButtonGroupProps]{
	"Three": {Children: gx.Frag(
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("One")}),
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Two")}),
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Three")}),
	)},
	"Vertical": {Orientation: Vertical, Children: gx.Frag(
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("One")}),
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Two")}),
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Three")}),
	)},
	"Separator": {Children: gx.Frag(
		button.Button(button.ButtonProps{Variant: button.Secondary, Children: gx.Text("Copy")}),
		ButtonGroupSeparator(ButtonGroupSeparatorProps{}),
		button.Button(button.ButtonProps{Variant: button.Secondary, Size: button.Icon, Attrs: gx.Attrs{{Key: "aria-label", Value: "More"}}, Children: icons.ChevronDown(icons.ChevronDownProps{})}),
	)},
	"Text": {Children: gx.Frag(
		ButtonGroupText(ButtonGroupTextProps{Children: gx.Text("https://")}),
		button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("example.com")}),
	)},
	"Nested": {Children: gx.Frag(
		ButtonGroup(ButtonGroupProps{Children: gx.Frag(
			button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("One")}),
			button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Two")}),
		)}),
		ButtonGroup(ButtonGroupProps{Children: button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Three")})}),
	)},
}
ui/button-group/ButtonGroupText.gx
package buttongroup

props {
  // For is the id of an input. A non-empty value renders the text as the label of that input.
  For      string = ""
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the content of the text block.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

if p.For != "" {
  <label for={p.For} class={gx.Cx("flex items-center gap-2 rounded-md border border-border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", p.Class)} {...p.Attrs}>{p.Children}</label>
} else {
  <div class={gx.Cx("flex items-center gap-2 rounded-md border border-border bg-muted px-4 text-sm font-medium shadow-xs [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4", p.Class)} {...p.Attrs}>{p.Children}</div>
}
ui/button-group/ButtonGroupText.fixtures.go
package buttongroup

import "github.com/alternayte/gx"

var ButtonGroupTextFixtures = gx.Fixtures[ButtonGroupTextProps]{"Text": {Children: gx.Text("https://")}}
ui/button-group/ButtonGroupSeparator.gx
package buttongroup

props {
  // Orientation sets the direction of the line: Vertical or Horizontal. Use Vertical in a
  // horizontal group.
  Orientation Orientation = Vertical
  // Class adds classes to the root element.
  Class       string = ""
  // Attrs adds HTML attributes to the root element.
  Attrs       gx.Attrs = nil
}

<div data-slot="button-group-separator" role="none" data-orientation={p.orientation()} class={gx.Cx("shrink-0 bg-border", separatorClass[Orientation(p.orientation())], "relative m-0! self-stretch bg-input", p.Class)} {...p.Attrs}></div>
ui/button-group/ButtonGroupSeparator.fixtures.go
package buttongroup

import (
	"github.com/alternayte/gx"
	"github.com/alternayte/gx/registry/button"
)

var ButtonGroupSeparatorFixtures = gx.Fixtures[ButtonGroupSeparatorProps]{"Default": {}}

// ButtonGroupSeparatorWrap renders the separator between two buttons, as a
// group uses it.
func ButtonGroupSeparatorWrap(n gx.Node) gx.Node {
	return ButtonGroup(ButtonGroupProps{Children: gx.Frag(
		button.Button(button.ButtonProps{Variant: button.Secondary, Children: gx.Text("One")}),
		n,
		button.Button(button.ButtonProps{Variant: button.Secondary, Children: gx.Text("Two")}),
	)})
}
ui/button-group/styles.go
package buttongroup

import "github.com/alternayte/gx"

// Orientation is the direction of a button group or of its separator.
type Orientation string

// The orientations of buttongroup.ButtonGroup and
// buttongroup.ButtonGroupSeparator.
const (
	Horizontal Orientation = "horizontal"
	Vertical   Orientation = "vertical"
)

var orientationClass = gx.Enum[Orientation]{
	Horizontal: "[&>*:not(:first-child)]:rounded-l-none [&>*:not(:first-child)]:border-l-0 [&>*:not(:last-child)]:rounded-r-none",
	Vertical:   "flex-col [&>*:not(:first-child)]:rounded-t-none [&>*:not(:first-child)]:border-t-0 [&>*:not(:last-child)]:rounded-b-none",
}

// separatorClass sizes the separator: a vertical line takes the height of
// the group, a horizontal line takes its width.
var separatorClass = gx.Enum[Orientation]{
	Horizontal: "h-px w-full",
	Vertical:   "h-auto w-px",
}

// orientation returns the data-orientation value; a zero value is
// Horizontal.
func (p ButtonGroupProps) orientation() string {
	if p.Orientation == "" {
		return string(Horizontal)
	}
	return string(p.Orientation)
}

// orientation returns the data-orientation value; a zero value is
// Vertical, the line between the buttons of a horizontal group.
func (p ButtonGroupSeparatorProps) orientation() string {
	if p.Orientation == "" {
		return string(Vertical)
	}
	return string(p.Orientation)
}

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

Usage

A row or a column of related buttons.

<buttongroup.ButtonGroup>
  <button.Button variant={button.Outline}>One</button.Button>
  <button.Button variant={button.Outline}>Two</button.Button>
</buttongroup.ButtonGroup>

<buttongroup.ButtonGroup>
  <button.Button variant={button.Secondary}>Copy</button.Button>
  <buttongroup.ButtonGroupSeparator />
  <button.Button variant={button.Secondary} size={button.Icon}><icons.ChevronDown /></button.Button>
</buttongroup.ButtonGroup>

Set orientation={buttongroup.Vertical} for a column. ButtonGroupText shows text or a label next to a button or an input. Set For to make it the label of an input. ButtonGroupSeparator draws a line between two buttons that have no border. A group in a group makes a gap between the inner groups.

Examples

ButtonGroup: Three

<buttongroup.ButtonGroup>
  <button.Button variant={button.Outline}>One</button.Button>
  <button.Button variant={button.Outline}>Two</button.Button>
  <button.Button variant={button.Outline}>Three</button.Button>
</buttongroup.ButtonGroup>

ButtonGroup: Vertical

<buttongroup.ButtonGroup orientation={buttongroup.Vertical}>
  <button.Button variant={button.Outline}>One</button.Button>
  <button.Button variant={button.Outline}>Two</button.Button>
  <button.Button variant={button.Outline}>Three</button.Button>
</buttongroup.ButtonGroup>

ButtonGroup: Separator

<buttongroup.ButtonGroup>
  <button.Button variant={button.Secondary}>Copy</button.Button>
  <buttongroup.ButtonGroupSeparator />
  <button.Button variant={button.Secondary} size={button.Icon} attrs={gx.Attrs{{Key: "aria-label", Value: "More"}}}>
    <icons.ChevronDown />
  </button.Button>
</buttongroup.ButtonGroup>

ButtonGroup: Text

<buttongroup.ButtonGroup>
  <buttongroup.ButtonGroupText>https://</buttongroup.ButtonGroupText>
  <button.Button variant={button.Outline}>example.com</button.Button>
</buttongroup.ButtonGroup>

ButtonGroup: Nested

<buttongroup.ButtonGroup>
  <buttongroup.ButtonGroup>
    <button.Button variant={button.Outline}>One</button.Button>
    <button.Button variant={button.Outline}>Two</button.Button>
  </buttongroup.ButtonGroup>
  <buttongroup.ButtonGroup><button.Button variant={button.Outline}>Three</button.Button></buttongroup.ButtonGroup>
</buttongroup.ButtonGroup>

ButtonGroupSeparator: Default

<buttongroup.ButtonGroupSeparator />

ButtonGroupText: Text

<buttongroup.ButtonGroupText>https://</buttongroup.ButtonGroupText>

API reference

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

buttongroup.ButtonGroup

Prop Type Default Description
Orientation Orientation Horizontal Orientation sets the direction of the group: Horizontal for a row or Vertical for a column.
Children gx.Node Required Children is the content of the group: the buttons, inputs, separators and text.
Class string "" Class adds classes to the root element.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

buttongroup.ButtonGroupSeparator

Prop Type Default Description
Orientation Orientation Vertical Orientation sets the direction of the line: Vertical or Horizontal. Use Vertical in a horizontal group.
Class string "" Class adds classes to the root element.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

buttongroup.ButtonGroupText

Prop Type Default Description
For string "" For is the id of an input. A non-empty value renders the text as the label of that input.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the content of the text block.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Give the group an aria-label when its purpose is not clear from the buttons.
  • Use the same variant for every button of a group.

Don't

  • Do not mix button sizes in one group.
  • Do not put a separator between outline buttons. Their borders divide them.

Keyboard

Key Action
Tab Moves focus from one button to the next.
Enter Activates the focused button.
Space Activates the focused button.