Skip to content
Gx
GitHub

Slider

A native range control.

<slider.Slider name="volume" value={50} label="Volume" />

Installation

Run the command in the app module.

Terminal
gx add slider

The command writes these files.

File Path in the app
Slider.gx ui/slider/Slider.gx
Slider.fixtures.go ui/slider/Slider.fixtures.go
styles.go ui/slider/styles.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/slider/Slider.gx
package slider

props {
  // Orientation sets the direction: Horizontal or Vertical.
  Orientation Orientation = Horizontal
  // Name is the form field name.
  Name        string = ""
  // Min is the minimum value.
  Min         int = 0
  // Max is the maximum value. A zero value is 100.
  Max         int = 100
  // Value is the value of the first render.
  Value       int = 0
  // Step is the size of one step. A zero value is 1.
  Step        int = 1
  // Label is the accessible name of the slider.
  Label       string = ""
  // Disabled disables the slider.
  Disabled    bool = false
  // Class adds classes to the root element.
  Class       string = ""
  // Attrs adds HTML attributes to the root element.
  Attrs       gx.Attrs = nil
}

<input type="range" name={p.Name} min={p.min()} max={p.max()} value={p.Value} step={p.step()} aria-label={p.Label} disabled={p.Disabled} data-gx-behavior style={p.fill()} class={p.class()} {...p.Attrs} />
ui/slider/Slider.fixtures.go
package slider

import "github.com/alternayte/gx"

var SliderFixtures = gx.Fixtures[SliderProps]{
	"Half":     {Name: "volume", Value: 50, Label: "Volume"},
	"Full":     {Name: "volume", Value: 100, Label: "Volume"},
	"Empty":    {Name: "volume", Value: 0, Label: "Volume"},
	"Range":    {Name: "price", Min: 10, Max: 200, Value: 80, Step: 10, Label: "Price"},
	"Disabled": {Name: "volume", Value: 50, Label: "Volume", Disabled: true},
	"Vertical": {Name: "level", Value: 50, Label: "Level", Orientation: Vertical, Class: "h-44"},
}
ui/slider/styles.go
package slider

import (
	"strconv"

	"github.com/alternayte/gx"
)

// Orientation is the direction of a slider.
type Orientation string

// The orientations of slider.Slider.
const (
	Horizontal Orientation = "horizontal"
	Vertical   Orientation = "vertical"
)

// The track and the thumb of a native range input are pseudo-elements with
// one name per engine. WebKit and Blink have no pseudo-element for the range,
// so the track paints it with a gradient up to --gx-fill; Gecko has
// ::-moz-range-progress.
var orientationClass = gx.Enum[Orientation]{
	Horizontal: "h-1.5 w-full [&::-webkit-slider-runnable-track]:h-1.5 [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_right,var(--primary)_var(--gx-fill),var(--muted)_var(--gx-fill))] [&::-webkit-slider-thumb]:-mt-[5px] [&::-moz-range-track]:h-1.5 [&::-moz-range-progress]:h-1.5",
	Vertical:   "h-full min-h-44 w-1.5 [direction:rtl] [writing-mode:vertical-lr] [&::-webkit-slider-runnable-track]:w-1.5 [&::-webkit-slider-runnable-track]:bg-[linear-gradient(to_top,var(--primary)_var(--gx-fill),var(--muted)_var(--gx-fill))] [&::-webkit-slider-thumb]:-ml-[5px] [&::-moz-range-track]:w-1.5 [&::-moz-range-progress]:w-1.5",
}

// class returns the classes of one slider.
func (p SliderProps) class() string {
	const base = "block touch-none appearance-none bg-transparent outline-none select-none disabled:pointer-events-none disabled:opacity-50 [&::-webkit-slider-runnable-track]:rounded-full [&::-webkit-slider-thumb]:box-border [&::-webkit-slider-thumb]:size-4 [&::-webkit-slider-thumb]:appearance-none [&::-webkit-slider-thumb]:rounded-full [&::-webkit-slider-thumb]:border [&::-webkit-slider-thumb]:border-primary [&::-webkit-slider-thumb]:bg-white [&::-webkit-slider-thumb]:shadow-sm [&::-webkit-slider-thumb]:ring-ring/50 [&::-webkit-slider-thumb]:transition-[color,box-shadow] [&::-webkit-slider-thumb]:hover:ring-4 focus-visible:[&::-webkit-slider-thumb]:ring-4 motion-reduce:[&::-webkit-slider-thumb]:transition-none [&::-moz-range-track]:rounded-full [&::-moz-range-track]:bg-muted [&::-moz-range-progress]:rounded-full [&::-moz-range-progress]:bg-primary [&::-moz-range-thumb]:box-border [&::-moz-range-thumb]:size-4 [&::-moz-range-thumb]:rounded-full [&::-moz-range-thumb]:border [&::-moz-range-thumb]:border-primary [&::-moz-range-thumb]:bg-white [&::-moz-range-thumb]:shadow-sm [&::-moz-range-thumb]:ring-ring/50 [&::-moz-range-thumb]:transition-[color,box-shadow] [&::-moz-range-thumb]:hover:ring-4 focus-visible:[&::-moz-range-thumb]:ring-4 motion-reduce:[&::-moz-range-thumb]:transition-none"
	orientation := p.Orientation
	if orientation == "" {
		orientation = Horizontal
	}
	return gx.Cx(base, orientationClass[orientation], p.Class)
}

// min returns the minimum value.
func (p SliderProps) min() int {
	return p.Min
}

// max returns the maximum value; a zero value is 100.
func (p SliderProps) max() int {
	if p.Max == 0 {
		return 100
	}
	return p.Max
}

// step returns the step; a zero value is 1.
func (p SliderProps) step() int {
	if p.Step == 0 {
		return 1
	}
	return p.Step
}

// fill returns the custom property that paints the range of the first
// render. The behaviour runtime updates it on input.
func (p SliderProps) fill() gx.Style {
	span := p.max() - p.min()
	v := p.Value - p.min()
	if span <= 0 || v < 0 {
		v, span = 0, 1
	}
	if v > span {
		v = span
	}
	return gx.Style("--gx-fill: " + strconv.Itoa(v*100/span) + "%")
}

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

Usage

<slider.Slider name="volume" value={p.Volume} label="Volume" />
<slider.Slider name="level" value={p.Level} label="Level" orientation={slider.Vertical} />

The component is a styled native <input type="range">. The server paints the range for the first render. The behaviour runtime repaints it on input in Chrome and Safari. Firefox paints the range itself. A slider has one thumb.

Examples

Half

<slider.Slider name="volume" value={50} label="Volume" />

Full

<slider.Slider name="volume" value={100} label="Volume" />

Empty

<slider.Slider name="volume" value={0} label="Volume" />

Range

<slider.Slider name="price" min={10} max={200} value={80} step={10} label="Price" />

Disabled

<slider.Slider name="volume" value={50} label="Volume" disabled />

Vertical

<slider.Slider name="level" value={50} label="Level" orientation={slider.Vertical} class="h-44" />

API reference

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

slider.Slider

Prop Type Default Description
Orientation Orientation Horizontal Orientation sets the direction: Horizontal or Vertical.
Name string "" Name is the form field name.
Min int 0 Min is the minimum value.
Max int 100 Max is the maximum value. A zero value is 100.
Value int 0 Value is the value of the first render.
Step int 1 Step is the size of one step. A zero value is 1.
Label string "" Label is the accessible name of the slider.
Disabled bool false Disabled disables the slider.
Class string "" Class adds classes to the root element.
Attrs gx.Attrs nil Attrs adds HTML attributes to the root element.

Do and do not

Do

  • Set Min, Max and Step for the range.
  • Give the slider a Label.

Don't

  • Do not use a slider for an exact number. Use an input.
  • Do not omit the label.

Keyboard

Key Action
Tab Moves focus to the slider.
Arrow keys Changes the value by one step.
Home, End Moves to the minimum or maximum.