Skip to content

fix: scope Tailwind Preflight to prevent style conflicts in embedded …#138

Open
sashathor wants to merge 16 commits into
mainfrom
sasha/c-17056-scope-tailwind-preflight-css-to-prevent-host-app-style
Open

fix: scope Tailwind Preflight to prevent style conflicts in embedded …#138
sashathor wants to merge 16 commits into
mainfrom
sasha/c-17056-scope-tailwind-preflight-css-to-prevent-host-app-style

Conversation

@sashathor

@sashathor sashathor commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Fixes global CSS conflicts when @trycourier/react-designer is embedded in a host application. The editor's stylesheet previously shipped unscoped Tailwind Preflight resets (*, h1h6, a, button, ol, ul, img, etc.) that could override the host app's styles outbound, while hostile host-app element selectors could leak into the editor inbound.

This PR introduces scoped Preflight, inbound isolation rules, and defensive UI hardening so the editor renders correctly even when the host page injects aggressive global CSS.

Linear: C-17056
Canary: @trycourier/react-designer@canary from this branch


Problem

When embedded, the designer imports styles.css, which included Tailwind Preflight as global, unscoped element resets. That caused two classes of bugs:

  1. Outbound leakage — Editor Preflight overrode host typography, buttons, lists, links, etc. outside .theme-container.
  2. Inbound leakage — Host-app bare selectors (button { … }, h1 { … }, svg { display: none }, etc.) overrode editor chrome and content because:
    • Portaled UI (dropdowns, tooltips, dialogs) renders outside the editor DOM subtree
    • Host CSS injected on the page is unlayered and beats Tailwind @layer utilities regardless of specificity

Solution

1. Outbound isolation — scoped Preflight

  • Disable built-in Preflight in tailwind.config.js (corePlugins: { preflight: false })
  • Add scoped-preflight.css (~1,300 lines) with Preflight rules scoped via :where(.theme-container)
  • :where() contributes zero specificity, so courier-* utilities still override resets naturally
  • Import order: scoped-preflight.csseditor.css → Tailwind layers

2. Inbound isolation — block host styles from editor content

  • Layer 2 rules on :where(.theme-container) .ProseMirror explicitly reset text/font/box properties that host apps commonly override
  • Avoids all: revert (breaks Tailwind utility specificity inside the editor)
  • Layer 3 rules on html :where(.theme-container) protect editor chrome (buttons, tabs, inputs, menus)

3. Unlayered defensive rules (Layer 3b)

Host CSS is unlayered, so fixes that must win over hostile globals use unlayered rules in scoped-preflight.css at higher specificity, including:

Issue Fix
Drag handle red border on hover Inline borderColor on Handle; unlayered border-color on button hover
Dropdown menu red hover / wrong text color Unlayered [role="menuitem"] hover with var(--accent) and color: inherit
Sidebar vs channel tabs wrong colors Tab rules scoped to [role="tablist"].courier-justify-stretch vs .courier-border-none
Ghost button missing hover background Targeted button.hover\:courier-bg-accent:where(:hover) instead of blanket button:hover { background: transparent }
ContentTypePicker chevron missing courier-shrink-0 + inline width/height; ghost buttons get border-width: 0

4. Component hardening (~30 files)

Portaled and chrome UI components received defensive inline styles and courier- prefix fixes so they survive hostile host CSS:

  • Portaled surfaces: Surface, AlertDialog, DropdownMenu, Popover, Tooltip, VariableAutocomplete
  • Toolbars / menus: TextMenu, ContentTypePicker, ContentItemMenu, SlashMenu/MenuList
  • Icons: Icon.tsx path attrs promoted inline; Lucide strokeWidth inline across toolbars
  • Blocks: scoped .tiptap heading margin overrides on .node-element; block wrapper hardening
  • Misc: PreviewPanel toggle styles, Tabs.tsx, Status, Handle, SMS courier-text-* fix, DividerForm justify class fix

5. styles.css scoping

  • @layer base rules use :where(.theme-container) instead of bare * / body
  • .dark input spin buttons scoped to .theme-container
  • .tiptap and .courier-email-editor .tiptap selectors scoped under .theme-container

Dev tooling

New /css-scoping page in editor-dev (CssScopingPage.tsx):

  • Injects hostile host-app CSS (headings, buttons, links, SVGs, tabs, focus rings, etc.)
  • Loads TemplateEditor with all channels (email, sms, push, inbox, slack, msteams)
  • Includes sample variables and brand editor for realistic embedding tests

Tests

Unit (css-scoping.test.ts)

