Skip to content

poc: CSS Modules using shadowrootadoptedstylesheets for the Lit SSR pipeline#3012

Open
zeroedin wants to merge 23 commits into
mainfrom
poc/lit-ssr/css-modules
Open

poc: CSS Modules using shadowrootadoptedstylesheets for the Lit SSR pipeline#3012
zeroedin wants to merge 23 commits into
mainfrom
poc/lit-ssr/css-modules

Conversation

@zeroedin
Copy link
Copy Markdown
Collaborator

@zeroedin zeroedin commented May 8, 2026

PROOF OF CONCEPT

Summary

Implements shadowrootadoptedstylesheets for the Lit SSR pipeline. Instead of inlining duplicate <style> blocks inside every component's declarative shadow DOM <template>, shared styles are defined once per page as <style type="module" specifier="name"> blocks and referenced via shadowrootadoptedstylesheets attributes on host elements and templates. Template literal whitespace and comments are also stripped at build time via @literals/html-css-minifier.

How it works

  1. Marker injection (lit-css-node.ts) — The CSS loader prepends /* @sheet:specifier */ markers to CSS content during SSR, derived from the CSS filename (e.g., rh-button.css@sheet:rh-button). The marker survives the @pwrs/lit-css transform and becomes part of CSSResult.cssText.

  2. Renderer extraction (worker.ts) — A custom LitElementRenderer override extracts @sheet: markers from elementStyles in connectedCallback(), emits shadowrootadoptedstylesheets attributes on host elements and template tags, and processes CSS through lightningcss. After rendering, a post-processing step collects all unique stylesheets into shared <style type="module"> blocks inserted before </head>.

  3. Template minification (lit-html-minifier-node.ts) — An ESM loader hook intercepts element module imports and runs @literals/html-css-minifier on the compiled source, stripping whitespace and comments from html and css tagged template literals before SSR renders them.

  4. Polyfill (shadowroot-adopted-stylesheets.js) — Since no browser natively supports shadowrootadoptedstylesheets yet, a MutationObserver-based polyfill in <head> applies styles as elements appear during HTML parsing. It collects <style type="module" specifier="..."> blocks into CSSStyleSheet objects and applies them to shadow roots via adoptedStyleSheets. Because MutationObserver callbacks run as microtasks before the browser paints, styles are applied before each frame — preventing FOUC on Safari and Firefox.

  5. Color palette marker (lib/color-palettes.ts) — The colorPalettes decorator injects a @sheet:rhds-color-palette marker into its CSS so the SSR renderer can extract and deduplicate it alongside component stylesheets. This is inert when SSR is not available.

Based on work by @sorvell https://lit.dev/playground/#gist=0a095178701ec2c5426eddb1a37545ea

Related links

shadowrootadoptedstylesheets Performance Report

Comparison of production (ux.redhat.com) vs deploy preview (deploy-preview-3012) with shadowrootadoptedstylesheets CSS deduplication and template literal minification.

HTML Payload Size

Page Production Preview CSS Dedup Saved % Minifier Saved % Total Saved % Reduction Shared Modules
Homepage 771 KB 192 KB 441 KB 57.2% 138 KB 17.9% 579 KB 75.1% 14
Button docs 912 KB 276 KB 486 KB 53.3% 150 KB 16.4% 636 KB 69.7% 29
Accordion docs 929 KB 287 KB 491 KB 52.9% 151 KB 16.3% 642 KB 69.1% 29
Tabs docs 964 KB 298 KB 509 KB 52.8% 157 KB 16.3% 666 KB 69.1% 29
Card docs 969 KB 288 KB 523 KB 54.0% 158 KB 16.3% 681 KB 70.3% 29
Dialog docs 933 KB 285 KB 495 KB 53.1% 153 KB 16.4% 648 KB 69.4% 29
Theming 627 KB 166 KB 333 KB 53.1% 128 KB 20.4% 461 KB 73.5% 17
Card patterns 1.34 MB 351 KB 801 KB 58.4% 220 KB 16.0% 1021 KB 74.4% 28
Release notes 4.10 MB 608 KB 3.08 MB 75.1% 436 KB 10.4% 3.50 MB 85.5% 23
Color tokens 10.22 MB 3.01 MB 6.01 MB 58.8% 1.21 MB 11.8% 7.21 MB 70.6% 24
Code-block (thousands) 36.08 MB 7.93 MB 21.82 MB 60.5% 6.33 MB 17.5% 28.15 MB 78.0% 4

Lighthouse Performance (Mobile Emulation)

Single-run scores. Results vary between runs.

