Skip to content

[lexical-utils] Add a colors option to markSelection / selectionAlwaysOnDisplay (#7492)#8710

Draft
pro-vi wants to merge 1 commit into
facebook:mainfrom
pro-vi:frac-7492-colors
Draft

[lexical-utils] Add a colors option to markSelection / selectionAlwaysOnDisplay (#7492)#8710
pro-vi wants to merge 1 commit into
facebook:mainfrom
pro-vi:frac-7492-colors

Conversation

@pro-vi

@pro-vi pro-vi commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

What

markSelection's defaultOnReposition hardcodes background: 'Highlight', so a consumer that wants a different selection color has to re-derive the whole styler (copying its marginTop / paddingTop / paddingBottom offsets). That is the configurability #7492 asks for.

This adds an optional colors argument ({ background }) to markSelection, selectionAlwaysOnDisplay, and SelectionAlwaysOnDisplayExtension, and exports a createDefaultOnReposition(colors) factory so a consumer passing its own onReposition can reuse the default rect styling with a different color:

markSelection(editor, undefined, {background: 'var(--selection)'});
selectionAlwaysOnDisplay(editor, undefined, {background: 'var(--selection)'});
// or, to keep the default offsets but change the color from a custom onReposition:
const onReposition = createDefaultOnReposition({background: 'var(--selection)'});

background defaults to 'Highlight', so every current call site is byte-identical. Covered by createDefaultOnReposition.test.ts; the existing markSelection test still passes.

The harder half of #7492 (not coded here, asking first)

The reporter's other case is the fake selection going invisible over text that carries its own background, and that one is a paint-order problem, not a color choice. positionNodeOnRange prepends its position: relative wrapper to root.parentElement, so the rects sit behind the glyphs and below the editable. When a span has its own opaque background (inline code, a code block), that background paints over the behind-text rect and the selection disappears for the length of that span. A different background color cannot fix that, the rect is occluded, not mis-colored.

The way to make it visible is to lift the rect above the content (z-index). But a translucent rect lifted over syntax-highlighted code then washes the token colors out. I measured this by canvas-compositing each token color over the rect versus over the code background, in dark mode:

token native (over code bg) over the lifted rect
keyword 3-6:1 1.97:1
string 3-6:1 2.72:1
number 3-6:1 2.16:1
comment 3-6:1 1.96:1

Below ~2:1 selected code is hard to read, so lifting the rect makes the selection visible but the code less legible, a net loss for exactly the code-block case the issue is about.

One correction, since I had this muddled at first: the clean way to recover the contrast is to recolor the selected text to one foreground color via a ::selection rule, but that only works for a focused native selection. markSelection / selectionAlwaysOnDisplay draw the fake selection when the editor is blurred (the real selection is elsewhere), so there is no native ::selection on the editor text to override. That ::selection normalization belongs to the focused selection, which is the separate WebKit drawSelection work in #8709, not here. Recoloring the blurred fake selection's text would mean touching the actual nodes, which this path deliberately leaves alone.

So the honest scope: colors.background (this PR) is the clean answer to the literal "make the color configurable" ask. Making the blurred fake selection both visible and legible over an opaque code background is a genuine open problem, the z-index lift buys visibility but the contrast can't be bought back the same way, and I'd rather hear how you'd want to approach it (lift behind an option, a documented limitation) than guess.

Test plan

  • createDefaultOnReposition.test.ts: default color, custom color, offsets preserved. pnpm vitest --project unit green; existing markSelection.test.tsx unaffected.
  • prettier, eslint, check-flow-types all clean.

Open questions

  • Util or Extension, or both? This threads colors through markSelection, selectionAlwaysOnDisplay, and the Extension config for parity.
  • Positional colors? (zero-churn back-compat, what this PR does) or an options bag (markSelection(editor, {onReposition, colors})) that soft-deprecates the positional onReposition?
  • Export createDefaultOnReposition, or keep it internal? It lets a custom-onReposition consumer reuse the default styling, at the cost of a slightly wider public surface.
  • The visible-over-opaque-background case above: worth a z-index lift behind an option here, or better left to the focused-selection path ([lexical-utils] Add dedupeSelectionRects: fix duplicate/extra selection rects on WebKit (#7106, #7492) #8709) and documented as a limitation of the blurred fake selection? Open to either.

…book#7492)

markSelection's default styler hardcodes background: 'Highlight', so a consumer
that wants a different selection color has to re-derive the whole styler. This
adds an optional `colors` argument ({ background }) to markSelection,
selectionAlwaysOnDisplay, and SelectionAlwaysOnDisplayExtension, and exposes a
createDefaultOnReposition(colors) factory so a custom onReposition can reuse the
default rect styling with a different color. Defaults to 'Highlight', so every
current call site is byte-identical.

The other half of facebook#7492 (the selection going invisible over text that has its
own background, e.g. code blocks) is a paint-order problem rather than a color
choice; the contrast measurement and the proposed fix are in the PR description.
@meta-cla meta-cla Bot added the CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed. label Jun 17, 2026
@vercel

vercel Bot commented Jun 17, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated (UTC)
lexical Ready Ready Preview, Comment Jun 17, 2026 6:33am
lexical-playground Ready Ready Preview, Comment Jun 17, 2026 6:33am

Request Review

@mayrang mayrang left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Not a deep review, but a few notes from skimming — hopefully helpful while you iterate.

On the implementation side:

I think the extension config colors should have a concrete default like {background: 'Highlight'} rather than undefined. An undefined-default field forces every consumer that overrides anything else to remember it's not really a partial — and once a second undefined-default lives on the same config, the pattern is harder to walk back later.

The nested colors object would also be safer with a mergeConfig. Right now it has one field so a shallow merge looks safe — but the type is clearly designed to grow (your own open question 4 hints at foreground and a lift opt-in). If a second consumer ever passes colors: {background: 'red'}, it would wipe whatever sibling field a previous layer set. I'd land the mergeConfig shape with this PR rather than retrofit later.

The test for createDefaultOnReposition covers the no-colors and concrete-color paths but doesn't pin colors: {} or colors: {background: undefined} — the guard handles both, so it's worth covering. An end-to-end pass on selectionAlwaysOnDisplay(editor, undefined, {background: 'red'}) would also exercise the threading itself, not just the factory.

On the open questions:

  1. I think the parity (util + extension both) is the right call, since markSelection and the extension already mirror each other.

  2. I'd keep it positional rather than moving to an options bag. The function is public with a .js.flow declaration, and switching the shape would churn every existing caller for one new option. The lexical utilities I've looked at (mergeRegister, \$findMatchingParent, positionNodeOnRange) are all positional too.

  3. I think createDefaultOnReposition should be exported. The motivation in the issue is "I want to change the color without re-deriving the offset styling", and keeping the factory internal would undo that. positionNodeOnRange is already public on the same layer.

  4. I'd defer the z-index lift to #8709 and document the limitation here. Your contrast measurement is the convincing part — lifting the blurred rect buys visibility, but ::selection doesn't apply when the selection isn't focused, so the token contrast can't really be recovered. The focused-selection path in #8709 is what can set ::selection { color }, so the lift fits naturally there.

These are just my read though — the maintainer may see it differently. Hope something in here is useful.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

CLA Signed This label is managed by the Facebook bot. Authors need to sign the CLA before a PR can be reviewed.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants