Toast
Flexible toast component for notifications and feedback.
TailwindCSS
Alpine.js
package showcase
import "github.com/axzilla/templui/pkg/components"
templ Toast() {
<div>
<form
class="flex flex-col gap-2"
hx-post="/docs/toast/demo"
hx-trigger="submit"
hx-target="#toast-container"
>
@components.Button(components.ButtonProps{
Text: "Show Toast",
Type: "submit",
})
</form>
<div id="toast-container"></div>
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"strconv"
)
type ToastProps struct {
Message string // Message to display
Type string // Type of the toast
Position string // Position of the toast
Duration int // Duration in milliseconds
Dismissible bool // Show dismiss button
Size string // Size of the toast
Icon bool // Show icon
}
func (p ToastProps) withDefaults() ToastProps {
if p.Message == "" {
p.Message = "Notification"
}
if p.Type == "" {
p.Type = "default"
}
if p.Position == "" {
p.Position = "bottom-right"
}
if p.Duration == 0 {
p.Duration = 3000
}
if p.Size == "" {
p.Size = "md"
}
return p
}
// Flexible toast component for notifications and feedback.
templ ToastScript() {
{{ handler := templ.NewOnceHandle() }}
@handler.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('toast', () => ({
show: true,
duration: 0,
timer: null,
init() {
this.duration = parseInt(this.$el.dataset.duration || 0);
this.startTimer();
if (this.duration > 0) {
const progress = this.$refs.progress;
if (progress) {
progress.style.transition = `width ${this.duration}ms linear`;
progress.style.width = '100%';
setTimeout(() => {
progress.style.width = '0%';
}, 10);
}
}
},
startTimer() {
if (this.duration <= 0) return;
this.timer = setTimeout(() => {
this.show = false;
}, this.duration);
},
pauseTimer() {
if (this.timer) clearTimeout(this.timer);
const progress = this.$refs.progress;
if (progress) {
const width = progress.getBoundingClientRect().width;
const total = progress.parentElement.getBoundingClientRect().width;
this.duration = (width / total) * this.duration;
progress.style.transition = "none";
progress.style.width = width + "px";
}
},
resumeTimer() {
const progress = this.$refs.progress;
if (progress) {
progress.style.transition = "width " + this.duration + "ms linear";
progress.style.width = "0";
this.startTimer();
}
},
dismissToast() {
this.show = false;
}
}))
})
</script>
}
}
templ Toast(props ToastProps) {
// Apply defaults before rendering
{{ props = props.withDefaults() }}
<div
data-duration={ strconv.Itoa(props.Duration) }
x-data="toast"
@mouseenter="pauseTimer"
@mouseleave="resumeTimer"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class={
"z-50 fixed pointer-events-auto",
templ.KV("top-4 right-4", props.Position == "top-right"),
templ.KV("top-4 left-4", props.Position == "top-left"),
templ.KV("top-4 left-1/2 -translate-x-1/2", props.Position == "top-center"),
templ.KV("bottom-4 right-4", props.Position == "bottom-right"),
templ.KV("bottom-4 left-4", props.Position == "bottom-left"),
templ.KV("bottom-4 left-1/2 -translate-x-1/2", props.Position == "bottom-center"),
templ.KV("w-72", props.Size == "sm"),
templ.KV("w-96", props.Size == "md"),
templ.KV("w-[30rem]", props.Size == "lg"),
}
>
<div class="bg-primary-foreground rounded-lg shadow-sm border pt-5 pb-4 px-4 flex items-center justify-center relative overflow-hidden">
if props.Duration > 0 {
<div class="absolute top-0 left-0 right-0 h-1">
<div
x-ref="progress"
class={
"absolute inset-0",
templ.KV("bg-gray-500", props.Type == "default"),
templ.KV("bg-green-500", props.Type == "success"),
templ.KV("bg-red-500", props.Type == "error"),
templ.KV("bg-yellow-500", props.Type == "warning"),
templ.KV("bg-blue-500", props.Type == "info"),
}
></div>
</div>
}
if props.Icon {
if props.Type == "success" {
@icons.CircleCheck(icons.IconProps{Size: "18", Class: "text-green-500 mr-3"})
}
if props.Type == "error" {
@icons.CircleX(icons.IconProps{Size: "18", Class: "text-red-500 mr-3"})
}
if props.Type == "warning" {
@icons.TriangleAlert(icons.IconProps{Size: "18", Class: "text-yellow-500 mr-3"})
}
if props.Type == "info" {
@icons.Info(icons.IconProps{Size: "18", Class: "text-blue-500 mr-3"})
}
}
<div class="flex-1">{ props.Message }</div>
if props.Dismissible {
<button @click.stop="dismissToast">
@icons.X(icons.IconProps{
Size: "18",
Class: "opacity-75 hover:opacity-100",
})
</button>
}
</div>
</div>
}
Usage
1. Add the script to your page/layout:
// Option A: All components (recommended)
@utils.ComponentScripts()
// Option B: Just Toast
@components.ToastScript()
2. Use the component:
@components.Toast(components.ToastProps{})
// Template
templ UserForm() {
<form hx-post="/save" hx-target="#toast">
<input name="email" />
</form>
<div id="toast"></div>
}
// Handler
func Save(w http.ResponseWriter, r *http.Request) {
if err != nil {
components.Toast(components.ToastProps{
Message: err.Error(),
Type: "error",
}).Render(r.Context(), w)
}
}
// Template
templ UserForm(error string) {
if error != "" {
@components.Toast(components.ToastProps{
Message: error,
Type: "error",
})
}
<form method="POST">
<input name="email"/>
</form>
}
// Handler
func Save(w http.ResponseWriter, r *http.Request) {
if err != nil {
UserForm(err.Error()).Render(r.Context(), w)
}
}
Examples
Playground
package showcase
import "github.com/axzilla/templui/pkg/components"
templ ToastAdvanced() {
<div class="w-full max-w-4xl mx-auto p-8">
<section class="mb-12">
@components.Card(components.CardProps{}) {
@components.CardContent() {
<form
class="flex flex-col gap-2"
hx-post="/docs/toast/demo"
hx-trigger="submit"
hx-target="#toast-advanced-container"
>
// Message
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Message"})
@components.Input(components.InputProps{
Value: "Test Notification",
Name: "message",
})
}
// Type
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Type"})
@components.Select(components.SelectProps{
Name: "type",
Options: []components.SelectOption{
{Value: "default", Label: "Default"},
{Value: "success", Label: "Success"},
{Value: "error", Label: "Error"},
{Value: "warning", Label: "Warning"},
{Value: "info", Label: "Info"},
},
})
}
// Position
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Position"})
@components.Select(components.SelectProps{
Name: "position",
Options: []components.SelectOption{
{Value: "top-right", Label: "Top Right"},
{Value: "top-left", Label: "Top Left"},
{Value: "top-center", Label: "Top Center"},
{Value: "bottom-right", Label: "Bottom Right", Selected: true},
{Value: "bottom-left", Label: "Bottom Left"},
{Value: "bottom-center", Label: "Bottom Center"},
},
})
}
// Duration
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Duration (ms)"})
@components.Input(components.InputProps{
Type: "number",
Name: "duration",
Value: "3000",
})
}
// Size
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Size"})
@components.Select(components.SelectProps{
Name: "size",
Options: []components.SelectOption{
{Value: "sm", Label: "Small"},
{Value: "md", Label: "Medium"},
{Value: "lg", Label: "Large"},
},
})
}
// Options
@components.FormItem(components.FormItemProps{}) {
@components.Label(components.LabelProps{Text: "Options"})
@components.FormItemFlex(components.FormItemProps{}) {
@components.Toggle(components.ToggleProps{
Name: "dismissible",
Checked: true,
})
@components.Label(components.LabelProps{Text: "Dismissible"})
}
@components.FormItemFlex(components.FormItemProps{}) {
@components.Toggle(components.ToggleProps{
Name: "icon",
Checked: true,
})
@components.Label(components.LabelProps{Text: "Show Icon"})
}
}
// Submit
@components.Button(components.ButtonProps{
Text: "Show Toast",
Type: "submit",
Class: "w-full",
})
</form>
}
}
</section>
<div id="toast-advanced-container"></div>
</div>
}
package components
import (
"github.com/axzilla/templui/pkg/icons"
"strconv"
)
type ToastProps struct {
Message string // Message to display
Type string // Type of the toast
Position string // Position of the toast
Duration int // Duration in milliseconds
Dismissible bool // Show dismiss button
Size string // Size of the toast
Icon bool // Show icon
}
func (p ToastProps) withDefaults() ToastProps {
if p.Message == "" {
p.Message = "Notification"
}
if p.Type == "" {
p.Type = "default"
}
if p.Position == "" {
p.Position = "bottom-right"
}
if p.Duration == 0 {
p.Duration = 3000
}
if p.Size == "" {
p.Size = "md"
}
return p
}
// Flexible toast component for notifications and feedback.
templ ToastScript() {
{{ handler := templ.NewOnceHandle() }}
@handler.Once() {
<script defer nonce={ templ.GetNonce(ctx) }>
document.addEventListener('alpine:init', () => {
Alpine.data('toast', () => ({
show: true,
duration: 0,
timer: null,
init() {
this.duration = parseInt(this.$el.dataset.duration || 0);
this.startTimer();
if (this.duration > 0) {
const progress = this.$refs.progress;
if (progress) {
progress.style.transition = `width ${this.duration}ms linear`;
progress.style.width = '100%';
setTimeout(() => {
progress.style.width = '0%';
}, 10);
}
}
},
startTimer() {
if (this.duration <= 0) return;
this.timer = setTimeout(() => {
this.show = false;
}, this.duration);
},
pauseTimer() {
if (this.timer) clearTimeout(this.timer);
const progress = this.$refs.progress;
if (progress) {
const width = progress.getBoundingClientRect().width;
const total = progress.parentElement.getBoundingClientRect().width;
this.duration = (width / total) * this.duration;
progress.style.transition = "none";
progress.style.width = width + "px";
}
},
resumeTimer() {
const progress = this.$refs.progress;
if (progress) {
progress.style.transition = "width " + this.duration + "ms linear";
progress.style.width = "0";
this.startTimer();
}
},
dismissToast() {
this.show = false;
}
}))
})
</script>
}
}
templ Toast(props ToastProps) {
// Apply defaults before rendering
{{ props = props.withDefaults() }}
<div
data-duration={ strconv.Itoa(props.Duration) }
x-data="toast"
@mouseenter="pauseTimer"
@mouseleave="resumeTimer"
x-show="show"
x-transition:enter="transition ease-out duration-300"
x-transition:enter-start="opacity-0 translate-y-4"
x-transition:enter-end="opacity-100 translate-y-0"
x-transition:leave="transition ease-in duration-200"
x-transition:leave-start="opacity-100 translate-y-0"
x-transition:leave-end="opacity-0 translate-y-4"
class={
"z-50 fixed pointer-events-auto",
templ.KV("top-4 right-4", props.Position == "top-right"),
templ.KV("top-4 left-4", props.Position == "top-left"),
templ.KV("top-4 left-1/2 -translate-x-1/2", props.Position == "top-center"),
templ.KV("bottom-4 right-4", props.Position == "bottom-right"),
templ.KV("bottom-4 left-4", props.Position == "bottom-left"),
templ.KV("bottom-4 left-1/2 -translate-x-1/2", props.Position == "bottom-center"),
templ.KV("w-72", props.Size == "sm"),
templ.KV("w-96", props.Size == "md"),
templ.KV("w-[30rem]", props.Size == "lg"),
}
>
<div class="bg-primary-foreground rounded-lg shadow-sm border pt-5 pb-4 px-4 flex items-center justify-center relative overflow-hidden">
if props.Duration > 0 {
<div class="absolute top-0 left-0 right-0 h-1">
<div
x-ref="progress"
class={
"absolute inset-0",
templ.KV("bg-gray-500", props.Type == "default"),
templ.KV("bg-green-500", props.Type == "success"),
templ.KV("bg-red-500", props.Type == "error"),
templ.KV("bg-yellow-500", props.Type == "warning"),
templ.KV("bg-blue-500", props.Type == "info"),
}
></div>
</div>
}
if props.Icon {
if props.Type == "success" {
@icons.CircleCheck(icons.IconProps{Size: "18", Class: "text-green-500 mr-3"})
}
if props.Type == "error" {
@icons.CircleX(icons.IconProps{Size: "18", Class: "text-red-500 mr-3"})
}
if props.Type == "warning" {
@icons.TriangleAlert(icons.IconProps{Size: "18", Class: "text-yellow-500 mr-3"})
}
if props.Type == "info" {
@icons.Info(icons.IconProps{Size: "18", Class: "text-blue-500 mr-3"})
}
}
<div class="flex-1">{ props.Message }</div>
if props.Dismissible {
<button @click.stop="dismissToast">
@icons.X(icons.IconProps{
Size: "18",
Class: "opacity-75 hover:opacity-100",
})
</button>
}
</div>
</div>
}