Page Metric Production Preview Delta
Homepage Score 84 85 +1
FCP 2.8 s 2.5 s -0.3 s
LCP 3.5 s 3.5 s -0.0 s
TBT 22 ms 26 ms +5 ms
CLS 0.000 0.000 0.000
SI 4.6 s 4.4 s -0.2 s
TTFB 84 ms 86 ms +2 ms
Button docs Score 64 75 +11
FCP 3.0 s 3.0 s -0.0 s
LCP 4.7 s 4.4 s -0.4 s
TBT 440 ms 139 ms -302 ms
CLS 0.000 0.000 0.000
SI 4.8 s 4.7 s -0.1 s
TTFB 88 ms 51 ms -37 ms
Accordion docs Score 75 75 0
FCP 3.0 s 3.0 s -0.0 s
LCP 4.6 s 4.4 s -0.2 s
TBT 120 ms 141 ms +21 ms
CLS 0.000 0.000 0.000
SI 4.7 s 4.7 s -0.1 s
TTFB 70 ms 54 ms -16 ms
Tabs docs Score 71 75 +4
FCP 3.1 s 3.0 s -0.1 s
LCP 4.7 s 4.4 s -0.3 s
TBT 216 ms 130 ms -86 ms
CLS 0.000 0.000 0.000
SI 4.8 s 4.7 s -0.1 s
TTFB 72 ms 64 ms -8 ms
Card docs Score 75 77 +2
FCP 3.0 s 3.0 s -0.0 s
LCP 4.5 s 4.4 s -0.1 s
TBT 142 ms 72 ms -70 ms
CLS 0.000 0.000 0.000
SI 4.7 s 4.7 s -0.0 s
TTFB 76 ms 50 ms -26 ms
Dialog docs Score 72 76 +4
FCP 3.0 s 3.0 s -0.0 s
LCP 4.8 s 4.5 s -0.3 s
TBT 150 ms 76 ms -75 ms
CLS 0.000 0.000 0.000
SI 4.8 s 4.6 s -0.2 s
TTFB 129 ms 55 ms -74 ms
Theming Score 85 85 0
FCP 2.6 s 2.6 s +0.0 s
LCP 3.5 s 3.5 s +0.0 s
TBT 22 ms 18 ms -4 ms
CLS 0.000 0.000 0.000
SI 4.4 s 4.4 s -0.0 s
TTFB 95 ms 71 ms -24 ms
Card patterns Score 78 81 +3
FCP 3.0 s 2.8 s -0.2 s
LCP 4.0 s 3.8 s -0.2 s
TBT 106 ms 42 ms -64 ms
CLS 0.000 0.000 0.000
SI 4.9 s 4.6 s -0.4 s
TTFB 219 ms 59 ms -160 ms
Release notes Score 46 51 +5
FCP 3.0 s 2.6 s -0.4 s
LCP 4.7 s 4.5 s -0.3 s
TBT 2,229 ms 1,749 ms -480 ms
CLS 0.000 0.000 0.000
SI 5.1 s 4.7 s -0.3 s
TTFB 215 ms 64 ms -151 ms
Color tokens Score 51 34 -17
FCP 2.1 s 2.9 s +0.7 s
LCP 4.2 s 8.9 s +4.7 s
TBT 12,970 ms 12,037 ms -933 ms
CLS 0.000 0.000 0.000
SI 4.6 s 6.6 s +2.0 s
TTFB 442 ms 61 ms -381 ms
Code-block (thousands) Score 43 52 +9
FCP 2.6 s 1.5 s -1.1 s
LCP 3.1 s 1.5 s -1.5 s
TBT 3,207 ms 6,379 ms +3,171 ms
CLS 0.293 0.293 0.000
SI 4.5 s 4.3 s -0.3 s
TTFB 1,396 ms 1,292 ms -104 ms

Key Findings

HTML Payload

  • 69-85% reduction in HTML payload across all pages (up from 48-73% with template literal minification)
  • Inline <style> tags dropped from hundreds (or thousands) to 1 per page
  • Release notes is the biggest win: 894 inline styles eliminated, saving 3.5 MB (85.5%)
  • Color tokens page: 10.2 MB down to 3.0 MB with 3,503 inline styles eliminated
  • Code-block (thousands): 36 MB down to 7.9 MB — 78% reduction
  • Each page uses 16-32 shared style modules to replace all duplicate inline styles

Lighthouse

  • Scores improved or held steady across most pages
  • Button docs gained +11 points (64 → 75), with TBT dropping 302 ms
  • CLS regressions resolved — all pages now report 0.000 CLS
  • TBT improved on most pages (Button -302 ms, Tabs -86 ms, Dialog -75 ms, Card -70 ms, Card patterns -64 ms, Release notes -480 ms)
  • Release notes score improved 46 → 51 with 480 ms TBT reduction
  • Code-block (thousands) improved overall score 43 → 52 despite TBT regression, with FCP/LCP significantly faster (-1.1 s / -1.5 s)
  • Color tokens regressed on LCP (4.2 s → 8.9 s) — likely due to polyfill overhead on 3,500+ elements; warrants separate investigation

Caveats

  • Lighthouse results are single runs and can vary 10-20% between runs
  • TTFB differences are heavily influenced by CDN cache state and geographic location
  • Production and preview are on different Netlify deploy contexts, which can affect serving behavior
  • The Color tokens LCP regression may be related to polyfill overhead on high-element-count pages

Test plan

  • npm run docs builds successfully
  • Inspect built HTML: <style type="module" specifier="..."> blocks appear once before </head>
  • No inline <style> inside <template> tags for components with specifiers
  • shadowrootadoptedstylesheets attributes present on host elements and <template> tags
  • Open in browser — styles render correctly, no FOUC
  • DevTools: shadow roots have populated adoptedStyleSheets
  • Demo iframes render with styles applied (polyfill loaded in both base.njk and demo.html)

Testing Instructions

  • View the deploy preview

Notes to Reviewers

@qodo-code-review
Copy link
Copy Markdown

ⓘ You've reached your Qodo monthly free-tier limit. Reviews pause until next month — upgrade your plan to continue now, or link your paid account if you already have one.

@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented May 8, 2026

⚠️ No Changeset found

Latest commit: 77370c2

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

@netlify
Copy link
Copy Markdown

netlify Bot commented May 8, 2026

Deploy Preview for red-hat-design-system ready!

Name Link
🔨 Latest commit 77370c2
🔍 Latest deploy log https://app.netlify.com/projects/red-hat-design-system/deploys/6a03411f48fc280008b296a4
😎 Deploy Preview https://deploy-preview-3012--red-hat-design-system.netlify.app
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.
🤖 Make changes Run an agent on this branch

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Size Change: -21.9 kB (-6.96%) ✅

Total Size: 292 kB

