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.
@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"})
}
}Installation
Add this component to your project using the CLI.
templcn add message-scrollerUsage
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
├── MessageScrollerButtonExamples
Named examples and common variations.
Default
Scrollable chat window with anchored message rows and floating jump button.
@ui.MessageScrollerProvider(ui.DOMProps{}) {
@ui.MessageScroller(ui.MessageScrollerProps{DOMProps: ui.DOMProps{Class: "h-80 w-full max-w-md border rounded-xl"}}) {
@ui.MessageScrollerViewport(ui.DOMProps{Class: "p-4"}) {
@ui.MessageScrollerContent(ui.DOMProps{Class: "gap-6"}) {
@ui.MessageScrollerItem(ui.MessageScrollerItemProps{}) {
@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{}) { Welcome to the workspace! How can I assist 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{}) { How does the chat message scroller handle streaming turns? }
}
@ui.MessageFooter(ui.DOMProps{}) { Sent }
}
}
}
}
}
@ui.MessageScrollerButton(ui.MessageScrollerButtonProps{Direction: "end"})
}
}API Reference
| Prop | Type | Default | Description |
|---|---|---|---|
| Direction | string | "end" | "start" or "end" for the scroll button. |
| ScrollAnchor | bool | false | Anchors 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))
})
}