T templcn/ui
Componentmessage-scroller

Message Scroller

A chat transcript scroller that manages anchored turns, streaming output, history loading, and scroll controls.

Preview

A chat transcript scroller that manages anchored turns, streaming output, history loading, and scroll controls.

AI
Assistant
Welcome to the workspace! How can I assist you today?
ME
How does the chat message scroller handle streaming turns?
Sent
AI
Assistant
MessageScroller tracks the live edge and maintains anchor positions during streamed tokens without stealing the reader's scroll position.
Delivered

Installation

Add this component to your project using the CLI.

templcn add message-scroller

Usage

1. Import the component in your Go or templ file:

import "your-module/ui"

2. Use the component in your template:

@ui.MessageScrollerProvider(ui.DOMProps{}) {
  @ui.MessageScroller(ui.MessageScrollerProps{DOMProps: ui.DOMProps{Class: "h-96 w-full max-w-lg border rounded-xl"}}) {
    @ui.MessageScrollerViewport(ui.DOMProps{}) {
      @ui.MessageScrollerContent(ui.DOMProps{}) {
        @ui.MessageScrollerItem(ui.MessageScrollerItemProps{ScrollAnchor: false}) {
          @ui.Message(ui.MessageProps{Align: "start"}) {
            @ui.MessageAvatar(ui.DOMProps{}) {
              @ui.Avatar(ui.AvatarProps{Class: "size-8"}) {
                @ui.AvatarFallback(ui.DOMProps{}) { AI }
              }
            }
            @ui.MessageContent(ui.DOMProps{}) {
              @ui.MessageHeader(ui.DOMProps{}) { Assistant }
              @ui.Bubble(ui.BubbleProps{Variant: "muted"}) {
                @ui.BubbleContent(ui.DOMProps{}) { How can I help you today? }
              }
            }
          }
        }
        @ui.MessageScrollerItem(ui.MessageScrollerItemProps{ScrollAnchor: true}) {
          @ui.Message(ui.MessageProps{Align: "end"}) {
            @ui.MessageAvatar(ui.DOMProps{}) {
              @ui.Avatar(ui.AvatarProps{Class: "size-8"}) {
                @ui.AvatarFallback(ui.DOMProps{}) { ME }
              }
            }
            @ui.MessageContent(ui.DOMProps{}) {
              @ui.Bubble(ui.BubbleProps{Variant: "default"}) {
                @ui.BubbleContent(ui.DOMProps{}) { Can you explain how MessageScroller handles anchored turns? }
              }
              @ui.MessageFooter(ui.DOMProps{}) { Sent }
            }
          }
        }
      }
    }
    @ui.MessageScrollerButton(ui.MessageScrollerButtonProps{Direction: "end"})
  }
}

Composition

Use these parts together to build the component.

MessageScrollerProvider
├── MessageScroller
├── MessageScrollerViewport
├── MessageScrollerContent
├── MessageScrollerItem
├── Message
├── MessageAvatar
├── Avatar
├── AvatarFallback
├── MessageContent
├── MessageHeader
├── Bubble
├── BubbleContent
├── MessageFooter
├── MessageScrollerButton

Examples

Named examples and common variations.

Default

Scrollable chat window with anchored message rows and floating jump button.

AI
Assistant
Welcome to the workspace! How can I assist you today?
ME
How does the chat message scroller handle streaming turns?
Sent
AI
Assistant
MessageScroller tracks the live edge and maintains anchor positions during streamed tokens without stealing the reader's scroll position.
Delivered
API Reference
PropTypeDefaultDescription
Directionstring"end""start" or "end" for the scroll button.
ScrollAnchorboolfalseAnchors the viewport position to this item during streaming.
View source
package ui

import (
	"context"
	"github.com/a-h/templ"
	"io"
)

type MessageScrollerProps struct{ DOMProps }
type MessageScrollerItemProps struct {
	DOMProps
	ScrollAnchor bool
}
type MessageScrollerButtonProps struct {
	DOMProps
	Direction string
	Variant   ButtonVariant
	Size      ButtonSize
}

func MessageScrollerProvider(props DOMProps) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		return renderElement(ctx, w, "div", attrsFromDOMProps(props, "message-scroller-provider", "contents"), templ.GetChildren(ctx))
	})
}
func MessageScroller(props MessageScrollerProps) templ.Component {
	return messageScrollerBlock("message-scroller", "group/message-scroller relative flex size-full min-h-0 flex-col overflow-hidden", props.DOMProps)
}
func MessageScrollerViewport(props DOMProps) templ.Component {
	return messageScrollerBlock("message-scroller-viewport", "size-full min-h-0 min-w-0 overflow-y-auto overscroll-contain", props)
}
func MessageScrollerContent(props DOMProps) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		attrs := attrsFromDOMProps(props, "message-scroller-content", "flex h-max min-h-full flex-col gap-8")
		if _, ok := attrs["role"]; !ok {
			attrs["role"] = "log"
		}
		return renderElement(ctx, w, "div", attrs, templ.GetChildren(ctx))
	})
}
func MessageScrollerItem(props MessageScrollerItemProps) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		attrs := attrsFromDOMProps(props.DOMProps, "message-scroller-item", "min-w-0 shrink-0")
		if props.ScrollAnchor {
			attrs["data-scroll-anchor"] = true
		}
		return renderElement(ctx, w, "div", attrs, templ.GetChildren(ctx))
	})
}
func MessageScrollerButton(props MessageScrollerButtonProps) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		direction, variant, size := props.Direction, props.Variant, props.Size
		if direction == "" {
			direction = "end"
		}
		if variant == "" {
			variant = ButtonVariantSecondary
		}
		if size == "" {
			size = ButtonSizeIconSM
		}
		attrs := attrsFromDOMProps(props.DOMProps, "message-scroller-button", "absolute left-1/2 -translate-x-1/2 bottom-4 z-20 inline-flex size-8 items-center justify-center rounded-full border border-border bg-background text-foreground shadow-md transition-all hover:bg-muted cursor-pointer")
		attrs["data-direction"], attrs["data-variant"], attrs["data-size"] = direction, variant, size
		attrs["type"] = "button"
		children := templ.GetChildren(ctx)
		if children == nil {
			children = templ.ComponentFunc(func(_ context.Context, out io.Writer) error {
				arrow := "↓"
				if direction == "start" {
					arrow = "↑"
				}
				_, err := io.WriteString(out, `<span aria-hidden="true">`+arrow+`</span><span class="sr-only">Scroll to `+map[bool]string{true: "end", false: "start"}[direction == "end"]+`</span>`)
				return err
			})
		}
		return renderElement(ctx, w, "button", attrs, children)
	})
}
func messageScrollerBlock(slot, className string, props DOMProps) templ.Component {
	return templ.ComponentFunc(func(ctx context.Context, w io.Writer) error {
		return renderElement(ctx, w, "div", attrsFromDOMProps(props, slot, className), templ.GetChildren(ctx))
	})
}