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 viacaret-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.