Skip to content

feat(overlay): ticker scroll mode with a speed control - #36

Merged
nathanialhenniges merged 4 commits into
mainfrom
claude/scrolling-theme-speed-control-772359
Aug 21, 2026
Merged

feat(overlay): ticker scroll mode with a speed control#36
nathanialhenniges merged 4 commits into
mainfrom
claude/scrolling-theme-speed-control-772359

Conversation

@nathanialhenniges

@nathanialhenniges nathanialhenniges commented Aug 21, 2026

Copy link
Copy Markdown
Member

What

Adds a horizontal ticker as an option for the overlay: chat lays out as a single lane sliding across the source instead of a column stacking up the side. Two new URL params:

Param Values Default
scroll off, ticker off
scrollspeed 1 to 5, whole steps 1

It is an orthogonal param rather than a theme, so it composes with all 31 themes. ?theme=neon&scroll=ticker&scrollspeed=3 is a neon ticker.

align stops being a dead control in this mode and picks the direction: right runs right to left the way a news ticker does, left runs the way you read. It is the same keyframe played in reverse.

1x is deliberately a crawl (50 px/s, about 45 seconds to cross a 1920-wide source) and every step climbs from there.

The honest limitation

A lane has a throughput ceiling that a column does not. One message occupies it for the whole time it takes to cross, so it fits roughly one message every eight seconds at 1x. A busy channel sends 10 to 40 a second.

Anything that cannot start within six seconds is dropped rather than queued, so the lane shows a sample of recent chat instead of a backlog running minutes behind. This is stated in /docs under scroll, because it is the first thing a streamer would otherwise discover live.

Dropping is not optional. Parking those rows instead looks harmless and is not: a run lasts 10-20s while a busy channel refills ?max in about three, so unscheduled rows push the in-flight ones out of the list and the lane goes permanently blank. Only messages actually flying hold a slot. Caught in the browser against xqc, not in review.

Notes on the implementation

  • No setTimeout anywhere. The whole schedule rides animation-delay, since OBS throttles JS timers while a source is hidden and CSS animation clocks keep running.
  • The clock is document.timeline.currentTime, not performance.now(). A hidden document freezes the timeline, so the cursor freezes with the animations instead of racing ahead. Its reading is legitimately 0 while hidden, so the fallback cannot be a truthiness check.
  • --hb-ticker-w is written per row, not on the lane. A shared variable handed a later row's re-measurement to every message already in flight while their durations stayed, which reads as a speed jump mid-run.
  • ChatMessageRow stays memoized: the scheduling lives in a []-dep callback in MessageList and the row hands back its own id.
  • Emote, cheermote and badge art measures 0 before it loads, so ticker mode reserves a square min-width on each. Without it a four-badge row under-measures by ~74px and collides with its neighbour.
  • Scheduling math is a pure module, lib/overlay/ticker.ts, with its own tests.

Also in here

Every range slider in the configurator is now a row of named stops, which is the UI the rest of the builder already used. The stops grew to cover what the sliders reached: text size gains 2XL and 3XL to touch the schema's 300 ceiling, emote size gains the two missing half steps, refresh becomes Off through 24 h. Dropping htmlFor also turns each into a real fieldset/legend rather than a label pointing at one input.

Verified

bun test (241 pass), bun run check-types, bun run check all green.

In the browser against xqc:

  • Lane pins to a single strip and does not stretch; row geometry is exact (a row at 50% sits at lane - (lane + width) / 2).
  • travel / duration is exactly 250 px/s at 5x across every row, so speed is genuinely constant.
  • align=right reports normal and travels 1271 -> 478 -> -316; default align=left reports reverse and travels -199 -> 536 -> 1271.
  • bg=bubble rows are content-width, not lane-width. bg=panel is a strip. layout=stacked renders identically to inline. theme=nobox no longer fades the glyph tops. emotescale=4 grows the lane from 37px to 114px without clipping.
  • Builder round-trips: no input[type=range] left, and the buttons produce ?scroll=ticker&scrollspeed=4, size=300, refresh=360.

Not verified on real hardware: the OBS hide/show behaviour, and the reduced-motion fallback (it reuses the useReducedMotion that already gates staticMedia).

🤖 Generated with Claude Code

Summary by CodeRabbit

  • New Features

    • Added ticker scrolling for overlay messages, with configurable direction and speeds from 1x to 5x.
    • Added scroll mode and speed controls to the configuration builder and live preview.
    • Added URL parameters for configuring ticker behavior.
    • Ticker mode reserves media space and supports reduced-motion preferences.
  • Documentation

    • Updated configuration, alignment, URL parameter, and CSS hook documentation.
  • Bug Fixes

    • Invalid scroll settings now safely fall back to supported defaults.

nathanialhenniges and others added 4 commits August 20, 2026 19:48
scroll picks the message layout over time (off, ticker) and scrollspeed
is the ticker lane's multiplier, 1 to 5 in whole steps. Both go through
the usual four-file contract so the Zod schema and the dependency-free
OBS parser cannot drift, with parity cases for the enum, the clamp and
every malformed shape.

lib/overlay/ticker.ts holds the scheduling math on its own: duration
from the row's own width so every message moves at the same speed, and a
shared cursor so the next one does not start on the back of the one
ahead. planTickerRun returns null past TICKER_HORIZON_MS rather than
queueing, because a lane carries far less than a busy channel sends and
an unbounded cursor would run minutes behind real time.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
hb-messages becomes a one-line lane and each row is absolutely
positioned past the clip edge, carried across by the hb-ticker keyframe.
MessageList measures the row in a stable callback ref and writes the
animation shorthand on the node, so ChatMessageRow stays memoized and
never puts animation in its style prop for React to clear.