Expanded static analysis suite (~2,600+ assertions) covering:

  • Preflight disabled in Tailwind config
  • Import order and absence of bare * / body in @layer base
  • Outbound :where(.theme-container) scoping in scoped-preflight.css
  • Inbound ProseMirror isolation rules
  • Unlayered menuitem / ghost-button / tab rules
  • Build output CSS structure

E2E

  • text-block.spec.ts — Backspace at paragraph start no longer deletes the block
  • Full-cycle designer snapshots updated for heading/blockquote/column variants after scoped .tiptap heading margins removed 24px of document-flow spacing inside .node-element blocks
  • Email-side full-cycle snapshots remain warn-only (non-blocking)

Files changed

91 files — core CSS architecture + UI hardening + dev page + snapshot updates

Area Key files
CSS architecture scoped-preflight.css, styles.css, tailwind.config.js
Tests css-scoping.test.ts, full-cycle snapshot PNGs
Dev app CssScopingPage.tsx, App.tsx, Layout.tsx
UI components ui-kit/ (Tabs, DropdownMenu, AlertDialog, …), ui/ (TextMenu, Handle, Surface, …), channel sidebars

Test plan

  • pnpm --filter @trycourier/react-designer test (css-scoping + unit)
  • Manual verification on editor-dev/css-scoping with hostile styles
  • Full-cycle e2e designer snapshot baselines updated
  • CI: unit-test, e2e-test, full-cycle-e2e-test
  • Canary published from this branch for integration testing

Manual QA checklist

  • Embed editor in a host app with global button, h1, a, svg styles — host page unaffected outside editor
  • Editor chrome: channel tabs, sidebar tabs, dropdowns, tooltips, drag handles, ghost buttons
  • Email channel: headings, blockquotes, columns, text formatting, slash menu
  • Portaled UI (color picker, content type picker, variable autocomplete) over hostile host CSS

Publishing

Changeset included (@trycourier/react-designer patch):

Fix global CSS override by scoping Tailwind Preflight to prevent host-app style conflicts when embedding the editor


Checklist

  • Tests added/updated
  • Manually tested (/css-scoping dev page)
  • Changeset added
  • Merge conflict with main resolved (packageManager + Corepack-aligned workflows)
  • PR approved

@linear

linear Bot commented Mar 9, 2026

Copy link
Copy Markdown

sashathor and others added 13 commits March 10, 2026 15:07
…o-prevent-host-app-style

Resolve snapshot conflicts by keeping branch versions (reflect CSS scoping changes).

Made-with: Cursor
…ailwind-preflight-css-to-prevent-host-app-style

Co-authored-by: Cursor <cursoragent@cursor.com>

# Conflicts:
#	packages/react-designer/e2e/full-cycle/blockquote-visual.spec.ts-snapshots/email-text-style-h3-chromium-darwin-actual.png
#	packages/react-designer/e2e/full-cycle/heading-visual.spec.ts-snapshots/email-h3-default-chromium-darwin-actual.png
#	packages/react-designer/src/components/extensions/List/ListForm.tsx
Replace global preflight with scoped rules, add unlayered hover/focus resets for tabs, menus, and ghost buttons, harden portaled components and icons, expand the css-scoping dev harness, and refresh full-cycle snapshots.

Co-authored-by: Cursor <cursoragent@cursor.com>
…o-prevent-host-app-style

Resolve editor-dev routing conflicts by keeping both CSS Scoping and Translation Editor pages.

Co-authored-by: Cursor <cursoragent@cursor.com>
…t in CI

pnpm/action-setup@v4 fails when both workflow version: 10 and package.json packageManager are set.

Co-authored-by: Cursor <cursoragent@cursor.com>
Scoped .tiptap heading rules remove 24px of document-flow margin inside
.node-element blocks, so heading/blockquote/column designer baselines need
refreshing.

Co-authored-by: Cursor <cursoragent@cursor.com>
Restore pnpm packageManager in package.json and update column full-cycle
email actual screenshots from the latest local run.

Co-authored-by: Cursor <cursoragent@cursor.com>
pnpm/action-setup@v4 errors when both workflow version: 10 and
package.json packageManager are set.

Co-authored-by: Cursor <cursoragent@cursor.com>
…o-prevent-host-app-style

Resolve package.json conflict by keeping main's packageManager field,
which aligns with updated workflows that read pnpm version from Corepack.

Co-authored-by: Cursor <cursoragent@cursor.com>
@sashathor
sashathor marked this pull request as ready for review June 8, 2026 14:51
@sashathor
sashathor requested a review from a team June 8, 2026 14:51
@sashathor
sashathor requested a review from gerjunior as a code owner June 8, 2026 14:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant