Skip to content
Gx
GitHub

Select

A native list of exclusive choices.

<selectbox.Select name="plan" label="Plan" class="w-48">
  <selectbox.SelectOption value="free" selected>Free</selectbox.SelectOption>
  <selectbox.SelectOption value="pro">Pro</selectbox.SelectOption>
  <selectbox.SelectOption value="team">Team</selectbox.SelectOption>
</selectbox.Select>

Installation

Run the command in the app module.

Terminal
gx add select

The command also installs icons.

The command writes these files.

File Path in the app
Select.gx ui/select/Select.gx
SelectGroup.gx ui/select/SelectGroup.gx
SelectOption.gx ui/select/SelectOption.gx
SelectSeparator.gx ui/select/SelectSeparator.gx
Select.fixtures.go ui/select/Select.fixtures.go
SelectGroup.fixtures.go ui/select/SelectGroup.fixtures.go
SelectOption.fixtures.go ui/select/SelectOption.fixtures.go
SelectSeparator.fixtures.go ui/select/SelectSeparator.fixtures.go
styles.go ui/select/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/select/Select.gx
package selectbox

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

props {
  // Name is the form field name.
  Name        string = ""
  // Label is the accessible name of the select.
  Label       string = ""
  // Size sets the height: Md or Sm.
  Size        Size = Md
  // Placeholder is the text of a disabled first option that shows while no value is selected. An
  // empty value renders no placeholder option.
  Placeholder string = ""
  // Class adds classes to the root element.
  Class       string = ""
  // Children is the options of the select.
  Children    gx.Node
  // Attrs adds HTML attributes to the select element.
  Attrs       gx.Attrs = nil
}

<span class={gx.Cx("relative inline-flex w-fit", p.Class)}>
  <select name={p.Name} aria-label={p.Label} class={gx.Cx("w-full min-w-0 appearance-none rounded-md border border-input bg-transparent py-0 pr-9 pl-3 text-sm whitespace-nowrap shadow-xs outline-none transition-[color,box-shadow] focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 has-[option[value='']:checked]:text-muted-foreground dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40", sizeClass[p.size()])} {...p.Attrs}>
    if p.Placeholder != "" {
      <option value="" disabled selected hidden>{p.Placeholder}</option>
    }
    {p.Children}
  </select>
  <icons.ChevronDown class="pointer-events-none absolute top-1/2 right-3 size-4 -translate-y-1/2 text-muted-foreground opacity-50" />
</span>
ui/select/SelectGroup.gx
package selectbox

props {
  // Label is the visible name of the option group.
  Label    string
  // Disabled disables every option of the group.
  Disabled bool = false
  // Children is the options of the group.
  Children gx.Node
}

<optgroup label={p.Label} disabled={p.Disabled}>{p.Children}</optgroup>
ui/select/SelectOption.gx
package selectbox

props {
  // Value is the value the form submits when the option is selected.
  Value    string = ""
  // Selected selects the option for the first render.
  Selected bool = false
  // Disabled disables the option.
  Disabled bool = false
  // Children is the option label.
  Children gx.Node
}

<option value={p.Value} selected={p.Selected} disabled={p.Disabled}>{p.Children}</option>
ui/select/SelectSeparator.gx
package selectbox

props {
  // Attrs adds HTML attributes to the root element.
  Attrs gx.Attrs = nil
}

<hr {...p.Attrs} />
ui/select/Select.fixtures.go
package selectbox

import "github.com/alternayte/gx"

func plans() gx.Node {
	return gx.Frag(
		SelectOption(SelectOptionProps{Value: "free", Selected: true, Children: gx.Text("Free")}),
		SelectOption(SelectOptionProps{Value: "pro", Children: gx.Text("Pro")}),
		SelectOption(SelectOptionProps{Value: "team", Children: gx.Text("Team")}),
	)
}

var SelectFixtures = gx.Fixtures[SelectProps]{
	"Plan":     {Name: "plan", Label: "Plan", Class: "w-48", Children: plans()},
	"Small":    {Name: "plan", Label: "Plan", Size: Sm, Class: "w-48", Children: plans()},
	"Fit":      {Name: "plan", Label: "Plan", Children: plans()},
	"Disabled": {Name: "plan", Label: "Plan", Class: "w-48", Attrs: gx.Attrs{gx.Bool("disabled", true)}, Children: SelectOption(SelectOptionProps{Value: "free", Children: gx.Text("Free")})},
	"Invalid":  {Name: "plan", Label: "Plan", Class: "w-48", Attrs: gx.Attrs{{Key: "aria-invalid", Value: "true"}}, Children: plans()},
	"Placeholder": {Name: "fruit", Label: "Fruit", Placeholder: "Select a fruit", Class: "w-48", Children: gx.Frag(
		SelectGroup(SelectGroupProps{Label: "Fruits", Children: gx.Frag(
			SelectOption(SelectOptionProps{Value: "apple", Children: gx.Text("Apple")}),
			SelectOption(SelectOptionProps{Value: "banana", Children: gx.Text("Banana")}),
		)}),
		SelectSeparator(SelectSeparatorProps{}),
		SelectGroup(SelectGroupProps{Label: "Vegetables", Children: gx.Frag(
			SelectOption(SelectOptionProps{Value: "carrot", Children: gx.Text("Carrot")}),
			SelectOption(SelectOptionProps{Value: "leek", Disabled: true, Children: gx.Text("Leek")}),
		)}),
	)},
}
ui/select/SelectGroup.fixtures.go
package selectbox

import "github.com/alternayte/gx"

var SelectGroupFixtures = gx.Fixtures[SelectGroupProps]{
	"Group": {Label: "Fruits", Children: gx.Frag(
		SelectOption(SelectOptionProps{Value: "apple", Children: gx.Text("Apple")}),
		SelectOption(SelectOptionProps{Value: "banana", Children: gx.Text("Banana")}),
	)},
}

// SelectGroupWrap renders the group inside a select, as a page uses it.
func SelectGroupWrap(n gx.Node) gx.Node {
	return Select(SelectProps{Label: "Fruit", Children: n})
}
ui/select/SelectOption.fixtures.go
package selectbox

import "github.com/alternayte/gx"

var SelectOptionFixtures = gx.Fixtures[SelectOptionProps]{"Option": {Value: "free", Children: gx.Text("Free")}}

// SelectOptionWrap renders the option inside a select, as a page uses it.
func SelectOptionWrap(n gx.Node) gx.Node {
	return Select(SelectProps{Label: "Plan", Children: n})
}
ui/select/SelectSeparator.fixtures.go
package selectbox

import "github.com/alternayte/gx"

var SelectSeparatorFixtures = gx.Fixtures[SelectSeparatorProps]{"Default": {}}

// SelectSeparatorWrap renders the separator between two options of a select.
func SelectSeparatorWrap(n gx.Node) gx.Node {
	return Select(SelectProps{Label: "Plan", Children: gx.Frag(
		SelectOption(SelectOptionProps{Value: "free", Children: gx.Text("Free")}),
		n,
		SelectOption(SelectOptionProps{Value: "pro", Children: gx.Text("Pro")}),
	)})
}
ui/select/styles.go
package selectbox

import "github.com/alternayte/gx"

// Size is the height of a select.
type Size string

// The sizes of selectbox.Select.
const (
	Md Size = "default"
	Sm Size = "sm"
)

var sizeClass = gx.Enum[Size]{
	Md: "h-9",
	Sm: "h-8",
}

// size returns the size of one select; a zero value is Md.
func (p SelectProps) size() Size {
	if p.Size == "" {
		return Md
	}
	return p.Size
}

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

Usage

<selectbox.Select name="fruit" label="Fruit" placeholder="Select a fruit" class="w-48">
  <selectbox.SelectGroup label="Fruits">
    <selectbox.SelectOption value="apple">Apple</selectbox.SelectOption>
    <selectbox.SelectOption value="banana">Banana</selectbox.SelectOption>
  </selectbox.SelectGroup>
  <selectbox.SelectSeparator />
  <selectbox.SelectOption value="carrot" selected={true}>Carrot</selectbox.SelectOption>
</selectbox.Select>

The component is a styled native <select>, so the keyboard, the option list and the mobile picker come from the browser. Size is selectbox.Md (default) or selectbox.Sm. Class sets the width of the control; without it the control is as wide as its longest option. Attrs go on the <select>.

Placeholder shows muted text until the user selects an option. SelectGroup is an <optgroup> and SelectSeparator is an <hr> between options.

Examples

Select: Plan

