Skip to content
Gx
GitHub

Button

A button with the shadcn variants and sizes.

<button.Button>Button</button.Button>

Installation

Run the command in the app module.

Terminal
gx add button

The command also installs icons.

The command writes these files.

File Path in the app
Button.gx ui/button/Button.gx
Button.fixtures.go ui/button/Button.fixtures.go
styles.go ui/button/styles.go

Install 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/Button.gx
package button

props {
  // Variant sets the visual style: Default, Secondary, Destructive, Outline, Ghost or Link.
  Variant  Variant = Default
  // Size sets the height and padding: Xs, Sm, Md, Lg, or an Icon size for a square button.
  Size     Size = Md
  // Type is the type attribute: "button", "submit" or "reset".
  Type     string = "button"
  // Class adds classes to the root element.
  Class    string = ""
  // Children is the button label.
  Children gx.Node
  // Attrs adds HTML attributes to the root element.
  Attrs    gx.Attrs = nil
}

<button data-slot="button" data-variant={p.variant()} data-size={p.size()} type={p.typeAttr()} class={Class(p.Variant, p.Size, p.Class)} {...p.Attrs}>{p.Children}</button>
ui/button/Button.fixtures.go
package button

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

// iconLabel is the accessible name of an icon-only fixture.
var iconLabel = gx.Attrs{{Key: "aria-label", Value: "Next"}}

var ButtonFixtures = gx.Fixtures[ButtonProps]{
	"Default":     {Children: gx.Text("Button")},
	"Secondary":   {Variant: Secondary, Children: gx.Text("Secondary")},
	"Destructive": {Variant: Destructive, Children: gx.Text("Delete")},
	"Outline":     {Variant: Outline, Children: gx.Text("Outline")},
	"Ghost":       {Variant: Ghost, Children: gx.Text("Ghost")},
	"Link":        {Variant: Link, Children: gx.Text("Link")},
	"ExtraSmall":  {Size: Xs, Children: gx.Text("Extra small")},
	"Small":       {Size: Sm, Children: gx.Text("Small")},
	"Large":       {Size: Lg, Children: gx.Text("Large")},
	"WithIcon":    {Variant: Outline, Children: gx.Frag(gx.Text("Next"), icons.ChevronRight(icons.ChevronRightProps{}))},
	"Icon":        {Variant: Outline, Size: Icon, Attrs: iconLabel, Children: icons.ChevronRight(icons.ChevronRightProps{})},
	"IconXs":      {Variant: Outline, Size: IconXs, Attrs: iconLabel, Children: icons.ChevronRight(icons.ChevronRightProps{})},
	"IconSm":      {Variant: Outline, Size: IconSm, Attrs: iconLabel, Children: icons.ChevronRight(icons.ChevronRightProps{})},
	"IconLg":      {Variant: Outline, Size: IconLg, Attrs: iconLabel, Children: icons.ChevronRight(icons.ChevronRightProps{})},
	"Disabled":    {Attrs: gx.Attrs{gx.Bool("disabled", true)}, Children: gx.Text("Disabled")},
}
ui/button/styles.go
package button

import "github.com/alternayte/gx"

// Variant is the visual style of a button.
type Variant string

// The variants of button.Button.
const (
	Default     Variant = "default"
	Secondary   Variant = "secondary"
	Destructive Variant = "destructive"
	Outline     Variant = "outline"
	Ghost       Variant = "ghost"
	Link        Variant = "link"
)

var variantClass = gx.Enum[Variant]{
	Default:     "bg-primary text-primary-foreground hover:bg-primary/90",
	Secondary:   "bg-secondary text-secondary-foreground hover:bg-secondary/80",
	Destructive: "bg-destructive text-white hover:bg-destructive/90 focus-visible:ring-destructive/20 dark:bg-destructive/60 dark:focus-visible:ring-destructive/40",
	Outline:     "border border-border bg-background shadow-xs hover:bg-accent hover:text-accent-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50",
	Ghost:       "hover:bg-accent hover:text-accent-foreground dark:hover:bg-accent/50",
	Link:        "text-primary underline-offset-4 hover:underline",
}

// Size is the height and padding of a button.
type Size string

// The sizes of button.Button.
const (
	Xs     Size = "xs"
	Sm     Size = "sm"
	Md     Size = "default"
	Lg     Size = "lg"
	Icon   Size = "icon"
	IconXs Size = "icon-xs"
	IconSm Size = "icon-sm"
	IconLg Size = "icon-lg"
)

var sizeClass = gx.Enum[Size]{
	Xs:     "h-6 gap-1 rounded-md px-2 text-xs has-[>svg]:px-1.5 [&_svg:not([class*='size-'])]:size-3",
	Sm:     "h-8 gap-1.5 rounded-md px-3 has-[>svg]:px-2.5",
	Md:     "h-9 px-4 py-2 has-[>svg]:px-3",
	Lg:     "h-10 rounded-md px-6 has-[>svg]:px-4",
	Icon:   "size-9",
	IconXs: "size-6 rounded-md [&_svg:not([class*='size-'])]:size-3",
	IconSm: "size-8",
	IconLg: "size-10",
}

// typeAttr returns the button type; a zero value is "button".
func (p ButtonProps) typeAttr() string {
	if p.Type == "" {
		return "button"
	}
	return p.Type
}

// variant returns the data-variant value; a zero value is Default.
func (p ButtonProps) variant() string {
	if p.Variant == "" {
		return string(Default)
	}
	return string(p.Variant)
}

// size returns the data-size value; a zero value is Md.
func (p ButtonProps) size() string {
	if p.Size == "" {
		return string(Md)
	}
	return string(p.Size)
}

// Class returns the classes of one button state. A link that looks like a
// button takes these classes on its anchor.
func Class(variant Variant, size Size, extra ...string) string {
	const base = "inline-flex shrink-0 items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-all outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
	// A zero-value prop is the default, so a Go caller and a .gx caller
	// render the same markup.
	if variant == "" {
		variant = Default
	}
	if size == "" {
		size = Md
	}
	return gx.Cx(append([]string{base, variantClass[variant], sizeClass[size]}, extra...)...)
}

The theme must define these tokens: --primary, --primary-foreground, --secondary, --secondary-foreground, --destructive, --accent, --accent-foreground, --background, --border, --input, --ring.

Usage

A clickable control with variant and size options.

<button.Button>Save</button.Button>
<button.Button variant={button.Destructive}>Delete</button.Button>
<button.Button variant={button.Outline} size={button.Sm}>Cancel</button.Button>
<button.Button variant={button.Ghost} size={button.Icon} attrs={gx.Attrs{{Key: "aria-label", Value: "Close"}}}>
  <icons.X />
</button.Button>

Pass Type when the button submits a form: <button.Button type="submit">Send</button.Button>.

The sizes are Xs, Sm, Md, Lg, Icon, IconXs, IconSm and IconLg. An icon without a size- class takes the size of the button.

A link that looks like a button takes button.Class on its anchor: <a href={route} class={button.Class(button.Outline, button.Sm)}>Docs</a>.

Examples

Default

<button.Button>Button</button.Button>

Secondary

<button.Button variant={button.Secondary}>Secondary</button.Button>

Destructive

<button.Button variant={button.Destructive}>Delete</button.Button>

Outline

<button.Button variant={button.Outline}>Outline</button.Button>

Ghost

<button.Button variant={button.Ghost}>Ghost</button.Button>
<button.Button variant={button.Link}>Link</button.Button>

Extra small

<button.Button size={button.Xs}>Extra small</button.Button>

Small

<button.Button size={button.Sm}>Small</button.Button>

Large

<button.Button size={button.Lg}>Large</button.Button>

With icon

<button.Button variant={button.Outline}>
  Next
  <icons.ChevronRight />
</button.Button>

Icon

<button.Button variant={button.Outline} size={button.Icon} attrs={gx.Attrs{{Key: "aria-label", Value: "Next"}}}>
  <icons.ChevronRight />
</button.Button>

Icon xs

<button.Button variant={button.Outline} size={button.IconXs} attrs={gx.Attrs{{Key: "aria-label", Value: "Next"}}}>
  <icons.ChevronRight />
</button.Button>

Icon sm

<button.Button variant={button.Outline} size={button.IconSm} attrs={gx.Attrs{{Key: "aria-label", Value: "Next"}}}>
  <icons.ChevronRight />
</button.Button>

Icon lg

<button.Button variant={button.Outline} size={button.IconLg} attrs={gx.Attrs{{Key: "aria-label", Value: "Next"}}}>
  <icons.ChevronRight />
</button.Button>

Disabled

<button.Button attrs={gx.Attrs{gx.Bool("disabled", true)}}>Disabled</button.Button>

API reference

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

button.Button

Prop Type Default Description
Variant Variant Default Variant sets the visual style: Default, Secondary, Destructive, Outline, Ghost or Link.
Size Size Md Size sets the height and padding: Xs, Sm, Md, Lg, or an Icon size for a square button.
Type string "button" Type is the type attribute: "button", "submit" or "reset".
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the button label.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Use one default button per view for the main action.
  • Set type="submit" on a form button. The default button type does not submit.
  • Give an icon-only button an aria-label.

Don't

  • Do not use a button as a link. Use an <a> with a typed href and button.Class.
  • Do not put a button inside another button.

Keyboard

Key Action
Tab Moves focus to the button.
Enter Activates the button.
Space Activates the button.