align picks the direction rather than an edge to hug: right runs right
to left, left is the same run reversed. layout=stacked folds back to
inline in HbRoot instead of a CSS override, which would have outranked
the group rule. Emote, cheermote and badge art reserves a square
min-width, or a row measures short before its art loads and collides
with its neighbour.

The clock is document.timeline.currentTime, not performance.now(): a
hidden document freezes it, so the cursor freezes with the animations
rather than dumping a backlog when OBS shows the source again. A
reduced-motion preference turns the mode off outright.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Motion and Ticker speed sit with the layout controls, and the speed
buttons only appear once the ticker is on.

Every slider in the form is now a row of named stops, so the text size,
emote size and refresh controls read like the rest of the builder. The
stops had to grow to cover the ranges the sliders used to reach: text
size gains 2XL and 3XL to touch the schema's 300 ceiling, emote size
gains the two missing half steps, and refresh becomes Off through 24 h.
Dropping htmlFor also turns each one into a real fieldset and legend
instead of a label pointing at a single input.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
The reference gets scroll and scrollspeed with their own anchors, and
the align entry now says it picks the lane's direction. The honest part
is the ceiling: a lane fits roughly one message every eight seconds at
1x, so on a fast channel the ticker shows a sample of recent chat rather
than all of it, and saying so up front is better than a streamer working
it out live.

CLAUDE.md records the two invariants that look like details and are not:
the document timeline is the clock, and a message past the horizon is
expired rather than parked.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Aug 21, 2026

Copy link
Copy Markdown

Review Change Stack

Caution

Review failed

The pull request is closed.

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro Plus

Run ID: 083dfb93-0b62-411c-b534-4753a86f87fd

📥 Commits

Reviewing files that changed from the base of the PR and between 7796b7f and ead1f2f.

📒 Files selected for processing (23)
  • CLAUDE.md
  • README.md
  • apps/web/src/components/chat/chat-message.tsx
  • apps/web/src/components/chat/chat-overlay.tsx
  • apps/web/src/components/chat/hb-root.tsx
  • apps/web/src/components/chat/message-list.tsx
  • apps/web/src/components/chat/overlay.css
  • apps/web/src/components/landing/config-builder/form-model.ts
  • apps/web/src/components/landing/config-builder/output.tsx
  • apps/web/src/components/landing/config-builder/sections.tsx
  • apps/web/src/components/landing/overlay-preview.tsx
  • apps/web/src/lib/overlay/config.ts
  • apps/web/src/lib/overlay/params.test.ts
  • apps/web/src/lib/overlay/params.ts
  • apps/web/src/lib/overlay/parse-search.test.ts
  • apps/web/src/lib/overlay/parse-search.ts
  • apps/web/src/lib/overlay/ticker.test.ts
  • apps/web/src/lib/overlay/ticker.ts
  • apps/web/src/lib/overlay/url.test.ts
  • apps/web/src/lib/overlay/url.ts
  • apps/web/src/lib/twitch/events.test.ts
  • apps/web/src/overlay-app.tsx
  • apps/web/src/routes/docs.tsx

Walkthrough

The overlay now supports scroll=ticker and scrollspeed=1–5. It validates and serializes these parameters, schedules measured horizontal message animations, supports alignment-based direction, handles reduced motion, adds builder controls, and documents the ticker CSS contract.

Changes

Ticker scrolling

Layer / File(s) Summary
Scroll configuration and URL handling
apps/web/src/lib/overlay/config.ts, apps/web/src/lib/overlay/params.ts, apps/web/src/lib/overlay/parse-search.ts, apps/web/src/lib/overlay/url.ts, apps/web/src/lib/overlay/*test.ts, apps/web/src/lib/twitch/events.test.ts
Adds off and ticker modes, defaults, speed validation from 1 through 5, URL serialization, and parser coverage.
Ticker scheduling and message animation
apps/web/src/lib/overlay/ticker.ts, apps/web/src/lib/overlay/ticker.test.ts, apps/web/src/components/chat/message-list.tsx, apps/web/src/components/chat/chat-message.tsx, apps/web/src/components/chat/overlay.css
Measures lane and row widths, schedules runs within a horizon, applies alignment-based animations, reserves media space, and expires unschedulable rows.
Overlay wiring and motion handling
apps/web/src/components/chat/chat-overlay.tsx, apps/web/src/components/chat/hb-root.tsx, apps/web/src/overlay-app.tsx
Forwards scroll settings, emits data-scroll, suppresses stacked layout styling for ticker mode, and disables ticker mode for reduced motion.
Builder controls and live preview
apps/web/src/components/landing/config-builder/form-model.ts, apps/web/src/components/landing/config-builder/sections.tsx, apps/web/src/components/landing/config-builder/output.tsx, apps/web/src/components/landing/overlay-preview.tsx
Adds scroll settings and presets to the configuration builder and forwards them to the live preview.
Ticker documentation
README.md, CLAUDE.md, apps/web/src/routes/docs.tsx
Documents ticker direction, speed, scheduling, reduced-motion behavior, and CSS hooks.

Estimated code review effort: 4 (Complex) | ~45 minutes

Poem

I’m a rabbit with a ticker to steer,
Messages hop from far to near.
Widths are measured, lanes stay clear,
Speed knobs range from one to five here.
With motion reduced, stillness draws near.

✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch claude/scrolling-theme-speed-control-772359

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@nathanialhenniges
nathanialhenniges merged commit 3d7b9ec into main Aug 21, 2026
2 of 3 checks passed
@nathanialhenniges
nathanialhenniges deleted the claude/scrolling-theme-speed-control-772359 branch August 21, 2026 00:50
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant