Toast
Server-pushed toasts: the toaster region and the toast.
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Text: "Event created"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Event created")Installation
Run the command in the app module.
gx add toastThe command writes these files.
| File | Path in the app |
|---|---|
Toaster.gx |
ui/toast/Toaster.gx |
Toast.gx |
ui/toast/Toast.gx |
icons.go |
ui/toast/icons.go |
Toaster.fixtures.go |
ui/toast/Toaster.fixtures.go |
Toast.fixtures.go |
ui/toast/Toast.fixtures.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.
package toast
props {
// Class adds classes to the root element.
Class string = ""
// Children is the toasts the toaster holds at render. Nil renders an empty toaster.
Children gx.Node = nil
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div id="gx-toaster" role="region" aria-label="Notifications" aria-live="polite" data-gx-toaster class={gx.Cx("fixed right-4 bottom-4 z-50 flex w-[356px] max-w-[calc(100vw-2rem)] flex-col gap-3", p.Class)} {...p.Attrs}>{p.Children}</div>package toast
props {
// Toast is the pushed toast: its text, kind, description, action and timing.
Toast gx.ToastPatch
// Class adds classes to the root element.
Class string = ""
// Attrs adds HTML attributes to the root element.
Attrs gx.Attrs = nil
}
<div class={gx.Cx("flex w-full items-start gap-2 rounded-lg border border-border bg-popover p-4 text-sm text-popover-foreground shadow-lg transition-[opacity,translate] duration-200 ease-out starting:translate-y-2 starting:opacity-0 data-[closing]:pointer-events-none data-[closing]:translate-x-4 data-[closing]:opacity-0 motion-reduce:transition-none", p.Class)} {...gx.ToastAttrs(p.Toast)} {...p.Attrs}>
if p.icon() != "" {
{gx.Icon(p.icon(), gx.IconProps{Class: p.iconClass()})}
}
<div class="grid min-w-0 flex-1 gap-0.5">
<div class="leading-5 font-medium">{p.Toast.Text}</div>
if p.Toast.Description != "" {
<div class="leading-5 text-muted-foreground">{p.Toast.Description}</div>
}
</div>
if p.Toast.Action.Method != "" {
<button type="button" data-gx-close class="inline-flex h-6 shrink-0 items-center rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50" {...p.invoke()}>{p.Toast.Action.Label}</button>
} else if p.Toast.Action.Label != "" {
<a href={p.Toast.Action.URL} class="inline-flex h-6 shrink-0 items-center rounded-md bg-primary px-2 text-xs font-medium text-primary-foreground outline-none hover:bg-primary/90 focus-visible:ring-[3px] focus-visible:ring-ring/50">{p.Toast.Action.Label}</a>
}
<button type="button" data-gx-close aria-label="Close" class="-my-0.5 -mr-1 inline-flex size-6 shrink-0 items-center justify-center rounded-md text-muted-foreground outline-none hover:bg-accent hover:text-accent-foreground focus-visible:ring-[3px] focus-visible:ring-ring/50">
{gx.Icon(iconX, gx.IconProps{Class: "size-4"})}
</button>
</div>package toast
import "github.com/alternayte/gx"
// The icon bodies come from Lucide (https://lucide.dev), ISC License,
// Copyright (c) Lucide Contributors.
const (
iconCircleCheck = `<circle cx="12" cy="12" r="10"/><path d="m9 12 2 2 4-4"/>`
iconInfo = `<circle cx="12" cy="12" r="10"/><path d="M12 16v-4"/><path d="M12 8h.01"/>`
iconTriangleAlert = `<path d="m21.73 18-8-14a2 2 0 0 0-3.48 0l-8 14A2 2 0 0 0 4 21h16a2 2 0 0 0 1.73-3"/><path d="M12 9v4"/><path d="M12 17h.01"/>`
iconOctagonX = `<path d="m15 9-6 6"/><path d="M2.586 16.726A2 2 0 0 1 2 15.312V8.688a2 2 0 0 1 .586-1.414l4.688-4.688A2 2 0 0 1 8.688 2h6.624a2 2 0 0 1 1.414.586l4.688 4.688A2 2 0 0 1 22 8.688v6.624a2 2 0 0 1-.586 1.414l-4.688 4.688a2 2 0 0 1-1.414.586H8.688a2 2 0 0 1-1.414-.586z"/><path d="m9 9 6 6"/>`
iconLoaderCircle = `<path d="M21 12a9 9 0 1 1-6.219-8.56"/>`
iconX = `<path d="M18 6 6 18"/><path d="m6 6 12 12"/>`
)
// icon returns the icon body of the toast kind, or "" for the default kind.
func (p ToastProps) icon() string {
switch p.Toast.Kind {
case gx.ToastSuccess:
return iconCircleCheck
case gx.ToastInfo:
return iconInfo
case gx.ToastWarning:
return iconTriangleAlert
case gx.ToastError:
return iconOctagonX
case gx.ToastLoading:
return iconLoaderCircle
}
return ""
}
// iconClass returns the classes of the kind icon. The icon sits on the first
// text line, and the loading icon turns.
func (p ToastProps) iconClass() string {
const base = "mt-0.5 size-4 shrink-0"
if p.Toast.Kind == gx.ToastLoading {
return base + " animate-spin motion-reduce:animate-none"
}
return base
}
// invoke returns the attribute that invokes the action of the toast. The
// adapter writes it. data-gx-close on the same button closes the toast.
func (p ToastProps) invoke() gx.Attrs {
return gx.Attrs{gx.Invoke(p.Toast.Action.Method, string(p.Toast.Action.URL), "")}
}
// Render renders one pushed toast. An app passes it as gx.Config.Toast.
func Render(p gx.ToastPatch) gx.Node {
return Toast(ToastProps{Toast: p})
}package toast
import "github.com/alternayte/gx"
// The fixture toaster is static, so it stays inside its gallery section, and
// its toast is sticky, so it stays for the audits.
var ToasterFixtures = gx.Fixtures[ToasterProps]{
"WithToast": {Class: "static", Children: Render(gx.ToastPatch{Text: "Saved", Sticky: true})},
}package toast
import "github.com/alternayte/gx"
// fixtureWidth is the width a toast has inside the toaster.
const fixtureWidth = "max-w-[356px]"
var ToastFixtures = gx.Fixtures[ToastProps]{
"Default": {Class: fixtureWidth, Toast: gx.ToastPatch{Text: "Event created"}},
"Success": {Class: fixtureWidth, Toast: gx.ToastPatch{Kind: gx.ToastSuccess, Text: "Changes saved"}},
"Info": {Class: fixtureWidth, Toast: gx.ToastPatch{Kind: gx.ToastInfo, Text: "A new version is available"}},
"Warning": {Class: fixtureWidth, Toast: gx.ToastPatch{Kind: gx.ToastWarning, Text: "Your trial ends in 3 days"}},
"Error": {Class: fixtureWidth, Toast: gx.ToastPatch{Kind: gx.ToastError, Text: "The upload failed"}},
"Loading": {Class: fixtureWidth, Toast: gx.ToastPatch{Kind: gx.ToastLoading, Text: "Uploading the file"}},
"WithDescription": {Class: fixtureWidth, Toast: gx.ToastPatch{
Kind: gx.ToastSuccess,
Text: "Event created",
Description: "Monday, 12 January at 09:00",
}},
"WithLink": {Class: fixtureWidth, Toast: gx.ToastPatch{
Text: "Item added to the cart",
Description: "Open the cart to check out.",
Action: gx.ToastControl{Label: "View", URL: "/"},
}},
"WithAction": {Class: fixtureWidth, Toast: gx.ToastPatch{
Text: "Item removed from the cart",
Action: gx.ToastControl{Label: "Undo", URL: "/", Method: "POST"},
}},
}The theme must define these tokens: --popover, --popover-foreground, --border, --muted-foreground, --primary, --primary-foreground, --accent, --accent-foreground, --ring.
Usage
Render the toaster once, in the root layout.
<toast.Toaster />Pass toast.Render to the app. The app then renders every pushed toast with the Toast component of this item.
app := gx.New(gx.Config{Adapter: datastar.Adapter(), Toast: toast.Render})An action or a form pushes a toast.
return c.Toast("Saved")A kind is an option. The kinds are gx.ToastDefault, gx.ToastSuccess, gx.ToastInfo, gx.ToastWarning, gx.ToastError and gx.ToastLoading.
return c.Toast("Changes saved", gx.ToastSuccess)Add a description, a duration or a link to a route.
return c.Toast("Item added to the cart",
gx.ToastDescription("Open the cart to check out."),
gx.ToastDuration(8*time.Second),
gx.ToastLink("View", route.Cart{}))Add a button that invokes an action, for example Undo. Pass the route value of the action. The toast closes when the user presses the button.
return c.Toast("Item removed", gx.ToastAction("Undo", route.Undo{ID: in.ID}))A toast holds one control. When an action passes gx.ToastLink and gx.ToastAction, the later option wins.
A toast leaves after 4 seconds. A loading toast and a gx.ToastSticky toast stay until the user closes them.
Give two toasts the same ID. The second toast replaces the first in place.
// The first action.
return c.Toast("Uploading the file", gx.ToastLoading, gx.ToastID("upload"))
// A later action.
return c.Toast("File uploaded", gx.ToastSuccess, gx.ToastID("upload"))A failed action or form pushes its error as a gx.ToastError toast.
The toaster shows at most three toasts. The oldest toast leaves first.
Examples
Toast: Default
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Text: "Event created"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Event created")Toast: Success
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Kind: gx.ToastSuccess, Text: "Changes saved"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Changes saved", gx.ToastSuccess)Toast: Info
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Kind: gx.ToastInfo, Text: "A new version is available"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("A new version is available", gx.ToastInfo)Toast: Warning
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Kind: gx.ToastWarning, Text: "Your trial ends in 3 days"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Your trial ends in 3 days", gx.ToastWarning)Toast: Error
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Kind: gx.ToastError, Text: "The upload failed"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("The upload failed", gx.ToastError)Toast: Loading
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{Kind: gx.ToastLoading, Text: "Uploading the file"}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Uploading the file", gx.ToastLoading)Toast: With description
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{
Kind: gx.ToastSuccess,
Text: "Event created",
Description: "Monday, 12 January at 09:00",
}} />The toast renderer of the app writes this tag. An action sends the toast.
return c.Toast("Event created",
gx.ToastSuccess,
gx.ToastDescription("Monday, 12 January at 09:00"))Toast: With link
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{
Text: "Item added to the cart",
Description: "Open the cart to check out.",
Action: gx.ToastControl{Label: "View", URL: "/"},
}} />Toast: With action
<toast.Toast class="max-w-[356px]" toast={gx.ToastPatch{
Text: "Item removed from the cart",
Action: gx.ToastControl{Label: "Undo", URL: "/", Method: "POST"},
}} />Toaster: With toast
<toast.Toaster class="static">{toast.Render(gx.ToastPatch{Text: "Saved", Sticky: true})}</toast.Toaster>API reference
A tag sets a prop by its name with a lower-case first letter: Class is class.
toast.Toast
| Prop | Type | Default | Description |
|---|---|---|---|
Toast |
gx.ToastPatch |
Required | Toast is the pushed toast: its text, kind, description, action and timing. |
Class |
string |
"" |
Class adds classes to the root element. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
toast.Toaster
| Prop | Type | Default | Description |
|---|---|---|---|
Class |
string |
"" |
Class adds classes to the root element. |
Children |
gx.Node |
nil |
Children is the toasts the toaster holds at render. Nil renders an empty toaster. |
Attrs |
gx.Attrs |
nil |
Attrs adds HTML attributes to the root element. |
Do and do not
Do
- Render one toaster in the root layout.
- Set
Toast: toast.Renderingx.Config. - Keep the text short. Put detail in the description.
- Use one ID for the steps of one operation.
Don't
- Do not render a toaster per page.
- Do not use a toast for a value the user must not miss. Use an alert.
- Do not put the only path to a task in a toast link or a toast action. The toast leaves.
- Do not write toast classes in package
gxcode. ChangeToast.gx.
Keyboard
| Key | Action |
|---|---|
| Tab | Moves focus to the link or the action button, and to the close button of a toast. The timers stop while focus is in the toaster. |
| Enter | Follows the focused link, or invokes the action and closes the toast on the action button, or closes the toast on the close button. |
| Space | Invokes the action and closes the toast on the action button, or closes the toast on the close button. |
| Escape | Closes the toast that holds focus. Focus returns to the element that had it before the toaster. |