📦 View Changed
Filename Size Change
elements/rh-accordion/rh-accordion-header.js 2.88 kB -455 B (-13.66%) 👏
elements/rh-accordion/rh-accordion-panel.js 1.82 kB -262 B (-12.61%) 👏
elements/rh-accordion/rh-accordion.js 3.52 kB -145 B (-3.95%)
elements/rh-alert/rh-alert.js 5.24 kB -285 B (-5.16%)
elements/rh-announcement/rh-announcement.js 2.34 kB -507 B (-17.83%) 👏
elements/rh-audio-player/rh-audio-player-about.js 2.02 kB -268 B (-11.74%) 👏
elements/rh-audio-player/rh-audio-player-rate-stepper.js 2.03 kB -42 B (-2.03%)
elements/rh-audio-player/rh-audio-player-scrolling-text-overflow.js 1.66 kB -117 B (-6.58%)
elements/rh-audio-player/rh-audio-player-subscribe.js 1.63 kB -269 B (-14.17%) 👏
elements/rh-audio-player/rh-audio-player.js 12.5 kB -671 B (-5.1%)
elements/rh-audio-player/rh-cue.js 2.22 kB -105 B (-4.52%)
elements/rh-audio-player/rh-transcript.js 3.14 kB -249 B (-7.36%)
elements/rh-avatar/rh-avatar.js 3.22 kB -234 B (-6.78%)
elements/rh-back-to-top/rh-back-to-top.js 2.45 kB -342 B (-12.25%) 👏
elements/rh-badge/rh-badge.js 1.88 kB -176 B (-8.58%)
elements/rh-blockquote/rh-blockquote.js 1.7 kB -223 B (-11.58%) 👏
elements/rh-breadcrumb/rh-breadcrumb.js 2.15 kB -196 B (-8.37%)
elements/rh-button-group/rh-button-group.js 850 B -87 B (-9.28%)
elements/rh-button/rh-button.js 3.77 kB -332 B (-8.1%)
elements/rh-card/rh-card.js 2.98 kB -477 B (-13.79%) 👏
elements/rh-chip/rh-chip-group.js 1.68 kB -301 B (-15.21%) 👏
elements/rh-chip/rh-chip.js 2.37 kB -85 B (-3.46%)
elements/rh-code-block/prism.css.js 622 B -45 B (-6.75%)
elements/rh-code-block/rh-code-block.js 8.54 kB -663 B (-7.2%)
elements/rh-cta/rh-cta.js 3.75 kB -152 B (-3.9%)
elements/rh-dialog/rh-dialog.js 4.58 kB -460 B (-9.12%)
elements/rh-disclosure/rh-disclosure.js 2.83 kB -580 B (-17%) 👏
elements/rh-footer/rh-footer-block.js 891 B -177 B (-16.57%) 👏
elements/rh-footer/rh-footer-copyright.js 467 B -182 B (-28.04%) 🎉
elements/rh-footer/rh-footer-links.js 1.35 kB -296 B (-17.97%) 👏
elements/rh-footer/rh-footer-social-link.js 1.1 kB -131 B (-10.61%) 👏
elements/rh-footer/rh-footer-universal.js 4.26 kB -1.1 kB (-20.55%) 🎉
elements/rh-footer/rh-footer.js 4.78 kB -1.06 kB (-18.21%) 👏
elements/rh-health-index/rh-health-index.js 2.45 kB -24 B (-0.97%)
elements/rh-icon/rh-icon.js 2.82 kB -158 B (-5.3%)
elements/rh-jump-links/rh-jump-link.js 1.73 kB -163 B (-8.62%)
elements/rh-jump-links/rh-jump-links-list.js 1.44 kB -231 B (-13.82%) 👏
elements/rh-jump-links/rh-jump-links.js 2.77 kB -302 B (-9.82%) 👏
elements/rh-menu-dropdown/rh-menu-dropdown.js 4.13 kB -302 B (-6.82%)
elements/rh-menu/rh-menu-item-group.js 838 B -103 B (-10.95%) 👏
elements/rh-menu/rh-menu-item.js 2.3 kB -251 B (-9.84%) 👏
elements/rh-menu/rh-menu.js 1.91 kB -125 B (-6.13%)
elements/rh-navigation-link/rh-navigation-link.js 1.77 kB -364 B (-17.06%) 👏
elements/rh-navigation-primary/rh-navigation-primary-item-menu.js 1.24 kB -163 B (-11.6%) 👏
elements/rh-navigation-primary/rh-navigation-primary-item.js 3.83 kB -405 B (-9.57%) 👏
elements/rh-navigation-primary/rh-navigation-primary.js 9.5 kB -520 B (-5.19%)
elements/rh-navigation-secondary/rh-navigation-secondary-dropdown.js 2.79 kB -362 B (-11.49%) 👏
elements/rh-navigation-secondary/rh-navigation-secondary-menu-section.js 1.49 kB -522 B (-25.91%) 🎉
elements/rh-navigation-secondary/rh-navigation-secondary-menu.js 1.86 kB -154 B (-7.66%)
elements/rh-navigation-secondary/rh-navigation-secondary-overlay.js 827 B -1 B (-0.12%)
elements/rh-navigation-secondary/rh-navigation-secondary.js 5.31 kB -1 kB (-15.88%) 👏
elements/rh-navigation-secondary/test/fixtures.js 705 B -64 B (-8.32%)
elements/rh-navigation-vertical/rh-navigation-vertical-list.js 2.35 kB -209 B (-8.15%)
elements/rh-navigation-vertical/rh-navigation-vertical.js 1.54 kB -104 B (-6.31%)
elements/rh-pagination/rh-pagination.js 5.93 kB -577 B (-8.87%)
elements/rh-progress-stepper/rh-progress-step.js 2.6 kB -417 B (-13.8%) 👏
elements/rh-progress-stepper/rh-progress-stepper.js 4.65 kB -130 B (-2.72%)
elements/rh-readtime/rh-readtime.js 2.93 kB +1 B (+0.03%)
elements/rh-scheme-toggle/rh-scheme-toggle.js 2.33 kB -27 B (-1.14%)
elements/rh-select/rh-option-group.js 1.47 kB -136 B (-8.48%)
elements/rh-select/rh-option.js 2.22 kB -168 B (-7.03%)
elements/rh-select/rh-select.js 9.09 kB -371 B (-3.92%)
elements/rh-site-status/rh-site-status.js 2.47 kB -63 B (-2.49%)
elements/rh-skeleton/rh-skeleton.js 566 B -176 B (-23.72%) 🎉
elements/rh-skip-link/rh-skip-link.js 1.35 kB -90 B (-6.23%)
elements/rh-spinner/rh-spinner.js 1.42 kB -84 B (-5.57%)
elements/rh-stat/rh-stat.js 2.37 kB -336 B (-12.4%) 👏
elements/rh-subnav/rh-subnav.js 2.99 kB -189 B (-5.95%)
elements/rh-surface/rh-surface.js 844 B -21 B (-2.43%)
elements/rh-surface/test/elements.js 757 B -6 B (-0.79%)
elements/rh-switch/rh-switch.js 3 kB -220 B (-6.83%)
elements/rh-table/rh-sort-button.js 1.71 kB -169 B (-9.01%)
elements/rh-table/rh-table.js 3.02 kB -253 B (-7.73%)
elements/rh-tabs/rh-tab-panel.js 1.09 kB -99 B (-8.35%)
elements/rh-tabs/rh-tab.js 3.5 kB -145 B (-3.98%)
elements/rh-tabs/rh-tabs.js 4.07 kB -277 B (-6.37%)
elements/rh-tag/rh-tag.js 3.08 kB -238 B (-7.17%)
elements/rh-tile/rh-tile-group.js 1.91 kB -61 B (-3.09%)
elements/rh-tile/rh-tile.js 4.72 kB -374 B (-7.34%)
elements/rh-timestamp/rh-timestamp.js 1.33 kB -6 B (-0.45%)
elements/rh-tooltip/rh-tooltip.js 3.2 kB -202 B (-5.93%)
elements/rh-video-embed/rh-video-embed.js 4.62 kB -826 B (-15.17%) 👏
lib/color-palettes.js 897 B +46 B (+5.41%) 🔍
lib/elements/rh-context-demo/rh-context-demo.js 1.15 kB -16 B (-1.38%)
lib/elements/rh-context-picker/rh-context-picker.js 2.15 kB -27 B (-1.24%)
ℹ️ View Unchanged
Filename Size
elements.js 834 B
elements/rh-accordion/context.js 162 B
elements/rh-avatar/random-pattern-controller.js 2.72 kB
elements/rh-chip/context.js 165 B
elements/rh-code-block/prism.js 572 B
elements/rh-dialog/yt-api.js 617 B
elements/rh-icon/ssr.js 181 B
elements/rh-jump-links/context.js 179 B
elements/rh-navigation-primary/context.js 176 B
elements/rh-progress-stepper/context.js 187 B
elements/rh-tabs/context.js 223 B
lib/context/headings/consumer.js 591 B
lib/context/headings/provider.js 1.2 kB
lib/environment.js 194 B
lib/functions.js 175 B
lib/I18nController.js 1.37 kB
lib/ScreenSizeController.js 876 B
lib/ssr-controller.js 201 B
lib/themable.js 549 B
react/lib/color-palettes.js 97 B
react/lib/context/headings/consumer.js 103 B
react/lib/context/headings/provider.js 105 B
react/lib/elements/rh-context-demo/rh-context-demo.js 186 B
react/lib/elements/rh-context-picker/rh-context-picker.js 189 B
react/lib/functions.js 92 B
react/lib/I18nController.js 97 B
react/lib/ScreenSizeController.js 102 B
react/lib/ssr-controller.js 97 B
react/lib/themable.js 91 B
react/rh-accordion/rh-accordion-header.js 199 B
react/rh-accordion/rh-accordion-panel.js 185 B
react/rh-accordion/rh-accordion.js 202 B
react/rh-alert/rh-alert.js 184 B
react/rh-announcement/rh-announcement.js 189 B
react/rh-audio-player/rh-audio-player-about.js 191 B
react/rh-audio-player/rh-audio-player-rate-stepper.js 223 B
react/rh-audio-player/rh-audio-player-scrolling-text-overflow.js 214 B
react/rh-audio-player/rh-audio-player-subscribe.js 196 B
react/rh-audio-player/rh-audio-player.js 183 B
react/rh-audio-player/rh-cue.js 195 B
react/rh-audio-player/rh-transcript.js 207 B
react/rh-avatar/rh-avatar.js 173 B
react/rh-back-to-top/rh-back-to-top.js 183 B
react/rh-badge/rh-badge.js 174 B
react/rh-blockquote/rh-blockquote.js 179 B
react/rh-breadcrumb/rh-breadcrumb.js 179 B
react/rh-button-group/rh-button-group.js 184 B
react/rh-button/rh-button.js 174 B
react/rh-card/rh-card.js 172 B
react/rh-chip/rh-chip-group.js 182 B
react/rh-chip/rh-chip.js 180 B
react/rh-code-block/rh-code-block.js 193 B
react/rh-cta/rh-cta.js 170 B
react/rh-dialog/rh-dialog.js 203 B
react/rh-disclosure/rh-disclosure.js 192 B
react/rh-footer/rh-footer-block.js 184 B
react/rh-footer/rh-footer-copyright.js 187 B
react/rh-footer/rh-footer-links.js 185 B
react/rh-footer/rh-footer-social-link.js 193 B
react/rh-footer/rh-footer-universal.js 188 B
react/rh-footer/rh-footer.js 174 B
react/rh-health-index/rh-health-index.js 184 B
react/rh-icon/rh-icon.js 195 B
react/rh-jump-links/rh-jump-link.js 183 B
react/rh-jump-links/rh-jump-links-list.js 189 B
react/rh-jump-links/rh-jump-links.js 195 B
react/rh-menu-dropdown/rh-menu-dropdown.js 198 B
react/rh-menu/rh-menu-item-group.js 190 B
react/rh-menu/rh-menu-item.js 181 B
react/rh-menu/rh-menu.js 182 B
react/rh-navigation-link/rh-navigation-link.js 186 B
react/rh-navigation-primary/rh-navigation-primary-item-menu.js 205 B
react/rh-navigation-primary/rh-navigation-primary-item.js 210 B
react/rh-navigation-primary/rh-navigation-primary.js 189 B
react/rh-navigation-secondary/rh-navigation-secondary-dropdown.js 227 B
react/rh-navigation-secondary/rh-navigation-secondary-menu-section.js 205 B
react/rh-navigation-secondary/rh-navigation-secondary-menu.js 199 B
react/rh-navigation-secondary/rh-navigation-secondary-overlay.js 201 B
react/rh-navigation-secondary/rh-navigation-secondary.js 213 B
react/rh-navigation-vertical/rh-navigation-vertical-list.js 209 B
react/rh-navigation-vertical/rh-navigation-vertical.js 189 B
react/rh-pagination/rh-pagination.js 178 B
react/rh-progress-stepper/rh-progress-step.js 196 B
react/rh-progress-stepper/rh-progress-stepper.js 186 B
react/rh-readtime/rh-readtime.js 175 B
react/rh-scheme-toggle/rh-scheme-toggle.js 183 B
react/rh-select/rh-option-group.js 187 B
react/rh-select/rh-option.js 177 B
react/rh-select/rh-select.js 204 B
react/rh-site-status/rh-site-status.js 181 B
react/rh-skeleton/rh-skeleton.js 176 B
react/rh-skip-link/rh-skip-link.js 181 B
react/rh-spinner/rh-spinner.js 175 B
react/rh-stat/rh-stat.js 171 B
react/rh-subnav/rh-subnav.js 175 B
react/rh-surface/rh-surface.js 175 B
react/rh-switch/rh-switch.js 185 B
react/rh-table/rh-sort-button.js 200 B
react/rh-table/rh-table.js 174 B
react/rh-tabs/rh-tab-panel.js 181 B
react/rh-tabs/rh-tab.js 187 B
react/rh-tabs/rh-tabs.js 184 B
react/rh-tag/rh-tag.js 171 B
react/rh-tile/rh-tile-group.js 183 B
react/rh-tile/rh-tile.js 181 B
react/rh-timestamp/rh-timestamp.js 176 B
react/rh-tooltip/rh-tooltip.js 175 B
react/rh-video-embed/rh-video-embed.js 227 B
uxdot/ssr-adopt-directive.js 2.02 kB
uxdot/uxdot-best-practice.js 812 B
uxdot/uxdot-copy-button.js 1.24 kB
uxdot/uxdot-copy-permalink.js 1.14 kB
uxdot/uxdot-demo.js 2.89 kB
uxdot/uxdot-example.js 1.14 kB
uxdot/uxdot-feedback.js 983 B
uxdot/uxdot-header.js 886 B
uxdot/uxdot-knob-attribute.js 3.73 kB
uxdot/uxdot-masthead.js 1.45 kB
uxdot/uxdot-pattern-ssr-controller-client.js 386 B
uxdot/uxdot-pattern-ssr-controller-server.js 1.71 kB
uxdot/uxdot-pattern-ssr-controller.js 213 B
uxdot/uxdot-pattern.js 2.37 kB
uxdot/uxdot-repo-status-checklist.js 1.39 kB
uxdot/uxdot-repo-status-list.js 1.24 kB
uxdot/uxdot-repo.js 867 B
uxdot/uxdot-sidenav.js 2.04 kB
uxdot/uxdot-spacer-tokens-table.js 2.45 kB
uxdot/uxdot-toc.js 1.89 kB

