Skip to content
Gx
GitHub

Drawer

A bottom panel for a short task.

<drawer.Drawer id="demo-drawer" title="Share this page" description="Choose a destination.">
  <:footer><button.Button>Copy link</button.Button></:footer>
  <:trigger><button.Button variant={button.Outline}>Open drawer</button.Button></:trigger>
  Drawer body.
</drawer.Drawer>

Installation

Run the command in the app module.

Terminal
gx add drawer

The command also installs button.

The command writes these files.

File Path in the app
Drawer.gx ui/drawer/Drawer.gx
Drawer.fixtures.go ui/drawer/Drawer.fixtures.go
styles.go ui/drawer/styles.go

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

props {
  // Id is the id of the dialog element.
  // The trigger points at it, so it must be unique on the page.
  Id          string
  // Side sets the edge the drawer slides from: Bottom, Top, Right or Left.
  Side        Side = Bottom
  // Title is the heading of the drawer. 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 control that opens the drawer.
  Trigger     gx.Node = nil
  // Footer is the content of the drawer footer. A nil value renders no footer.
  Footer      gx.Node = nil
  // Open renders the drawer open. The default is closed.
  Open        bool = false
  // Class adds classes to the dialog element.
  Class       string = ""
  // Children is the content of the drawer body.
  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 data-side={p.side()} class={gx.Cx("group/drawer-content fixed m-0 max-h-none max-w-none flex-col border-border bg-background text-foreground 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}>
    <div class="mx-auto mt-4 hidden h-2 w-[100px] shrink-0 rounded-full bg-muted group-data-[side=bottom]/drawer-content:block"></div>
    if p.Title != "" || p.Description != "" {
      <div class="flex flex-col gap-0.5 p-4 group-data-[side=bottom]/drawer-content:text-center group-data-[side=top]/drawer-content:text-center md:gap-1.5 md:text-left">
        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="overflow-y-auto p-4 pt-0 text-sm">{p.Children}</div>
    }
    if p.Footer != nil {
      <div class="mt-auto flex flex-col gap-2 p-4">{p.Footer}</div>
    }
  </dialog>
</span>
ui/drawer/Drawer.fixtures.go
package drawer

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

var DrawerFixtures = gx.Fixtures[DrawerProps]{
	"Default": {
		Id:          "demo-drawer",
		Title:       "Share this page",
		Description: "Choose a destination.",
		Children:    gx.Text("Drawer body."),
		Footer:      button.Button(button.ButtonProps{Children: gx.Text("Copy link")}),
		Trigger:     button.Button(button.ButtonProps{Variant: button.Outline, Children: gx.Text("Open drawer")}),
	},
}
ui/drawer/styles.go
package drawer

import "github.com/alternayte/gx"

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

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

// A closed drawer rests off its edge; open moves it in, and starting: gives
// the first frame of the slide.
var sideClass = gx.Enum[Side]{
	Bottom: "inset-x-0 bottom-0 top-auto mt-24 h-auto max-h-[80vh] w-full translate-y-full rounded-t-lg border-t open:translate-y-0 starting:open:translate-y-full",
	Top:    "inset-x-0 top-0 bottom-auto mb-24 h-auto max-h-[80vh] w-full -translate-y-full rounded-b-lg border-b open:translate-y-0 starting:open:-translate-y-full",
	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",
}

// side returns the data-side value; a zero value is Bottom.
func (p DrawerProps) side() string {
	if p.Side == "" {
		return string(Bottom)
	}
	return string(p.Side)
}

// sideClass returns the classes of one drawer side.
func (p DrawerProps) sideClass() string {
	return sideClass[Side(p.side())]
}

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

Usage

<drawer.Drawer id="share" title="Share this page">
  <:trigger><button.Button variant={button.Outline}>Share</button.Button></:trigger>
  <p>Choose a destination.</p>
</drawer.Drawer>

Examples

Default

<drawer.Drawer id="demo-drawer" title="Share this page" description="Choose a destination.">
  <:footer><button.Button>Copy link</button.Button></:footer>
  <:trigger><button.Button variant={button.Outline}>Open drawer</button.Button></:trigger>
  Drawer body.
</drawer.Drawer>

API reference

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

drawer.Drawer

Prop Type Default Description
Id string Required Id is the id of the dialog element. The trigger points at it, so it must be unique on the page.
Side Side Bottom Side sets the edge the drawer slides from: Bottom, Top, Right or Left.
Title string "" Title is the heading of the drawer. 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 control that opens the drawer.
Footer gx.Node nil Footer is the content of the drawer footer. A nil value renders no footer.
Open bool false Open renders the drawer open. The default is closed.
Class string "" Class adds classes to the dialog element.
Children gx.Node nil Children is the content of the drawer body.
Attrs gx.Attrs nil Attrs adds HTML attributes to the dialog element.

Do and do not

Do

  • Use a drawer for a short, focused task on a small screen.
  • Keep the footer actions in one column.

Don't

  • Do not put a long form in a drawer.
  • Do not nest a drawer in a sheet.

Keyboard

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