Tony GorbProduct Designer & Builder

Chips in a text box: let the browser own the selection

Scenri's prompt mixes words with chips for products, people and places. Every caret, select all and paste bug went away the day I stopped managing the editing myself.

In this story4 sections
  1. One line, and chips the browser cannot enter
  2. Copy gives two answers
  3. The space beside a chip took three models in two days
  4. If you are building a prompt box with chips

In the first week of building Scenri, the prompt box looked right and behaved wrong. A brief reads like a sentence, with chips for a product, a person or a place sitting inline between the words. Press select all and the selection stopped at the edge of the first run of text. Copy across a chip and the clipboard came back short. Click near a chip and the caret went somewhere else.

Each run of text between chips was its own editable box. That made every boundary between them a place where the browser's editing stopped and my code had to take over. I could have patched each bug. The fix was to stop owning the problem.

One line, and chips the browser cannot enter

The composer has been one editable element since Scenri's first commit on 4 August 2026. The code comment on it says the design in two sentences:

The line is a single contenteditable, so selection, select all, copy, cut and paste behave the way the browser already knows how to, across prose and chips alike. Chips are contenteditable=false atoms inside it.

Scenri, apps/studio/src/composer/BriefInput.tsx

A chip is a span with contentEditable = 'false'. To the browser it is one solid thing, like an image in a paragraph: the caret goes before it or after it, a selection includes it or not, and nothing types inside it.

The second rule matters as much. React does not own the line's children. Tokens flow out through onChange when the text changes, and the line is rebuilt from tokens only on an explicit reset: a brief loaded for a remix, the line cleared after sending, a template seeded from Home. Never in response to typing. If the framework repaints the DOM while someone is typing in it, the caret loses its place, so it is never allowed to.

The module that holds the line's rules adds one more, found the hard way in Chromium. The browser anchors its caret to a node and an offset, and ignores a caret you move while it is still handling a click. So edits that change the line's structure rearrange the nodes around the anchor the browser already holds, rather than telling it where to go.

Copy gives two answers

Owning the clipboard turned out to be the one place where taking over was right. Chromium reports an empty selection string when the selection ends on an editable host, so a plain select all and copy would have written nothing.

Scenri writes both flavours itself:

  • text/plain reads each chip as its label. Paste the brief into an email and you get the sentence you saw.
  • text/html wraps the selection in a data-sc-brief marker and carries each chip's token. Paste it back into any Scenri composer and the chips come back as chips.

On paste, markup is never inserted. Only the tokens are read, and the chips are rebuilt from scratch. The wrapper is required on purpose, because any web page can put a chip-shaped attribute into the HTML it places on your clipboard. A picture on the clipboard, like a screenshot, becomes an upload and lands as a reference chip at the caret.

The space beside a chip took three models in two days

Selection was solved by architecture. Spacing was not, and the history shows it: 44 commits touched the composer on 2 and 3 September.

The first model kept one real space character on each side of every chip. It looked right, and it kept breaking. A space typed at a chip's edge made two and doubled the gap. Deleting a chip in Chromium left its two spaces behind as separate text nodes. Crossing a chip with the arrow key took two presses, because the browser treated the chip's space as a character of its own.

The second model gave the chip its own gap as a 2px margin and kept no text beside it at all. It was cleaner, and it was reverted the same day: a phone's caret only exists in text, and on iOS there was nowhere for it to sit.

The third model, since 3 September, keeps both halves. The chip owns its gap through its margin. Where the user has typed nothing beside a chip, the line holds a guard: one zero-width character that gives the caret text to sit in. The guard is never part of the sentence. The reader strips it, the clipboard drops it, and a keystroke into it leaves only what was typed. The line never adds or removes a space; the spaces beside a chip are the user's.

Deleting a chip stayed in Scenri's hands, because the browsers disagree, measured. With the caret in the text flush beside a chip, Chromium removes the chip and WebKit does nothing. With the caret on the line between two chips, Chromium does nothing and WebKit removes both. A caret flush beside a chip takes that chip, from keydown for a hardware key and beforeinput for a phone's keyboard.

Editable element for the whole brief
1
Clipboard flavours: text out, chips in
2
Spacing models tried, 2 and 3 September
3
Composer commits in those two days
44

If you are building a prompt box with chips

  1. Make it one editable element. Chips are contenteditable=false atoms inside it.
  2. Do not let your framework render its children. Read tokens out on input; write them back only on an explicit reset.
  3. Write the clipboard yourself: plain text for the world, your own marked HTML for your app, and rebuild chips from tokens on paste.
  4. Test the caret on a phone before you settle a spacing model. A caret needs text to live in.
  5. Handle chip deletion yourself. The browsers do not agree on it.

Tony Gorb, Product Designer & Builder

Questions about this story, or a design role to talk about? Write to bytonygorb@gmail.com or find me on LinkedIn.

More stories