Componentdialog
Dialog
A window overlaid on either the primary window or another dialog window.
Preview
A window overlaid on either the primary window or another dialog window.
@ui.Dialog(ui.DialogProps{}) {
@ui.DialogTrigger(ui.DOMProps{}) {
@ui.Button(ui.ButtonProps{Variant: ui.ButtonVariantOutline}) { Edit Profile }
}
@ui.DialogContent(ui.DOMProps{Class: "sm:max-w-[425px]"}) {
@ui.DialogHeader(ui.DOMProps{}) {
@ui.DialogTitle(ui.DOMProps{}) { Edit profile }
@ui.DialogDescription(ui.DOMProps{}) {
Make changes to your profile here. Click save when you're done.
}
}
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
@ui.Label(ui.LabelProps{For: "name", DOMProps: ui.DOMProps{Class: "text-right"}}) { Name }
@ui.Input(ui.InputProps{ID: "name", Value: "Pedro Duarte", DOMProps: ui.DOMProps{Class: "col-span-3"}})
</div>
</div>
@ui.DialogFooter(ui.DOMProps{}) {
@ui.Button(ui.ButtonProps{Type: "submit", Label: "Save changes"})
}
}
}Installation
Add this component to your project using the CLI.
templcn add dialogUsage
1. Import the component in your Go or templ file:
import "your-module/ui"2. Use the component in your template:
@ui.Dialog(ui.DialogProps{}) {
@ui.DialogTrigger(ui.DOMProps{}) {
@ui.Button(ui.ButtonProps{Variant: ui.ButtonVariantOutline}) { Edit Profile }
}
@ui.DialogContent(ui.DOMProps{Class: "sm:max-w-[425px]"}) {
@ui.DialogHeader(ui.DOMProps{}) {
@ui.DialogTitle(ui.DOMProps{}) { Edit profile }
@ui.DialogDescription(ui.DOMProps{}) {
Make changes to your profile here. Click save when you're done.
}
}
<div class="grid gap-4 py-4">
<div class="grid grid-cols-4 items-center gap-4">
@ui.Label(ui.LabelProps{For: "name", DOMProps: ui.DOMProps{Class: "text-right"}}) { Name }
@ui.Input(ui.InputProps{ID: "name", Value: "Pedro Duarte", DOMProps: ui.DOMProps{Class: "col-span-3"}})
</div>
</div>
@ui.DialogFooter(ui.DOMProps{}) {
@ui.Button(ui.ButtonProps{Type: "submit", Label: "Save changes"})
}
}
}Composition
Use these parts together to build the component.
Dialog
├── DialogTrigger
├── Button
├── DialogContent
├── DialogHeader
├── DialogTitle
├── DialogDescription
├── Label
├── Input
├── DialogFooterExamples
Named examples and common variations.
Default
Modal dialog with form inputs and trigger.
@ui.Dialog(ui.DialogProps{}) {
@ui.DialogTrigger(ui.DOMProps{}) { @ui.Button(ui.ButtonProps{Label: "Open Dialog"}) }
@ui.DialogContent(ui.DOMProps{}) {
@ui.DialogHeader(ui.DOMProps{}) {
@ui.DialogTitle(ui.DOMProps{}) { Dialog Title }
}
}
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| Open | bool | false | Controls dialog visibility. |
| Modal | bool | true | Traps focus inside the dialog. |
View source
package ui
import (
"context"
"io"
"strconv"
"sync/atomic"
"github.com/a-h/templ"
)
type DialogProps struct {
DOMProps
Open bool
DefaultOpen bool
Modal bool
ShowCloseButton bool
}
type dialogRenderState struct {
open bool
slot string
modal bool
showCloseButton bool
}
type dialogRenderStateKey struct{}
type dialogAccessibilityState struct {
contentID string
titleID string
descriptionID string
}
type dialogAccessibilityStateKey struct{}
var dialogAccessibilitySequence uint64
func prepareDialogAccessibility(ctx context.Context, attrs templ.Attributes, prefix string) (context.Context, templ.Attributes) {
contentID, _ := attrs["id"].(string)
if contentID == "" {
contentID = prefix + "-content-" + strconv.FormatUint(atomic.AddUint64(&dialogAccessibilitySequence, 1), 10)
attrs["id"] = contentID
}
state := dialogAccessibilityState{
contentID: contentID,
titleID: contentID + "-title",
descriptionID: contentID + "-description",
}
if _, ok := attrs["aria-labelledby"]; !ok {
attrs["aria-labelledby"] = state.titleID
}
if _, ok := attrs["aria-describedby"]; !ok {
attrs["aria-describedby"] = state.descriptionID
}
return context.WithValue(ctx, dialogAccessibilityStateKey{}, state), attrs
}
func dialogAccessibilityFromContext(ctx context.Context) (dialogAccessibilityState, bool) {
state, ok := ctx.Value(dialogAccessibilityStateKey{}).(dialogAccessibilityState)
return state, ok
}
func Dialog(props DialogProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
open := props.Open || props.DefaultOpen
attrs := attrsFromDOMProps(props.DOMProps, "dialog", "")
attrs["data-state"] = openState(open)
if open {
attrs["data-open"] = "true"
}
if props.DefaultOpen {
attrs["data-default-open"] = "true"
}
if props.Modal {
attrs["data-modal"] = "true"
}
if props.ShowCloseButton {
attrs["data-show-close-button"] = "true"
}
ctx = context.WithValue(ctx, dialogRenderStateKey{}, dialogRenderState{open: open, slot: "dialog", modal: props.Modal, showCloseButton: props.ShowCloseButton})
return renderElement(ctx, w, "div", attrs, templ.GetChildren(ctx))
})
}
func DialogTrigger(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
attrs := attrsFromDOMProps(props, "dialog-trigger", "")
attrs["type"] = "button"
attrs["aria-haspopup"] = "dialog"
if _, ok := attrs["aria-expanded"]; !ok {
attrs["aria-expanded"] = "false"
}
return renderElement(ctx, w, "button", attrs, templ.GetChildren(ctx))
})
}
func DialogPortal(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return renderElement(ctx, w, "div", attrsFromDOMProps(props, "dialog-portal", ""), templ.GetChildren(ctx))
})
}
func DialogOverlay(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
attrs := attrsFromDOMProps(props, "dialog-overlay", "fixed inset-0 z-50 bg-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0")
attrs["aria-hidden"] = "true"
if _, ok := attrs["data-state"]; !ok {
attrs["data-state"] = dialogStateFromContext(ctx)
}
return renderElement(ctx, w, "div", attrs, templ.GetChildren(ctx))
})
}
func DialogContent(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
ctx, ownChildren := childrenFromContext(ctx)
attrs := attrsFromDOMProps(props, "dialog-content", "fixed inset-0 z-50 h-dvh max-h-none w-dvw max-w-none overflow-visible border-0 bg-transparent p-0 text-foreground outline-none backdrop:bg-black/50 [&:not([open])]:hidden")
ctx, attrs = prepareDialogAccessibility(ctx, attrs, "dialog")
state := dialogStateFromContextValue(ctx)
attrs["role"] = "dialog"
attrs["tabindex"] = "-1"
if _, ok := attrs["data-state"]; !ok {
attrs["data-state"] = openState(state.open)
}
if state.open {
attrs["open"] = true
attrs["data-open"] = "true"
}
if state.modal {
attrs["data-modal"] = "true"
}
children := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
panelAttrs := templ.Attributes{
"data-dialog-panel": true,
"class": "fixed left-[50%] top-[50%] z-50 grid translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95",
"data-state": openState(state.open),
}
panel := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
if err := renderChildren(ctx, w, ownChildren); err != nil {
return err
}
if dialogStateFromContextValue(ctx).showCloseButton {
return renderDialogCloseIcon(ctx, w, "dialog-close")
}
return nil
})
return renderElement(ctx, w, "div", panelAttrs, panel)
})
return renderElement(ctx, w, "dialog", attrs, children)
})
}
func DialogHeader(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return renderElement(ctx, w, "div", attrsFromDOMProps(props, "dialog-header", "flex flex-col gap-2 text-center sm:text-left"), templ.GetChildren(ctx))
})
}
func DialogFooter(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
return renderElement(ctx, w, "div", attrsFromDOMProps(props, "dialog-footer", "flex flex-col-reverse gap-2 sm:flex-row sm:justify-end"), templ.GetChildren(ctx))
})
}
func DialogTitle(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
attrs := attrsFromDOMProps(props, "dialog-title", "text-lg leading-none font-semibold")
if state, ok := dialogAccessibilityFromContext(ctx); ok {
if _, exists := attrs["id"]; !exists {
attrs["id"] = state.titleID
}
}
return renderElement(ctx, w, "h2", attrs, templ.GetChildren(ctx))
})
}
func DialogDescription(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
attrs := attrsFromDOMProps(props, "dialog-description", "text-sm text-muted-foreground")
if state, ok := dialogAccessibilityFromContext(ctx); ok {
if _, exists := attrs["id"]; !exists {
attrs["id"] = state.descriptionID
}
}
return renderElement(ctx, w, "p", attrs, templ.GetChildren(ctx))
})
}
func DialogClose(props DOMProps) templ.Component {
return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
attrs := attrsFromDOMProps(props, "dialog-close", "")
attrs["type"] = "button"
return renderElement(ctx, w, "button", attrs, templ.GetChildren(ctx))
})
}
func openState(open bool) string {
if open {
return "open"
}
return "closed"
}
func dialogStateFromContext(ctx context.Context) string {
return openState(dialogStateFromContextValue(ctx).open)
}
func dialogStateFromContextValue(ctx context.Context) dialogRenderState {
state, ok := ctx.Value(dialogRenderStateKey{}).(dialogRenderState)
if !ok {
return dialogRenderState{}
}
return state
}
func renderDialogCloseIcon(ctx context.Context, w io.Writer, slot string) error {
attrs := templ.Attributes{
"type": "button",
"data-slot": slot,
"aria-label": "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 data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
"data-action": "close",
}
icon := templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
_, err := io.WriteString(w, `<svg xmlns="http://www.w3.org/2000/svg" class="size-4" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" aria-hidden="true"><path d="M18 6 6 18"></path><path d="m6 6 12 12"></path></svg>`)
return err
})
return renderElement(ctx, w, "button", attrs, icon)
}