Lists and Scrolling
July 10, 2026 · View on GitHub
For displaying collections of things, and for making content areas scrollable.
List — Vertical Scrollable List
The standard list. Only draws the items currently visible on screen, so 10,000 items is fine.
type UI struct {
scroll proton.Scrollable
}
items := []string{"Apples", "Bananas", "Cherries", "Durian (why)"}
proton.List(ctx, &u.scroll, len(items), func(ctx proton.Context, i int) {
proton.Label(ctx, items[i])
})
The callback receives the index i. Draw whatever you want for each row.
proton.List(ctx proton.Context, state *proton.Scrollable, length int, draw func(proton.Context, int))
Declare one proton.Scrollable per list. It tracks the scroll position.
Don't share one between two lists — they'll fight over scroll position and
both lose.
HList — Horizontal Scrollable List
Same as List but items go left to right.
proton.HList(ctx, &u.hscroll, len(items), func(ctx proton.Context, i int) {
proton.PadH(ctx, 8, func(ctx proton.Context) {
proton.Label(ctx, items[i])
})
})
proton.HList(ctx proton.Context, state *proton.Scrollable, length int, draw func(proton.Context, int))
Scroll — Scrollable Content Area
For arbitrary content that might overflow, not indexed items. The content function can call as many widgets as it wants.
type UI struct {
scroll proton.Scrollable
}
proton.Scroll(ctx, &u.scroll, func(ctx proton.Context) {
proton.H5(ctx, "A very long page")
proton.Gap(ctx, 8)
proton.Label(ctx, "Paragraph one...")
proton.Gap(ctx, 8)
proton.Label(ctx, "Paragraph two...")
proton.Gap(ctx, 8)
// as many widgets as you need
})
proton.Scroll(ctx proton.Context, state *proton.Scrollable, content func(proton.Context))
Use List when you have indexed data. Use Scroll for a page of mixed content.
TextView — Read-Only Scrollable Text
Displays a large block of text in a scrollable, monospace view. Good for file contents, help text, previewing code.
type UI struct {
scroll proton.Scrollable
}
proton.TextView(ctx, &u.scroll, longText)
proton.TextView(ctx proton.Context, state *proton.Scrollable, text string)
The text is split on newlines and each line is a virtual list item, so it handles very long documents without issue.
LogView — Auto-Scrolling Log Output
Like TextView but auto-scrolls to the bottom whenever new content is added. Color-codes common log prefixes automatically.
type UI struct {
logScroll proton.Scrollable
logText string
}
// append to logText from anywhere
u.logText += fmt.Sprintf("[OK] Step completed at %s\n", time.Now().Format("15:04:05"))
// draw it — auto-scrolls to the latest line
proton.LogView(ctx, &u.logScroll, u.logText)
proton.LogView(ctx proton.Context, state *proton.Scrollable, text string)
Color coding happens automatically based on line prefix:
| Prefix | Color |
|---|---|
[OK], DONE, SUCCESS | Green |
[WARN], WARN | Yellow |
[ERROR], ERROR | Red |
| Anything else | Muted |
Making List Rows Look Good
A bare proton.Label in a list row works but doesn't look great. Add some
padding and structure.
Padded rows
proton.List(ctx, &u.scroll, len(items), func(ctx proton.Context, i int) {
proton.PadV(ctx, 8, func(ctx proton.Context) {
proton.PadH(ctx, 12, func(ctx proton.Context) {
proton.Label(ctx, items[i].Name)
proton.Gap(ctx, 2)
proton.Muted(ctx, items[i].Description)
})
})
proton.Divider(ctx)
})
Two columns of text
type Contact struct {
Name string
Email string
}
proton.List(ctx, &u.scroll, len(contacts), func(ctx proton.Context, i int) {
c := contacts[i]
proton.PadV(ctx, 10, func(ctx proton.Context) {
proton.PadH(ctx, 12, func(ctx proton.Context) {
proton.Label(ctx, c.Name)
proton.Gap(ctx, 3)
proton.Muted(ctx, c.Email)
})
})
proton.Divider(ctx)
})
Clickable rows with hover highlight
type UI struct {
rows [256]proton.Clickable
selected int
scroll proton.Scrollable
}
proton.List(ctx, &u.scroll, len(items), func(ctx proton.Context, i int) {
bg := proton.RGB(0x2e3440)
hov := proton.RGB(0x3b4252)
if u.selected == i {
bg = proton.RGB(0x4c566a)
hov = bg
}
proton.PadV(ctx, 2, func(ctx proton.Context) {
if proton.HoverCard(ctx, &u.rows[i], bg, hov, 6, func(ctx proton.Context) {
proton.PadV(ctx, 10, func(ctx proton.Context) {
proton.PadH(ctx, 12, func(ctx proton.Context) {
proton.Label(ctx, items[i].Name)
proton.Gap(ctx, 2)
proton.Muted(ctx, items[i].Sub)
})
})
}) {
u.selected = i
}
})
})
List inside a Card
proton.Card(ctx, proton.RGB(0x1e1e2e), 10, 0, func(ctx proton.Context) {
proton.List(ctx, &u.scroll, len(items), func(ctx proton.Context, i int) {
proton.PadV(ctx, 8, func(ctx proton.Context) {
proton.PadH(ctx, 12, func(ctx proton.Context) {
proton.Label(ctx, items[i])
})
})
if i < len(items)-1 {
proton.Divider(ctx)
}
})
})
Performance
List and HList use virtual rendering — only visible items get their
draw function called. A slice of 50,000 items scrolls at 60fps without
breaking a sweat.
Scroll renders everything in the content function every frame. Use it for
pages with a reasonable number of widgets, not for huge dynamic datasets.