<selectbox.Select name="plan" label="Plan" class="w-48">
  <selectbox.SelectOption value="free" selected>Free</selectbox.SelectOption>
  <selectbox.SelectOption value="pro">Pro</selectbox.SelectOption>
  <selectbox.SelectOption value="team">Team</selectbox.SelectOption>
</selectbox.Select>

Select: Small

<selectbox.Select name="plan" label="Plan" size={selectbox.Sm} class="w-48">
  <selectbox.SelectOption value="free" selected>Free</selectbox.SelectOption>
  <selectbox.SelectOption value="pro">Pro</selectbox.SelectOption>
  <selectbox.SelectOption value="team">Team</selectbox.SelectOption>
</selectbox.Select>

Select: Fit

<selectbox.Select name="plan" label="Plan">
  <selectbox.SelectOption value="free" selected>Free</selectbox.SelectOption>
  <selectbox.SelectOption value="pro">Pro</selectbox.SelectOption>
  <selectbox.SelectOption value="team">Team</selectbox.SelectOption>
</selectbox.Select>

Select: Disabled

<selectbox.Select name="plan" label="Plan" class="w-48" attrs={gx.Attrs{gx.Bool("disabled", true)}}>
  <selectbox.SelectOption value="free">Free</selectbox.SelectOption>
</selectbox.Select>

Select: Invalid

<selectbox.Select name="plan" label="Plan" class="w-48" attrs={gx.Attrs{{Key: "aria-invalid", Value: "true"}}}>
  <selectbox.SelectOption value="free" selected>Free</selectbox.SelectOption>
  <selectbox.SelectOption value="pro">Pro</selectbox.SelectOption>
  <selectbox.SelectOption value="team">Team</selectbox.SelectOption>
</selectbox.Select>

Select: Placeholder

<selectbox.Select name="fruit" label="Fruit" placeholder="Select a fruit" class="w-48">
  <selectbox.SelectGroup label="Fruits">
    <selectbox.SelectOption value="apple">Apple</selectbox.SelectOption>
    <selectbox.SelectOption value="banana">Banana</selectbox.SelectOption>
  </selectbox.SelectGroup>
  <selectbox.SelectSeparator />
  <selectbox.SelectGroup label="Vegetables">
    <selectbox.SelectOption value="carrot">Carrot</selectbox.SelectOption>
    <selectbox.SelectOption value="leek" disabled>Leek</selectbox.SelectOption>
  </selectbox.SelectGroup>
</selectbox.Select>

SelectGroup: Group

<selectbox.SelectGroup label="Fruits">
  <selectbox.SelectOption value="apple">Apple</selectbox.SelectOption>
  <selectbox.SelectOption value="banana">Banana</selectbox.SelectOption>
</selectbox.SelectGroup>

SelectOption: Option

<selectbox.SelectOption value="free">Free</selectbox.SelectOption>

SelectSeparator: Default

<selectbox.SelectSeparator />

API reference

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

selectbox.Select

Prop Type Default Description
Name string "" Name is the form field name.
Label string "" Label is the accessible name of the select.
Size Size Md Size sets the height: Md or Sm.
Placeholder string "" Placeholder is the text of a disabled first option that shows while no value is selected. An empty value renders no placeholder option.
Class string "" Class adds classes to the root element.
Children gx.Node Required Children is the options of the select.
Attrs gx.Attrs nil Attrs adds HTML attributes to the select element.

selectbox.SelectGroup

Prop Type Default Description
Label string Required Label is the visible name of the option group.
Disabled bool false Disabled disables every option of the group.
Children gx.Node Required Children is the options of the group.

selectbox.SelectOption

Prop Type Default Description
Value string "" Value is the value the form submits when the option is selected.
Selected bool false Selected selects the option for the first render.
Disabled bool false Disabled disables the option.
Children gx.Node Required Children is the option label.

selectbox.SelectSeparator

Prop Type Default Description
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Mark one option with Selected, or set a Placeholder.
  • Pair the select with a label, or set Label.
  • Set aria-invalid through Attrs when the value fails validation.

Don't

  • Do not use a select for fewer than four options. Use a radio group.
  • Do not leave the select without a name in a form.

Keyboard

Key Action
Tab Moves focus to the select.
Arrow keys Moves through the options.
A letter Selects the next option that starts with the typed text.
Enter, Space Opens or commits the choice.