Skip to content
Gx
GitHub

Sheet

A panel that slides in from an edge.

<sheet.Sheet id="demo-sheet" title="Filters" description="Narrow the result set.">
  <:trigger><button.Button variant={button.Outline}>Open sheet</button.Button></:trigger>
  Sheet body.
</sheet.Sheet>

Installation

Run the command in the app module.

Terminal
gx add sheet

The command also installs button and icons.

The command writes these files.

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

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

props {
  // Id is the id of the dialog element. The trigger opens the sheet by this id.
  Id          string
  // Side sets the edge of the screen that the sheet slides from: Right, Left, Top or Bottom.
  Side        Side = Right
  // Title is the heading of the sheet. An empty value renders no heading.
  Title       string = ""
  // Description is the text below the title. An empty value renders no description.
  Description string = ""
  // Trigger is the element that opens the sheet on a click. A nil value renders no trigger.
  Trigger     gx.Node = nil
  // Footer is the content at the bottom of the sheet, usually the action buttons.
  Footer      gx.Node = nil
  // Open renders the sheet open on the first render.
  Open        bool = false
  // Class adds classes to the dialog element.
  Class       string = ""
  // Children is the content of the sheet.
  Children    gx.Node = nil
  // Attrs adds HTML attributes to the dialog element.
  Attrs       gx.Attrs = nil
}

<span class="contents">
  if p.Trigger != nil {
    <span data-gx-open={"#" + p.Id} class="contents">{p.Trigger}</span>
  }
  <dialog id={p.Id} open={p.Open} data-gx-dismiss data-gx-trap class={gx.Cx("fixed m-0 max-h-none max-w-none flex-col gap-4 border-border bg-background text-foreground shadow-lg outline-none open:flex transition-[translate,overlay,display] transition-discrete duration-300 ease-in-out open:duration-500 motion-reduce:transition-none backdrop:bg-black/50 backdrop:opacity-0 backdrop:transition-[opacity,overlay,display] backdrop:transition-discrete backdrop:duration-200 open:backdrop:opacity-100 starting:open:backdrop:opacity-0 motion-reduce:backdrop:transition-none", p.sideClass(), p.Class)} {...p.Attrs}>
    if p.Title != "" || p.Description != "" {
      <div class="flex flex-col gap-1.5 p-4">
        if p.Title != "" {
          <h2 class="font-semibold text-foreground">{p.Title}</h2>
        }
        if p.Description != "" {
          <p class="text-sm text-muted-foreground">{p.Description}</p>
        }
      </div>
    }
    if p.Children != nil {
      <div class="flex-1 overflow-y-auto px-4 text-sm">{p.Children}</div>
    }
    if p.Footer != nil {
      <div class="mt-auto flex flex-col gap-2 p-4">{p.Footer}</div>
    }
    <button type="button" data-gx-close class="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none">
      <icons.X class="size-4" />
      <span class="sr-only">Close</span>
    </button>
  </dialog>
</span>
ui/sheet/Sheet.fixtures.go
package sheet

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

var SheetFixtures = gx.Fixtures[SheetProps]{
	"Right": {
		Id:          "demo-sheet",
		Title:       "Filters",
		Description: "Narrow the result set.",
		Children:    gx.Text("Sheet body."),
		Trigger:     button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Open sheet")}),
	},
	"Bottom": {
		Id:      "demo-sheet-bottom",
		Side:    Bottom,
		Title:   "Filters",
		Trigger: button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Open bottom sheet")}),
	},
}
ui/sheet/styles.go
package sheet

import "github.com/alternayte/gx"

// Side is the edge a sheet slides from.
type Side string

// The sides of sheet.Sheet.
const (
	Right  Side = "right"
	Left   Side = "left"
	Top    Side = "top"
	Bottom Side = "bottom"
)

// A closed sheet rests off its edge; open moves it in, and starting: gives
// the first frame of the slide.
var sideClass = gx.Enum[Side]{
	Right:  "inset-y-0 right-0 left-auto h-full w-3/4 translate-x-full border-l open:translate-x-0 starting:open:translate-x-full sm:max-w-sm",
	Left:   "inset-y-0 left-0 right-auto h-full w-3/4 -translate-x-full border-r open:translate-x-0 starting:open:-translate-x-full sm:max-w-sm",
	Top:    "inset-x-0 top-0 bottom-auto h-auto w-full -translate-y-full border-b open:translate-y-0 starting:open:-translate-y-full",
	Bottom: "inset-x-0 bottom-0 top-auto h-auto w-full translate-y-full border-t open:translate-y-0 starting:open:translate-y-full",
}

// sideClass returns the classes of one sheet side; a zero value is Right.
func (p SheetProps) sideClass() string {
	if p.Side == "" {
		return sideClass[Right]
	}
	return sideClass[p.Side]
}

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

Usage

<sheet.Sheet id="filters" title="Filters" side={sheet.Right}>
  <:trigger><button.Button variant={button.Outline}>Filters</button.Button></:trigger>
  <p>Filter controls.</p>
</sheet.Sheet>

Examples

<sheet.Sheet id="demo-sheet" title="Filters" description="Narrow the result set.">
  <:trigger><button.Button variant={button.Outline}>Open sheet</button.Button></:trigger>
  Sheet body.
</sheet.Sheet>

Bottom

<sheet.Sheet id="demo-sheet-bottom" side={sheet.Bottom} title="Filters">
  <:trigger><button.Button variant={button.Outline}>Open bottom sheet</button.Button></:trigger>
</sheet.Sheet>

API reference

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

sheet.Sheet

Prop Type Default Description
Id string Required Id is the id of the dialog element. The trigger opens the sheet by this id.
Side Side Right Side sets the edge of the screen that the sheet slides from: Right, Left, Top or Bottom.
Title string "" Title is the heading of the sheet. An empty value renders no heading.
Description string "" Description is the text below the title. An empty value renders no description.
Trigger gx.Node nil Trigger is the element that opens the sheet on a click. A nil value renders no trigger.
Footer gx.Node nil Footer is the content at the bottom of the sheet, usually the action buttons.
Open bool false Open renders the sheet open on the first render.
Class string "" Class adds classes to the dialog element.
Children gx.Node nil Children is the content of the sheet.
Attrs gx.Attrs nil Attrs adds HTML attributes to the dialog element.

Do and do not

Do

  • Use the right side for a detail panel and the bottom for a small action sheet.
  • Keep one main task in the sheet.

Don't

  • Do not open a sheet over another overlay.
  • Do not use a sheet for a destructive confirmation. Use an alert dialog.

Keyboard

Key Action
Enter, Space Opens the sheet from the trigger.
Tab Cycles through the controls of the sheet.
Escape Closes the sheet.