compressed-size-action

@bennypowers
Copy link
Copy Markdown
Member

@zeroedin really cool! please link to the proposal explainer and to browser standards positions in the description

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 8, 2026

Documentation Health

Module Score
lib/color-palettes.js 268/360 ⚠️ 74%
Overall 268/360 ⚠️ 74%
colorPalettes — 73/90 ✅
Category Score
Element description 8/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
colorPalettes — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
colorPalettes — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0
PaletteController — 65/90 ⚠️
Category Score
Element description 0/25
Attribute documentation 20/20
Slot documentation 15/15
CSS documentation 15/15
Event documentation 15/15
Demos 0/0

Recommendations:

  1. PaletteController: use RFC 2119 keywords (MUST, SHOULD, AVOID) to clarify requirements (Element description, +5 pts)
  2. colorPalettes: add a description to explain what this declaration does (Element description, +5 pts)
  3. colorPalettes: use RFC 2119 keywords (MUST, SHOULD, AVOID) to clarify requirements (Element description, +5 pts)
  4. colorPalettes: describe purpose and context using words like 'for', 'when', 'provides', 'allows' (Element description, +5 pts)
  5. colorPalettes: add a description to explain what this declaration does (Element description, +5 pts)

@bennypowers
Copy link
Copy Markdown
Member

