diff --git a/flash-messages/README.md b/flash-messages/README.md
new file mode 100644
index 0000000..14c0966
--- /dev/null
+++ b/flash-messages/README.md
@@ -0,0 +1,87 @@
+# Flash Messages Example
+
+This example demonstrates flash messages in LiveTemplate - page-level notifications that show once and clear after each action.
+
+## Running
+
+```bash
+cd examples/flash-messages
+go run .
+```
+
+Then open http://localhost:8080
+
+## Flash Message Types
+
+| Type | Use Case | Style |
+|------|----------|-------|
+| `success` | Operation completed | Green |
+| `error` | Something went wrong | Red |
+| `warning` | Caution/duplicate | Yellow |
+| `info` | Informational | Blue |
+
+## Setting Flash Messages (Controller)
+
+```go
+func (c *Controller) MyAction(state State, ctx *livetemplate.Context) (State, error) {
+ // Success notification
+ ctx.SetFlash("success", "Item added successfully!")
+
+ // Error notification
+ ctx.SetFlash("error", "Failed to save changes")
+
+ // Warning notification
+ ctx.SetFlash("warning", "Item already exists")
+
+ // Info notification
+ ctx.SetFlash("info", "Processing complete")
+
+ return state, nil
+}
+```
+
+## Reading Flash Messages (Template)
+
+```html
+
+{{if .lvt.HasAnyFlash}}
+
+
+
+ {{if .lvt.HasFlash "success"}}
+
{{.lvt.Flash "success"}}
+ {{end}}
+
+ {{if .lvt.HasFlash "error"}}
+
{{.lvt.Flash "error"}}
+ {{end}}
+
+
+{{end}}
+```
+
+## Flash vs Field Errors
+
+| Aspect | Flash Messages | Field Errors |
+|--------|----------------|--------------|
+| **Purpose** | Page-level notifications | Form field validation |
+| **Affects Success** | No | Yes |
+| **Template Access** | `.lvt.Flash "key"` | `.lvt.Error "field"` |
+| **Lifecycle** | Cleared after render | Cleared on next action |
+| **Example** | "Changes saved!" | "Email is required" |
+
+## Key Behaviors
+
+1. **Show Once**: Flash messages are cleared after each action response
+2. **Per-Connection**: Not shared across browser tabs
+3. **No Persistence**: Don't survive page refresh or WebSocket reconnects
+4. **Don't Block Success**: Unlike field errors, flash messages don't set `Success: false`
+
+## Available Template Helpers
+
+| Helper | Description |
+|--------|-------------|
+| `.lvt.Flash "key"` | Get flash message for key |
+| `.lvt.HasFlash "key"` | Check if flash exists for key |
+| `.lvt.HasAnyFlash` | Check if any flash messages exist |
+| `.lvt.AllFlash` | Get all flash messages as map |
diff --git a/flash-messages/flash.tmpl b/flash-messages/flash.tmpl
new file mode 100644
index 0000000..3051fb6
--- /dev/null
+++ b/flash-messages/flash.tmpl
@@ -0,0 +1,220 @@
+
+
+
+ {{.Title}}
+
+
+
+
+
+ {{.Title}}
+
+
+ {{if .lvt.HasAnyFlash}}
+
+ {{if .lvt.HasFlash "success"}}
+
{{.lvt.Flash "success"}}
+ {{end}}
+ {{if .lvt.HasFlash "error"}}
+
{{.lvt.Flash "error"}}
+ {{end}}
+ {{if .lvt.HasFlash "warning"}}
+
{{.lvt.Flash "warning"}}
+ {{end}}
+ {{if .lvt.HasFlash "info"}}
+
{{.lvt.Flash "info"}}
+ {{end}}
+
+ {{end}}
+
+
+
+
+
+
+
+
+
+
+
+
+
Items ({{.ItemCount}})
+ {{if .Items}}
+ {{range .Items}}
+
+ {{.}}
+
+
+ {{end}}
+ {{else}}
+
No items. Add some above!
+ {{end}}
+
+
+
+
+
About Flash Messages:
+
+ - Flash messages show once and clear after each action
+ - They don't affect
ResponseMetadata.Success
+ - Types:
success, error, warning, info
+ - Set via:
ctx.SetFlash("success", "message")
+ - Read via:
.lvt.Flash "success", .lvt.HasFlash "success"
+
+
+
+ {{if .lvt.DevMode}}
+
+ {{else}}
+
+ {{end}}
+
+
diff --git a/flash-messages/flash_test.go b/flash-messages/flash_test.go
new file mode 100644
index 0000000..57baadc
--- /dev/null
+++ b/flash-messages/flash_test.go
@@ -0,0 +1,278 @@
+//go:build http
+
+package main
+
+import (
+ "io"
+ "net/http"
+ "net/http/httptest"
+ "net/url"
+ "strings"
+ "testing"
+
+ "github.com/livetemplate/livetemplate"
+)
+
+// TestFlash_ShowsInTemplate tests that flash messages appear in rendered HTML.
+func TestFlash_ShowsInTemplate(t *testing.T) {
+ // Create controller and state
+ controller := &FlashController{}
+ initialState := &FlashState{}
+
+ // Create template
+ tmpl := livetemplate.Must(livetemplate.New("flash",
+ livetemplate.WithDevMode(true),
+ ))
+
+ // Create test server
+ handler := tmpl.Handle(controller, livetemplate.AsState(initialState))
+ server := httptest.NewServer(handler)
+ defer server.Close()
+
+ // Create client with cookie jar for session persistence
+ jar, _ := newCookieJar()
+ client := &http.Client{Jar: jar}
+
+ // 1. First GET to establish session and mount state
+ resp, err := client.Get(server.URL + "/")
+ if err != nil {
+ t.Fatalf("Initial GET failed: %v", err)
+ }
+ resp.Body.Close()
+
+ // 2. POST to add item (should set success flash)
+ form := url.Values{}
+ form.Set("action", "add_item")
+ form.Set("item", "Test Item")
+
+ resp, err = client.PostForm(server.URL+"/", form)
+ if err != nil {
+ t.Fatalf("POST add_item failed: %v", err)
+ }
+ defer resp.Body.Close()
+
+ // Read response body
+ body := readBody(t, resp)
+
+ // Flash message should appear in response
+ if !strings.Contains(body, "Added item: Test Item") {
+ t.Errorf("Expected success flash 'Added item: Test Item' in response, got:\n%s", body)
+ }
+
+ // Should have flash-success class
+ if !strings.Contains(body, "flash-success") {
+ t.Errorf("Expected flash-success class in response")
+ }
+}
+
+// TestFlash_ClearsAfterAction tests that flash is cleared on subsequent action.
+func TestFlash_ClearsAfterAction(t *testing.T) {
+ controller := &FlashController{}
+ initialState := &FlashState{}
+
+ tmpl := livetemplate.Must(livetemplate.New("flash",
+ livetemplate.WithDevMode(true),
+ ))
+
+ handler := tmpl.Handle(controller, livetemplate.AsState(initialState))
+ server := httptest.NewServer(handler)
+ defer server.Close()
+
+ jar, _ := newCookieJar()
+ client := &http.Client{Jar: jar}
+
+ // 1. GET to establish session
+ resp, err := client.Get(server.URL + "/")
+ if err != nil {
+ t.Fatalf("Initial GET failed: %v", err)
+ }
+ resp.Body.Close()
+
+ // 2. POST to add first item (sets flash)
+ form := url.Values{}
+ form.Set("action", "add_item")
+ form.Set("item", "First Item")
+ resp, err = client.PostForm(server.URL+"/", form)
+ if err != nil {
+ t.Fatalf("First POST failed: %v", err)
+ }
+ body1 := readBody(t, resp)
+ resp.Body.Close()
+
+ if !strings.Contains(body1, "Added item: First Item") {
+ t.Error("First action should show flash")
+ }
+
+ // 3. POST to add second item (new flash replaces old)
+ form = url.Values{}
+ form.Set("action", "add_item")
+ form.Set("item", "Second Item")
+ resp, err = client.PostForm(server.URL+"/", form)
+ if err != nil {
+ t.Fatalf("Second POST failed: %v", err)
+ }
+ body2 := readBody(t, resp)
+ resp.Body.Close()
+
+ // Old flash should be gone
+ if strings.Contains(body2, "Added item: First Item") {
+ t.Error("Old flash should be cleared after new action")
+ }
+
+ // New flash should be present
+ if !strings.Contains(body2, "Added item: Second Item") {
+ t.Error("New flash should appear")
+ }
+}
+
+// TestFlash_DifferentTypes tests success, error, warning, and info flash types.
+func TestFlash_DifferentTypes(t *testing.T) {
+ controller := &FlashController{}
+ initialState := &FlashState{}
+
+ tmpl := livetemplate.Must(livetemplate.New("flash",
+ livetemplate.WithDevMode(true),
+ ))
+
+ handler := tmpl.Handle(controller, livetemplate.AsState(initialState))
+ server := httptest.NewServer(handler)
+ defer server.Close()
+
+ jar, _ := newCookieJar()
+ client := &http.Client{Jar: jar}
+
+ // Initial GET
+ resp, _ := client.Get(server.URL + "/")
+ resp.Body.Close()
+
+ tests := []struct {
+ name string
+ action string
+ item string
+ wantClass string
+ wantText string
+ }{
+ {
+ name: "success flash",
+ action: "add_item",
+ item: "New Item",
+ wantClass: "flash-success",
+ wantText: "Added item: New Item",
+ },
+ {
+ name: "warning flash (duplicate)",
+ action: "add_item",
+ item: "New Item", // Same item = duplicate
+ wantClass: "flash-warning",
+ wantText: "Item already exists",
+ },
+ {
+ name: "info flash",
+ action: "remove_item",
+ item: "New Item",
+ wantClass: "flash-info",
+ wantText: "Removed item: New Item",
+ },
+ {
+ name: "error flash",
+ action: "simulate_error",
+ item: "",
+ wantClass: "flash-error",
+ wantText: "Something went wrong",
+ },
+ }
+
+ for _, tt := range tests {
+ t.Run(tt.name, func(t *testing.T) {
+ form := url.Values{}
+ form.Set("action", tt.action)
+ if tt.item != "" {
+ form.Set("item", tt.item)
+ }
+
+ resp, err := client.PostForm(server.URL+"/", form)
+ if err != nil {
+ t.Fatalf("POST failed: %v", err)
+ }
+ body := readBody(t, resp)
+ resp.Body.Close()
+
+ if !strings.Contains(body, tt.wantClass) {
+ t.Errorf("Expected %s in response", tt.wantClass)
+ }
+ if !strings.Contains(body, tt.wantText) {
+ t.Errorf("Expected '%s' in response, got:\n%s", tt.wantText, body)
+ }
+ })
+ }
+}
+
+// TestFlash_FieldErrorsStillWork tests that field errors work alongside flash.
+func TestFlash_FieldErrorsStillWork(t *testing.T) {
+ controller := &FlashController{}
+ initialState := &FlashState{}
+
+ tmpl := livetemplate.Must(livetemplate.New("flash",
+ livetemplate.WithDevMode(true),
+ ))
+
+ handler := tmpl.Handle(controller, livetemplate.AsState(initialState))
+ server := httptest.NewServer(handler)
+ defer server.Close()
+
+ jar, _ := newCookieJar()
+ client := &http.Client{Jar: jar}
+
+ // Initial GET
+ resp, _ := client.Get(server.URL + "/")
+ resp.Body.Close()
+
+ // POST with empty item (triggers field error, not flash)
+ form := url.Values{}
+ form.Set("action", "add_item")
+ form.Set("item", "") // Empty = validation error
+
+ resp, err := client.PostForm(server.URL+"/", form)
+ if err != nil {
+ t.Fatalf("POST failed: %v", err)
+ }
+ body := readBody(t, resp)
+ resp.Body.Close()
+
+ // Should show field error, not flash
+ if !strings.Contains(body, "Item name is required") {
+ t.Error("Expected field error 'Item name is required'")
+ }
+ if !strings.Contains(body, "field-error") {
+ t.Error("Expected field-error class")
+ }
+}
+
+// Helper functions
+
+func newCookieJar() (http.CookieJar, error) {
+ // Simple cookie jar implementation
+ return &simpleCookieJar{cookies: make(map[string][]*http.Cookie)}, nil
+}
+
+type simpleCookieJar struct {
+ cookies map[string][]*http.Cookie
+}
+
+func (j *simpleCookieJar) SetCookies(u *url.URL, cookies []*http.Cookie) {
+ j.cookies[u.Host] = cookies
+}
+
+func (j *simpleCookieJar) Cookies(u *url.URL) []*http.Cookie {
+ return j.cookies[u.Host]
+}
+
+func readBody(t *testing.T, resp *http.Response) string {
+ t.Helper()
+ buf := new(strings.Builder)
+ _, err := io.Copy(buf, resp.Body)
+ if err != nil {
+ t.Fatalf("Failed to read response body: %v", err)
+ }
+ return buf.String()
+}
diff --git a/flash-messages/main.go b/flash-messages/main.go
new file mode 100644
index 0000000..e472657
--- /dev/null
+++ b/flash-messages/main.go
@@ -0,0 +1,166 @@
+package main
+
+import (
+ "log"
+ "net/http"
+ "os"
+ "time"
+
+ "github.com/livetemplate/livetemplate"
+ e2etest "github.com/livetemplate/lvt/testing"
+)
+
+// FlashController demonstrates flash messages for page-level notifications.
+//
+// Flash messages are per-connection and show once (cleared after render).
+// They don't affect ResponseMetadata.Success (unlike field validation errors).
+//
+// Common flash keys: "success", "error", "info", "warning"
+type FlashController struct{}
+
+// FlashState holds the demo data.
+type FlashState struct {
+ Title string `json:"title"`
+ Items []string `json:"items"`
+ ItemCount int `json:"item_count"`
+}
+
+// AddItem handles the "add_item" action - demonstrates success flash.
+func (c *FlashController) AddItem(state FlashState, ctx *livetemplate.Context) (FlashState, error) {
+ item := ctx.GetString("item")
+
+ if item == "" {
+ // Field validation error (affects Success: false)
+ return state, livetemplate.FieldError{Field: "item", Message: "Item name is required"}
+ }
+
+ // Check for duplicates
+ for _, existing := range state.Items {
+ if existing == item {
+ // Use flash for page-level warning (doesn't affect Success)
+ ctx.SetFlash("warning", "Item already exists: "+item)
+ return state, nil
+ }
+ }
+
+ state.Items = append(state.Items, item)
+ state.ItemCount = len(state.Items)
+
+ // Success flash message
+ ctx.SetFlash("success", "Added item: "+item)
+
+ return state, nil
+}
+
+// RemoveItem handles the "remove_item" action - demonstrates flash on removal.
+func (c *FlashController) RemoveItem(state FlashState, ctx *livetemplate.Context) (FlashState, error) {
+ item := ctx.GetString("item")
+
+ // Find and remove
+ found := false
+ newItems := make([]string, 0, len(state.Items))
+ for _, existing := range state.Items {
+ if existing == item {
+ found = true
+ } else {
+ newItems = append(newItems, existing)
+ }
+ }
+
+ if !found {
+ ctx.SetFlash("error", "Item not found: "+item)
+ return state, nil
+ }
+
+ state.Items = newItems
+ state.ItemCount = len(state.Items)
+
+ ctx.SetFlash("info", "Removed item: "+item)
+ return state, nil
+}
+
+// ClearItems handles the "clear_items" action - demonstrates warning flash.
+func (c *FlashController) ClearItems(state FlashState, ctx *livetemplate.Context) (FlashState, error) {
+ if len(state.Items) == 0 {
+ ctx.SetFlash("warning", "No items to clear")
+ return state, nil
+ }
+
+ count := len(state.Items)
+ state.Items = []string{}
+ state.ItemCount = 0
+
+ ctx.SetFlash("success", "Cleared all items ("+string(rune('0'+count))+" removed)")
+ return state, nil
+}
+
+// SimulateError handles the "simulate_error" action - demonstrates error flash.
+func (c *FlashController) SimulateError(state FlashState, ctx *livetemplate.Context) (FlashState, error) {
+ // Simulate a server error that should be shown as flash
+ ctx.SetFlash("error", "Something went wrong! Please try again.")
+ return state, nil
+}
+
+// Mount initializes state with sample data.
+func (c *FlashController) Mount(state FlashState, ctx *livetemplate.Context) (FlashState, error) {
+ state.Title = "Flash Messages Demo"
+ state.Items = []string{"Apple", "Banana", "Cherry"}
+ state.ItemCount = len(state.Items)
+ return state, nil
+}
+
+func main() {
+ log.Println("LiveTemplate Flash Messages Example starting...")
+
+ // Load configuration from environment variables
+ envConfig, err := livetemplate.LoadEnvConfig()
+ if err != nil {
+ log.Fatalf("Failed to load configuration: %v", err)
+ }
+
+ // Validate configuration
+ if err := envConfig.Validate(); err != nil {
+ log.Fatalf("Invalid configuration: %v", err)
+ }
+
+ // Create controller (singleton)
+ controller := &FlashController{}
+
+ // Create initial state (pure data, cloned per session)
+ initialState := &FlashState{}
+
+ // Create template with environment-based configuration
+ tmpl := livetemplate.Must(livetemplate.New("flash", envConfig.ToOptions()...))
+
+ // Mount handler
+ http.Handle("/", tmpl.Handle(controller, livetemplate.AsState(initialState)))
+
+ // Health check endpoint for testing
+ http.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) {
+ w.Header().Set("Content-Type", "application/json")
+ w.Write([]byte(`{"status":"ok","timestamp":"` + time.Now().Format(time.RFC3339) + `"}`))
+ })
+
+ // Serve client library (development only - use CDN in production)
+ http.HandleFunc("/livetemplate-client.js", e2etest.ServeClientLibrary)
+
+ port := os.Getenv("PORT")
+ if port == "" {
+ port = "8080"
+ }
+
+ log.Printf("Server starting on http://localhost:%s", port)
+ log.Println("")
+ log.Println("Flash Message Types:")
+ log.Println(" - success: Green notification (e.g., 'Item added')")
+ log.Println(" - error: Red notification (e.g., 'Something went wrong')")
+ log.Println(" - warning: Yellow notification (e.g., 'Item exists')")
+ log.Println(" - info: Blue notification (e.g., 'Item removed')")
+ log.Println("")
+ log.Println("Note: Flash messages show once and are cleared after each action.")
+ log.Println("")
+
+ if err := http.ListenAndServe(":"+port, nil); err != nil {
+ log.Fatalf("Server failed to start: %v", err)
+ }
+}
diff --git a/go.mod b/go.mod
index 955cb1b..27543b4 100644
--- a/go.mod
+++ b/go.mod
@@ -8,7 +8,7 @@ require (
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4
- github.com/livetemplate/livetemplate v0.7.5
+ github.com/livetemplate/livetemplate v0.7.7
github.com/livetemplate/lvt v0.0.0-20251130141940-9b94cde94e9d
modernc.org/sqlite v1.39.1
)
diff --git a/go.sum b/go.sum
index a5f4f07..09beb13 100644
--- a/go.sum
+++ b/go.sum
@@ -122,8 +122,8 @@ github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4 h1:wLfVleSSlcv4NPg5KN8pul0Rz9ub1CtI8OAcPlyBYlw=
github.com/livetemplate/components v0.0.0-20251224004709-1f8c1de230b4/go.mod h1:+C2iGZfdgjc6y6MsaDHBWzWGIbBHna4l+ygFYJfuyUo=
-github.com/livetemplate/livetemplate v0.7.5 h1:9esBqvhAG4DK/9lxuSEfFg8uyzc8C3bHlu8Aex6YSBk=
-github.com/livetemplate/livetemplate v0.7.5/go.mod h1:mTI76skBGEx4jD9pO52L9xBY4/ZDW4muAKWwXnupvtc=
+github.com/livetemplate/livetemplate v0.7.7 h1:vHwV5qjlwelgJCiGwQe88u7J3XheADdW86BEndezQtQ=
+github.com/livetemplate/livetemplate v0.7.7/go.mod h1:mTI76skBGEx4jD9pO52L9xBY4/ZDW4muAKWwXnupvtc=
github.com/livetemplate/lvt v0.0.0-20251130141940-9b94cde94e9d h1:KKaaDPTlSCL/BEz5B+xowvXgOpl0kLpAdWZLIXL/2a0=
github.com/livetemplate/lvt v0.0.0-20251130141940-9b94cde94e9d/go.mod h1:iwP3NZgs3EGXd6mTUAK3j4UENr7qhuUmAME44LoTExE=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=