Motivka

Fix: Cursor Not Following Text Across Chat Interface

Fixed the blinking underscore cursor across all chat input components so it follows typed text rather than staying fixed to the left, and unified the streaming cursor to use a thin underscore matching the brand style.

2 Phases
6 Tasks
1 Days

The Problem

The blinking underscore cursor is a brand signature element for Motivka -- it mirrors the white bar in the logo. But it was broken across every chat input component in different ways:

  • HeroChatInput and ChatField -- the underscore was a <span> positioned as a flex sibling before the <input> element. As a static flex item, it could never move with the text. It just sat there on the left while text grew to the right.
  • ChatMessage streaming cursor -- styled as a tall block (width: 0.5rem; height: 1em) instead of a thin underscore.
  • ChatInput -- used a > prompt prefix and a native browser caret via caret-color, with no underscore at all.

Three components, three different cursor implementations, none of them correct.

What I Fixed

The fix touched four files across two tracks:

Streaming cursor (ChatMessage): The simplest fix. Changed the .streaming-cursor dimensions from a tall block to a thin underscore style. This is pure CSS -- no structural changes needed.

Input cursors (HeroChatInput, ChatField): These required a text-mirror overlay pattern. You cannot use CSS ::after on <input> elements, so I needed an invisible <span> that mirrors the input's text content and positions the underscore cursor after the last character. The cursor span reads from the input value reactively and positions itself accordingly.

ChatInput unification: Replaced the > prompt and native caret with the same underscore pattern used in the other components. All chat inputs now share the same visual language.

The Tricky Part

The text-mirror approach is necessary because HTML <input> elements do not support pseudo-elements. The mirror span must use the same font properties (family, size, weight, letter-spacing) as the input to ensure pixel-accurate cursor positioning. Any mismatch causes the cursor to drift from the actual text end position. I verified this across different text lengths and with various characters to catch any width calculation issues.

What is Next

The cursor pattern is now consistent but lives in three separate components. If I add more text inputs that need the branded cursor, I will extract a shared CursorInput primitive to avoid duplicating the mirror logic.

Bugs Fixed

Key Decisions

  • Used text-mirror overlay pattern for input cursors — CSS ::after cannot be applied to <input> elements; an invisible span mirroring the text content is the standard workaround
  • Kept cursor logic in individual components rather than extracting a shared primitive — Three components is manageable; extraction warranted only if more cursor inputs are added