wdyt about outputting each style module in body, just prior to its first use? that might improve streaming performance

@zeroedin
Copy link
Copy Markdown
Collaborator Author

zeroedin commented May 8, 2026

wdyt about outputting each style module in body, just prior to its first use? that might improve streaming performance

that's an interesting thought.

@zeroedin zeroedin changed the title poc: css modules using shadowrootadoptedstylesheets for the Lit SSR pipeline poc: CSS Modules using shadowrootadoptedstylesheets for the Lit SSR pipeline May 8, 2026
@sorvell
Copy link
Copy Markdown

sorvell commented May 9, 2026

Just a heads up in case you don't know this. I noticed that on the home page (both production and preview) that the dsd templates include a significant amount of custom comments: <!-- summary ... -->. According to Claude this is "325 occurrences, totaling 138,174 characters (~135 KB) — about 17% of the file."

@bennypowers
Copy link
Copy Markdown
Member

/agentic_review

Comment thread docs/assets/javascript/dsd-polyfill.js Outdated
@zeroedin
Copy link
Copy Markdown
Collaborator Author

npm run serve server now uses compiled+minified .js files instead of transpiling .ts on-the-fly. This eliminates hydration mismatches caused by the SSR worker and browser seeing different template content (one minified, one not).

What changed:

  • worker.ts: Removed tsx registration; SSR imports compiled .js instead of .ts
  • lit.ts: Removed cleanCompiledJs() — compiled .js files are no longer deleted, they're the source of truth
  • rhds.ts: Removed clean() (which ran tspc -b --clean). Added eleventy.beforeWatch hook that recompiles and re-minifies when .ts/.css files in elements/ change. Added those files as watch
    targets.
  • typescript-assets.ts: npm run serve server serves compiled .js directly instead of esbuild-transpiling .ts. Fails if .js doesn't exist rather than silently falling back.
  • package.json: serve and docs wireit tasks now depend on minify-literals so compiled+minified .js files exist before eleventy starts.

@zeroedin
Copy link
Copy Markdown
Collaborator Author

zeroedin commented May 12, 2026

Update on latest commit:

When the SSR pipeline switched from importing .ts source files (via tsx) to importing pre-compiled .js artifacts, the @sheet: marker system broke. Previously, importing a .ts file would trigger the CSS loader hook (lit-css-node.ts) on each .css import, which injected /* @sheet:rh-card */ markers into the CSS text. The SSR renderer then used those markers to deduplicate styles and hoist them into <head> as shared <style type="module"> blocks.

But the TypeScript compiler's typescript-transform-lit-css plugin (with cssnano: true) inlines CSS directly into the .js output as minified css tagged template literals, stripping all comments and eliminating the .css import entirely. So by the time the SSR worker loads the compiled .js, there are no .css imports for the hook to intercept and no markers to find.

The fix replaces marker-based identification with object identity: a WeakMap tracks each CSSResult instance across components, so shared styles (like color-palette) are deduplicated automatically regardless of which component first uses them. Specifier names are derived from the element's tag name, and shared styles can declare an explicit .specifier property for a readable name (e.g., rhds-color-palette)

@bennypowers
Copy link
Copy Markdown
Member

/agentic_review

@qodo-for-redhat-ux
Copy link
Copy Markdown

qodo-for-redhat-ux Bot commented May 13, 2026

Code Review by Qodo

🐞 Bugs (4) 📘 Rule violations (0)

Grey Divider


Action required

1. Polyfill throws on unsupported APIs 🐞 Bug ☼ Reliability
Description
shadowroot-adopted-stylesheets.js unconditionally uses new CSSStyleSheet(), replaceSync(), and
shadowRoot.adoptedStyleSheets.push(...) without feature-detecting availability, which can throw
and break page rendering on browsers without constructable/adopted stylesheet support.
Code

docs/assets/javascript/shadowroot-adopted-stylesheets.js[R1-33]

+(function() {
+  if ('shadowRootAdoptedStyleSheets' in HTMLTemplateElement.prototype) {
+    return;
+  }
+  const SHEET_ATTR = 'shadowRootAdoptedStyleSheets';
+  const HOST_SEL = `[${SHEET_ATTR}]:not([${SHEET_ATTR}=""])`;
+  const sheets = new Map();
+  const applied = new WeakSet();
+  function collectStyles() {
+    document.querySelectorAll(
+      'style[type="module"][specifier]:not([specifier=""])'
+    ).forEach(function(el) {
+      const spec = el.getAttribute('specifier').trim();
+      if (!sheets.has(spec)) {
+        const s = new CSSStyleSheet();
+        s.replaceSync(el.textContent);
+        sheets.set(spec, s);
+      }
+    });
+  }
+  function applyToHost(el) {
+    if (applied.has(el) || !el.shadowRoot) {
+      return;
+    }
+    applied.add(el);
+    el.shadowRoot.adoptedStyleSheets.push(
+      ...el.getAttribute(SHEET_ATTR).trim().split(/\s+/)
+          .flatMap(function(n) {
+            return sheets.has(n) ? [sheets.get(n)] : [];
+          })
+    );
+    el.shadowRoot.querySelectorAll(HOST_SEL).forEach(applyToHost);
+  }
Evidence
The polyfill always constructs CSSStyleSheet and pushes into adoptedStyleSheets without any guards,
and base.njk loads it for every page.

docs/assets/javascript/shadowroot-adopted-stylesheets.js[1-33]
docs/_includes/layouts/base.njk[11-28]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
`docs/assets/javascript/shadowroot-adopted-stylesheets.js` assumes constructable stylesheets and `adoptedStyleSheets` exist, and will throw when they do not. Because this script is loaded on all pages, a single runtime exception can break SSR/hydration and page scripts.

## Issue Context
- The script constructs `CSSStyleSheet` and calls `replaceSync`, then mutates `shadowRoot.adoptedStyleSheets`.
- It’s included globally in the base layout.

## Fix Focus Areas
- Add explicit feature detection for **both** constructable stylesheet creation (`CSSStyleSheet` + `replaceSync`) and adoption (`ShadowRoot.prototype.adoptedStyleSheets`).
- Provide a safe fallback path when unsupported (e.g., clone `<style type="module" specifier>` contents into each shadowRoot as `<style data-specifier="...">...</style>`), or early-return without throwing.
- Wrap sheet creation/adoption in try/catch to avoid taking down the page.

### Fix Focus Areas (code pointers)
- docs/assets/javascript/shadowroot-adopted-stylesheets.js[1-33]
- docs/_includes/layouts/base.njk[11-28]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


2. ArrayBuffer source corrupted 🐞 Bug ≡ Correctness
Description
The lit-html-minifier-node loader converts ArrayBuffer module sources via .toString(), which
does not decode the buffer contents; if nextLoad() returns an ArrayBuffer, minification will
operate on garbage text and can break SSR module loading.
Code

docs/_plugins/lit-ssr/lit-html-minifier-node.ts[R20-23]

+  const source = typeof result.source === 'string' ?
+    result.source
+    : result.source?.toString();
+  if (!source || !source.includes('html`')) {
Evidence
The hook’s own types allow ArrayBuffer, and the implementation converts it using .toString()
before minifying, which is not a byte decode.

docs/_plugins/lit-ssr/lit-html-minifier-node.ts[4-34]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The ESM loader hook in `docs/_plugins/lit-ssr/lit-html-minifier-node.ts` calls `result.source?.toString()` when `source` is an `ArrayBuffer`. This does not decode bytes into the JavaScript source text and can corrupt the module source passed into `minifyHTMLLiterals()`.

## Issue Context
`HookContext` explicitly allows `source: string | ArrayBuffer`, so this branch is a supported code path.

## Fix Focus Areas
- Convert `ArrayBuffer` to a string with `Buffer.from(arrayBuffer).toString('utf8')` (or `new TextDecoder('utf-8').decode(arrayBuffer)`), not `.toString()`.
- Preserve behavior for string sources.

### Fix Focus Areas (code pointers)
- docs/_plugins/lit-ssr/lit-html-minifier-node.ts[4-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


3. Dev JS handler can throw 🐞 Bug ☼ Reliability
Description
The Eleventy dev server handler for @rhds/elements now reads ${path}.js directly without error
handling; if the compiled JS is missing or stale, readFile() will reject and can crash the request
handler.
Code

docs/_plugins/typescript-assets.ts[R86-94]

+              case 'elements': {
+                const jsPath = join(cwd, `${path}.js`);
+                const body = await readFile(jsPath, 'utf8');
+                return {
+                  body,
+                  status: 200,
+                  headers: { 'Content-Type': 'text/javascript' },
+                };
+              }
Evidence
The new handler reads the JS file without any guard, unlike the existing TS transform helper which
catches and returns a response object on failure.

docs/_plugins/typescript-assets.ts[16-48]
docs/_plugins/typescript-assets.ts[80-102]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
In `docs/_plugins/typescript-assets.ts`, the `@rhds/elements` JS route reads a compiled `.js` file with no try/catch. Any ENOENT/permission/read error will propagate and can break the dev server response path.

## Issue Context
The file already contains `transformTypescriptSource()` with explicit error handling and a structured 500 response.

## Fix Focus Areas
- Wrap the `readFile(jsPath, 'utf8')` call in try/catch.
- Return a 404 when the file does not exist (ENOENT), and/or fall back to `transformTypescriptSource(join(cwd, `${path}.ts`))` when the `.js` is missing.
- Keep response headers consistent.

### Fix Focus Areas (code pointers)
- docs/_plugins/typescript-assets.ts[16-48]
- docs/_plugins/typescript-assets.ts[80-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools



Remediation recommended

4. Brittle template postprocess regex 🐞 Bug ≡ Correctness
Description
postProcessAdoptedStyleSheets() only matches templates that contain shadowroot="..." followed by
shadowrootmode="...", so it likely won’t rewrite common declarative shadow DOM templates that use
only shadowrootmode, leaving the <!--@adopted:...--> marker in output and not emitting
shadowrootadoptedstylesheets on templates.
Code

docs/_plugins/lit-ssr/worker.ts[R184-194]

+  const TEMPLATE_RE = new RegExp(
+    '(<template\\s+shadowroot="[^"]*"\\s+shadowrootmode="[^"]*"'
+      + '(?:\\s+shadowrootdelegatesfocus)?)(>)\\s*'
+      + '<!--@adopted:([\\w -]+)-->',
+    'g'
+  );
+  let processed = html.replace(
+    TEMPLATE_RE,
+    (_, open, close, specs) =>
+      `${open} shadowrootadoptedstylesheets="${specs.trim()}"${close}`
+  );
Evidence
The regex requires shadowroot and a specific ordering, while a declarative shadow DOM example in
the repo uses only shadowrootmode, meaning it would not match this pattern.

docs/_plugins/lit-ssr/worker.ts[179-194]
elements/rh-cta/demo/no-cta-javascript.html[76-79]

Agent prompt
The issue below was found during a code review. Follow the provided context and guidance below and implement a solution

## Issue description
The SSR HTML post-processor uses a strict regex that requires a `shadowroot` attribute and a specific attribute order to inject `shadowrootadoptedstylesheets` onto `<template>` tags. This makes the rewrite fragile and likely to miss templates that only specify `shadowrootmode`.

## Issue Context
There are existing declarative shadow DOM templates in the repo that use `shadowrootmode` without `shadowroot`.

## Fix Focus Areas
- Update the regex to match `<template ... shadowrootmode="..." ...>` regardless of attribute order, and make the `shadowroot="..."` attribute optional.
- Consider parsing with an HTML parser (parse5) instead of regex to avoid attribute-order brittleness.

### Fix Focus Areas (code pointers)
- docs/_plugins/lit-ssr/worker.ts[179-194]
- elements/rh-cta/demo/no-cta-javascript.html[76-79]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools


Grey Divider

Qodo Logo

Comment on lines +1 to +33
(function() {
if ('shadowRootAdoptedStyleSheets' in HTMLTemplateElement.prototype) {
return;
}
const SHEET_ATTR = 'shadowRootAdoptedStyleSheets';
const HOST_SEL = `[${SHEET_ATTR}]:not([${SHEET_ATTR}=""])`;
const sheets = new Map();
const applied = new WeakSet();
function collectStyles() {
document.querySelectorAll(
'style[type="module"][specifier]:not([specifier=""])'
).forEach(function(el) {
const spec = el.getAttribute('specifier').trim();
if (!sheets.has(spec)) {
const s = new CSSStyleSheet();
s.replaceSync(el.textContent);
sheets.set(spec, s);
}
});
}
function applyToHost(el) {
if (applied.has(el) || !el.shadowRoot) {
return;
}
applied.add(el);
el.shadowRoot.adoptedStyleSheets.push(
...el.getAttribute(SHEET_ATTR).trim().split(/\s+/)
.flatMap(function(n) {
return sheets.has(n) ? [sheets.get(n)] : [];
})
);
el.shadowRoot.querySelectorAll(HOST_SEL).forEach(applyToHost);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

1. Polyfill throws on unsupported apis 🐞 Bug ☼ Reliability

shadowroot-adopted-stylesheets.js unconditionally uses new CSSStyleSheet(), replaceSync(), and
shadowRoot.adoptedStyleSheets.push(...) without feature-detecting availability, which can throw
and break page rendering on browsers without constructable/adopted stylesheet support.
Agent Prompt
## Issue description
`docs/assets/javascript/shadowroot-adopted-stylesheets.js` assumes constructable stylesheets and `adoptedStyleSheets` exist, and will throw when they do not. Because this script is loaded on all pages, a single runtime exception can break SSR/hydration and page scripts.

## Issue Context
- The script constructs `CSSStyleSheet` and calls `replaceSync`, then mutates `shadowRoot.adoptedStyleSheets`.
- It’s included globally in the base layout.

## Fix Focus Areas
- Add explicit feature detection for **both** constructable stylesheet creation (`CSSStyleSheet` + `replaceSync`) and adoption (`ShadowRoot.prototype.adoptedStyleSheets`).
- Provide a safe fallback path when unsupported (e.g., clone `<style type="module" specifier>` contents into each shadowRoot as `<style data-specifier="...">...</style>`), or early-return without throwing.
- Wrap sheet creation/adoption in try/catch to avoid taking down the page.

### Fix Focus Areas (code pointers)
- docs/assets/javascript/shadowroot-adopted-stylesheets.js[1-33]
- docs/_includes/layouts/base.njk[11-28]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

these are all baseline

Comment on lines +20 to +23
const source = typeof result.source === 'string' ?
result.source
: result.source?.toString();
if (!source || !source.includes('html`')) {
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

2. Arraybuffer source corrupted 🐞 Bug ≡ Correctness

The lit-html-minifier-node loader converts ArrayBuffer module sources via .toString(), which
does not decode the buffer contents; if nextLoad() returns an ArrayBuffer, minification will
operate on garbage text and can break SSR module loading.
Agent Prompt
## Issue description
The ESM loader hook in `docs/_plugins/lit-ssr/lit-html-minifier-node.ts` calls `result.source?.toString()` when `source` is an `ArrayBuffer`. This does not decode bytes into the JavaScript source text and can corrupt the module source passed into `minifyHTMLLiterals()`.

## Issue Context
`HookContext` explicitly allows `source: string | ArrayBuffer`, so this branch is a supported code path.

## Fix Focus Areas
- Convert `ArrayBuffer` to a string with `Buffer.from(arrayBuffer).toString('utf8')` (or `new TextDecoder('utf-8').decode(arrayBuffer)`), not `.toString()`.
- Preserve behavior for string sources.

### Fix Focus Areas (code pointers)
- docs/_plugins/lit-ssr/lit-html-minifier-node.ts[4-34]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

Comment on lines +86 to +94
case 'elements': {
const jsPath = join(cwd, `${path}.js`);
const body = await readFile(jsPath, 'utf8');
return {
body,
status: 200,
headers: { 'Content-Type': 'text/javascript' },
};
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Action required

3. Dev js handler can throw 🐞 Bug ☼ Reliability

The Eleventy dev server handler for @rhds/elements now reads ${path}.js directly without error
handling; if the compiled JS is missing or stale, readFile() will reject and can crash the request
handler.
Agent Prompt
## Issue description
In `docs/_plugins/typescript-assets.ts`, the `@rhds/elements` JS route reads a compiled `.js` file with no try/catch. Any ENOENT/permission/read error will propagate and can break the dev server response path.

## Issue Context
The file already contains `transformTypescriptSource()` with explicit error handling and a structured 500 response.

## Fix Focus Areas
- Wrap the `readFile(jsPath, 'utf8')` call in try/catch.
- Return a 404 when the file does not exist (ENOENT), and/or fall back to `transformTypescriptSource(join(cwd, `${path}.ts`))` when the `.js` is missing.
- Keep response headers consistent.

### Fix Focus Areas (code pointers)
- docs/_plugins/typescript-assets.ts[16-48]
- docs/_plugins/typescript-assets.ts[80-102]

ⓘ Copy this prompt and use it to remediate the issue with your preferred AI generation tools

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

Labels

None yet

Projects

Status: No status

Development

Successfully merging this pull request may close these issues.

3 participants