-
Notifications
You must be signed in to change notification settings - Fork 0
theme.hydration theme mismatch
Rule ID:
theme.hydration-theme-mismatchSeverity:WARNCategory:themeTarget Standards: W3C Web Performance Working Group (Core Web Vitals FOUC Prevention), React 18/19 Hydration Boundary Specification, Astro SSR Zero-JS Script Tag Standards
Detects SSR root layouts lacking blocking inline script for theme initialization
"Root SSR document layouts () must include a render-blocking inline theme script to resolve theme state before first paint and prevent theme FOUC."
In Server-Side Rendered (SSR) architectures (such as Astro, Next.js, or Remix):
- Flash of Unstyled Theme (FOUC): If theme detection runs only after deferred client hydration (e.g. inside useEffect), the browser paints a blinding white default page before snapping jarringly to dark mode.
- React Hydration Mismatch: Inconsistent theme attributes between server-rendered HTML and client hydration trigger React warning cascades and forced DOM re-mounts.
- Cumulative Layout Shift (CLS): Font, border, or icon shifts caused by late theme flipping harm Core Web Vitals.
Charites enforces placing an inline render-blocking theme initialization script directly in the SSR root
.| Risk Vector | Severity | Impact |
|---|---|---|
| Theme FOUC Glare | HIGH | Users in dark environments experience a painful full-screen white flash on every page navigation. |
| Hydration Error Cascade | MEDIUM | React discards server-rendered DOM nodes upon encountering mismatched class attributes, increasing TTI. |
<html>
<head>
<meta charset="utf-8" />
<title>Application</title>
</head>
<body>
<slot />
</body>
</html><head>
<meta charSet="utf-8" />
<title>Dashboard</title>
</head><head>
<meta charset="utf-8" />
<script is:inline>
const theme = localStorage.getItem('theme') || (window.matchMedia('(prefers-color-scheme: dark)').matches ? 'dark' : 'light');
document.documentElement.classList.toggle('dark', theme === 'dark');
</script>
</head><head>
<script
dangerouslySetInnerHTML={{
__html: "document.documentElement.classList.add(localStorage.getItem('theme') || 'light');",
}}
/>
</head>This theme rule evaluates source templates against the project's design token graph:
flowchart TD
Node["AST Node (Astro / TSX element)"] --> Extract["1. Extract Class Names (e.g. 'hover:bg-primary/10')"]
Extract --> Strip["2. Strip Variants (hover:, dark:, sm:) -> 'bg-primary/10'"]
Strip --> Split["3. Split Opacity Modifier -> Utility: 'bg-primary', Opacity: '/10'"]
Split --> Convention["4. Query TokenConvention (Candidate: '--color-primary-light')"]
Convention --> GraphQuery{"5. Check Token Graph (Does token exist in active scope?)"}
GraphQuery -- "No (Banana Test)" --> Safe["Pass (Valid Custom / Untokenized Color)"]
GraphQuery -- "Yes (Official Token Exists)" --> IgnoreCheck{"6. Check charites:ignore directive"}
IgnoreCheck -- "Ignored" --> Safe
IgnoreCheck -- "Not Ignored" --> Diag["7. Emit Diagnostic: theme.hydration-theme-mismatch with Replacement Suggestion"]
-
AST Node Traversal:
internal/analyzerstreams JSX/Astro AST elements to the rule'sEvaluatevisitor. -
Variant Normalization: Strips responsive (
sm:,md:), interaction state (hover:,focus:), and theme (dark:) prefixes to isolate the core utility class. - Modifier Extraction: Parses utility segments and extracts slash opacity modifiers.
-
Token Convention Resolution: Consults the
TokenConventionadapter to determine the official semantic design token replacement candidate. -
Token Graph Verification (Banana Test): Queries
token.Contextto verify that the candidate token is declared inglobal.cssortokens.jsonwithin the element's scope. If not declared, the custom value is permitted without a false-positive diagnostic. -
Directive Suppression Check: Inspects preceding AST comments for
charites:ignore theme.hydration-theme-mismatch. - Diagnostic Emission: Produces a structured diagnostic with line number, column span, and actionable replacement suggestion.
This rule is rigorously tested and validated across the canonical 1-SSOT Tri-Corpus in tests/correctness/theme.hydration-theme-mismatch/:
flowchart TD
subgraph GoldenCorpus ["1-SSOT Tri-Corpus Test Matrix for theme.hydration-theme-mismatch"]
subgraph P ["Positive Corpus (tests/correctness/theme.hydration-theme-mismatch/positive/)"]
P1["P1: Obvious Direct Violation"]
P2["P2: Indirect / Variant Concatenation"]
P3["P3: Helper / clsx / cn Wrapper"]
P4["P4: Deeply Nested Elements"]
P5["P5: Aliased Imports / Re-exports"]
end
subgraph N ["Negative Corpus (tests/correctness/theme.hydration-theme-mismatch/negative/)"]
N1["N1: Valid Design Tokens"]
N2["N2: Explicit charites:ignore Directive"]
N3["N3: Third-Party / Vendor Components"]
N4["N4: Clean Semantic HTML"]
N5["N5: Untokenized Custom Values (Banana Test)"]
end
subgraph A ["Adversarial Corpus (tests/correctness/theme.hydration-theme-mismatch/adversarial/)"]
A1["A1: Template Literal Interpolations"]
A2["A2: Ternary Conditional Expressions"]
A3["A3: Spread Properties & Dynamic Overrides"]
A4["A4: Dynamic Object Class Syntax"]
A5["A5: Shadowed Variable Identifiers"]
A6["A6: Nested Closures & HOC Wrappers"]
A7["A7: Obfuscated Classes & Cyclic Tokens"]
end
end
P --> TestRunner["Automated Runner (rule_test.go)"]
N --> TestRunner
A --> TestRunner
TestRunner --> Gates["Quality Gates: Zero Panic, Zero False-Positive, Zero Bypass"]
- Positive Fixtures (P1-P5): Verified to trigger diagnostics at exact lines and column spans.
- Negative Fixtures (N1-N5): Verified to produce zero diagnostics on valid tokens and legitimate exemptions.
- Adversarial Fixtures (A1-A7): Verified to prevent evasion across dynamic expressions, string interpolations, and cyclic references.
If this pattern is required for an intentional exception, suppress the diagnostic using the canonical Charites Rule ID:
<!-- charites:ignore theme.hydration-theme-mismatch intentional exception -->// charites:ignore theme.hydration-theme-mismatch intentional exceptionrules:
theme.hydration-theme-mismatch:
severity: warn # error | warn | info | offA11y (16 rules)
- A11y Overview
a11y.button-type-missinga11y.dialog-missing-ariaa11y.empty-interactivea11y.error-not-announceda11y.form-input-missing-namea11y.form-label-composite-controla11y.form-label-missing-controla11y.img-missing-alta11y.input-cramped-paddinga11y.input-ios-zoom-hazarda11y.keyboard-trap-missing-escapea11y.label-missing-controla11y.missing-focus-ringa11y.placeholder-as-labela11y.touch-target-sizea11y.touch-target-spacing
Browser (12 rules)
- Browser Overview
browser.appearance-native-overridebrowser.chrome-only-apibrowser.date-input-format-assumptionbrowser.experimental-api-no-featuredetectbrowser.firefox-only-apibrowser.hover-only-interactionbrowser.non-passive-scroll-listenerbrowser.obsolete-vendor-prefixbrowser.safari-only-apibrowser.scrollbar-vendor-incompletebrowser.user-agent-sniffingbrowser.webkit-only-api
Cls (16 rules)
- Cls Overview
cls.client-only-hydration-popcls.collapsible-height-jumpcls.dynamic-content-without-reserved-spacecls.dynamic-table-reflowcls.font-display-missingcls.font-import-late-discoverycls.layout-trigger-animationcls.layout-trigger-transitioncls.text-icon-late-reflowcls.unadjusted-font-metriccls.unconstrained-carouselcls.unreserved-ad-containercls.unreserved-fixed-headercls.unsized-embed-framecls.unsized-imagecls.unstable-scrollbar-gutter
Design (1 rules)
Ergonomy (5 rules)
Inp (16 rules)
- Inp Overview
inp.context-re-render-cascadeinp.expensive-render-computationinp.expensive-style-mutationinp.heavy-event-handlerinp.hydration-contentioninp.hydration-heavy-islandinp.large-interaction-layout-scopeinp.layout-thrashinginp.missing-start-transitioninp.missing-touch-actioninp.render-blocking-scriptinp.repeated-state-updateinp.sync-layout-effectinp.unbounded-collection-renderinp.unbounded-effect-depsinp.unyielded-long-task
Lcp (16 rules)
- Lcp Overview
lcp.blocked-critical-fontlcp.client-only-lcp-contentlcp.critical-head-style-bloatlcp.external-font-discovery-delaylcp.heavy-raster-lcp-assetlcp.image-source-density-mismatchlcp.lazy-loaded-lcp-imagelcp.lcp-content-visibility-suppressionlcp.legacy-critical-font-resourcelcp.missing-critical-origin-hintlcp.missing-lcp-image-preloadlcp.oversized-lcp-resource-selectionlcp.preload-font-cors-mismatchlcp.render-blocking-head-scriptlcp.undiscoverable-lcp-imagelcp.unhinted-lcp-image-priority
Mobile (5 rules)
Performance (16 rules)
- Performance Overview
performance.astro-island-boundary-overlapperformance.astro-over-prefetchingperformance.astro-unnecessary-client-directiveperformance.astro-unoptimized-local-imageperformance.react-context-domain-couplingperformance.react-derived-state-in-effectperformance.react-effect-missing-cleanupperformance.react-index-as-keyperformance.react-inline-prop-memoperformance.react-redundant-function-memoizationperformance.react-static-heavy-importperformance.react-unstable-hook-referenceperformance.tailwind-duplicate-arbitrary-rulesperformance.tailwind-duplicate-utility-definitionperformance.tailwind-dynamic-class-concatenationperformance.tailwind-untracked-package-source
Pwa (10 rules)
- Pwa Overview
pwa.apple-meta-missingpwa.icon-maskable-missingpwa.insecure-context-resourcepwa.manifest-missingpwa.manifest-required-fields-missingpwa.pwa-cache-runtime-api-riskpwa.service-worker-missingpwa.service-worker-no-offline-fallbackpwa.service-worker-registrationpwa.start-url-inconsistency
Responsive (18 rules)
- Responsive Overview
responsive.aspect-ratio-overflowresponsive.container-overconstraintresponsive.desktop-only-contentresponsive.dynamic-viewport-inconsistencyresponsive.fixed-width-overflowresponsive.flex-child-overflowresponsive.fractional-width-gap-driftresponsive.grid-min-columnresponsive.horizontal-overflowresponsive.image-overflowresponsive.keyboard-obstructionresponsive.missing-breakpointresponsive.mobile-density-overloadresponsive.mobile-text-overflowresponsive.safe-area-missingresponsive.unwrapped-table-overflowresponsive.viewport-meta-missingresponsive.viewport-unit-leak
Semantic (1 rules)
Theme (32 rules)
- Theme Overview
theme.apply-bloattheme.backdrop-blur-hardcodetheme.chart-color-hardcodetheme.dual-strategy-collisiontheme.dynamic-classtheme.focus-ring-hardcodetheme.gradient-hardcodetheme.hardcode-border-colortheme.hardcode-border-radiustheme.hardcode-colortheme.hardcode-monochrometheme.hardcode-opacity-colortheme.hardcode-shadow-colortheme.hardcode-sizetheme.hardcode-z-indextheme.hydration-theme-mismatchtheme.image-theme-hardcodetheme.important-overridetheme.inline-style-hardcodetheme.meta-theme-color-mismatchtheme.missing-color-schemetheme.missing-token-fallbacktheme.nested-opacity-contrasttheme.no-reduced-motiontheme.primitive-in-componenttheme.pseudo-hardcode-colortheme.shadow-without-border-darktheme.split-theme-statetheme.svg-hardcode-filltheme.token-source-drifttheme.unlayered-token-definitiontheme.unpaired-dark-variant
Ux (20 rules)
- Ux Overview
ux.camouflaged-linkux.competing-primary-ctaux.destructive-action-unconfirmedux.disabled-control-no-explanationux.empty-collection-unhandledux.missing-autofillux.monolithic-form-bloatux.multiline-input-misuseux.nav-overflow-chunkingux.number-input-identity-misuseux.number-input-missing-boundsux.orphaned-error-stateux.radio-overchoiceux.silent-catch-swallowux.spacing-inversionux.spacing-rhythm-driftux.submit-feedback-missingux.unbounded-async-flagux.unconventional-home-linkux.unthrottled-input-handler