Skip to content

Comments

fix(css): scope ProseMirror CSS to prevent bleeding into host apps (SD-1850)#2134

Open
tupizz wants to merge 3 commits intomainfrom
tadeu/scope-prosemirror-css-sd-1850
Open

fix(css): scope ProseMirror CSS to prevent bleeding into host apps (SD-1850)#2134
tupizz wants to merge 3 commits intomainfrom
tadeu/scope-prosemirror-css-sd-1850

Conversation

@tupizz
Copy link
Contributor

@tupizz tupizz commented Feb 20, 2026

Problem

SuperDoc's CSS was leaking into host applications in three ways:

  1. Bare a element selectora { text-decoration: auto } in global.css overrode ALL <a> tags on the page, not just links inside the editor.

  2. Unscoped .ProseMirror selectors — All ~55 .ProseMirror CSS rules in prosemirror.css affected any ProseMirror instance on the page. If a host app used ProseMirror for another editor (comments, chat), SuperDoc's styles would strip list styling, force table collapse, override image sizing, etc.

  3. .sr-only utility class collision — The DomPainter injected a .sr-only class at runtime that collides with Bootstrap, Tailwind, and many other frameworks that use the same class name.

Solution

This follows the CKEditor 5 model — the industry standard for editor CSS isolation. CKEditor scopes all content styles under .ck-content, we scope ours under .super-editor.

SuperDoc already did this for ~90% of its CSS. This PR closes the remaining gaps:

  • Remove bare a selector from global.css — the scoped .super-editor a rule already handles editor links
  • Scope all .ProseMirror and .ProseMirror-* selectors under .super-editor in prosemirror.css (~55 selectors)
  • Scope .sd-* selectors in extension CSS files: structured-content.css, document-section.css, noderesizer.css, ai.css, page-number.css
  • Namespace keyframe animations: aiTextAppearsuperdoc-aiTextAppear, ai-pulsesuperdoc-ai-pulse
  • Rename .sr-only.superdoc-sr-only in DomPainter runtime styles

Why this approach

Approach Prevents outward bleed Browser support Works with import 'lib/style.css'
Parent class scoping (this PR) Yes 100% Yes
Shadow DOM Yes ~96% No (breaks ProseMirror)
CSS @layer Yes ~95% Requires consumer changes
CSS Modules Yes 100% No (build-time only)

Parent class scoping is the simplest, most compatible approach. It's what CKEditor 5 and Quill use in production.

What is NOT changed

  • superdoc/ package CSS — already scoped to .superdoc, .super-editor, .superdoc-toolbar
  • isolation.css — inward protection (all: revert), already correctly scoped
  • DomPainter runtime styles — already scoped to .superdoc-* (except .sr-only which we fix)
  • Tippy/popover styles — render in document.body, kept global with unique [data-theme] selectors
  • .presentation-editor__* selectors — already namespaced, render in DomPainter layer

Test plan

  • pnpm --filter super-editor test — 672 files, 6345 tests pass
  • pnpm --filter @superdoc/painter-dom test — 23 files, 629 tests pass (.superdoc-sr-only assertions updated)
  • New css-no-bleed.test.js lint test — structurally verifies no bare element selectors, all .ProseMirror scoped under .super-editor, no generic utility class collisions (catches future regressions)
  • Visual regression tests (CI)
  • Manual verification: load editor in host app, confirm host styles unaffected

…D-1850)

SuperDoc's CSS was leaking into host applications in three ways:

1. A bare `a { text-decoration: auto }` selector overrode ALL links on the
   page, not just those inside the editor.

2. All `.ProseMirror` selectors (~55) were unscoped, affecting any
   ProseMirror editor on the page — not just SuperDoc's hidden instance.

3. A `.sr-only` utility class collided with Bootstrap/Tailwind definitions.

This follows the CKEditor 5 model (the industry standard): scope all
content styles under a parent class. SuperDoc already did this for ~90%
of its CSS — this commit closes the remaining gaps.

Changes:
- Remove bare `a` element selector from global.css
- Scope all `.ProseMirror` and `.ProseMirror-*` selectors under
  `.super-editor` in prosemirror.css
- Scope `.sd-*` selectors in extension CSS files (structured-content,
  document-section, noderesizer, ai, page-number)
- Rename keyframes `aiTextAppear` → `superdoc-aiTextAppear` and
  `ai-pulse` → `superdoc-ai-pulse` to avoid collisions
- Rename `.sr-only` → `.superdoc-sr-only` in DomPainter styles
- Add CSS bleed prevention lint test that structurally verifies no
  unscoped selectors exist (catches future regressions)
Copilot AI review requested due to automatic review settings February 20, 2026 23:19
@linear
Copy link

linear bot commented Feb 20, 2026

@chatgpt-codex-connector
Copy link

Codex usage limits have been reached for code reviews. Please check with the admins of this repo to increase the limits by adding credits.
Credits must be used to enable repository wide code reviews.

Copy link
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

This PR addresses CSS bleeding from SuperDoc into host applications by implementing comprehensive parent class scoping, following the CKEditor 5 model. The changes prevent three types of CSS conflicts: bare element selectors, unscoped ProseMirror selectors, and generic utility class name collisions.

Changes:

  • Removed bare a element selector from global.css (already covered by scoped .super-editor a rule)
  • Scoped all ~55 .ProseMirror and .ProseMirror-* selectors under .super-editor in prosemirror.css
  • Scoped all .sd-* selectors in extension CSS files under .super-editor
  • Renamed .sr-only to .superdoc-sr-only to prevent framework collisions
  • Namespaced AI-related keyframe animations with superdoc- prefix
  • Added comprehensive structural lint tests to prevent future CSS bleed regressions

Reviewed changes

Copilot reviewed 11 out of 11 changed files in this pull request and generated 1 comment.

Show a summary per file
File Description
packages/super-editor/src/tests/css-no-bleed.test.js New structural lint test that validates no bare HTML selectors, all .ProseMirror selectors scoped, and no generic utility class collisions
packages/super-editor/src/assets/styles/layout/global.css Removed unscoped a selector that was bleeding into host apps
packages/super-editor/src/assets/styles/elements/prosemirror.css Scoped all ProseMirror selectors under .super-editor to prevent conflicts with other ProseMirror instances
packages/super-editor/src/assets/styles/extensions/structured-content.css Scoped all .sd-structured-content* selectors under .super-editor
packages/super-editor/src/assets/styles/extensions/noderesizer.css Scoped all node resizer selectors under .super-editor
packages/super-editor/src/assets/styles/extensions/document-section.css Scoped all document section selectors under .super-editor
packages/super-editor/src/assets/styles/elements/page-number.css Scoped page number selectors under .super-editor
packages/super-editor/src/assets/styles/elements/ai.css Scoped AI selectors and namespaced keyframes (superdoc-aiTextAppear, superdoc-ai-pulse)
packages/layout-engine/painters/dom/src/styles.ts Renamed .sr-only to .superdoc-sr-only to prevent Bootstrap/Tailwind collisions
packages/layout-engine/painters/dom/src/renderer.ts Updated class name usage from sr-only to superdoc-sr-only
packages/layout-engine/painters/dom/src/index.test.ts Updated test assertions to verify superdoc-sr-only class name

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 368 to 372
@@ -368,18 +371,18 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html
}
}
Copy link

Copilot AI Feb 20, 2026

Choose a reason for hiding this comment

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

The keyframe animation ProseMirror-cursor-blink should be renamed to superdoc-ProseMirror-cursor-blink to prevent collisions with other ProseMirror instances on the page. Keyframe names are global and are not scoped by CSS parent selectors. This follows the same pattern used for the other keyframe animations in this PR (superdoc-aiTextAppear, superdoc-ai-pulse).

Copilot uses AI. Check for mistakes.
Copy link
Contributor

@caio-pizzol caio-pizzol left a comment

Choose a reason for hiding this comment

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

nice work scoping all the CSS

feel free to merge after addressing relevant comments

- Rename ProseMirror-cursor-blink keyframe to superdoc-cursor-blink
- Scope .toolbar-icon__icon--ai selectors under .super-editor
- Add @Keyframes name lint test requiring superdoc-/sd- prefix
- Remove .toolbar-icon from scopedPrefixes allowlist
The per-section measurement constraints change belongs to the SD-1962
table pagination fix, not the CSS scoping work. Reverting this file to
match main; the change lives in tadeu/sd-1962-nested-table-pagination.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants