Inlined styles: SSR and client end up with a different CSS cascade, so computed styles change on hydration · Issue #36620 · nuxt/nuxt · GitHub
Skip to content

Inlined styles: SSR and client end up with a different CSS cascade, so computed styles change on hydration #36620

Open

Description

@pont1s

Environment

Operating system macOS 27.0.0
CPU Apple M5 Pro (18 cores)
Node.js version v24.18.0
nuxt/cli version 4.0.1
Package manager pnpm@11.16.0
Nuxt version ^4.6.2
Nitro version -
Builder vite@unknown
Config compatibilityDate, css, devtools
Modules -

Reproduction

https://stackblitz.com/edit/nuxt-starter-jk6q98zq

  1. Run npm run build && npm run preview (dev mode doesn't inline styles).
  2. Open the preview with JS disabled or with CPU throttling, then let it hydrate.
  3. #case-a (component rule vs global CSS) and #case-b (parent vs child root) start at 88px / red and switch to 120px / green on hydration. On 4.5.2 they stay at 120px / green.

The page must be opened without using the browser cache.

Describe the bug

The order of equal-specificity rules coming from different files isn't guaranteed, and this issue doesn't ask for a particular order. It's about consistency: the server-rendered <head> and the <head> after hydration contain the same rules in a different order, so the same page renders differently before and after hydration.

Expected: computed styles are the same before hydration, after hydration and after client-side navigation, whatever order Nuxt picks.

Actual (4.6.2): both elements render at 88px / red from the SSR HTML and switch to 120px / green during hydration, which is a layout shift on every hard load. Client-side navigation renders 120px right away.

Computed height of #case-a / #case-b. "Before hydration" is measured at readyState === 'interactive', before any module script runs, with all stylesheets loaded:

version before hydration after hydration
4.5.2 120px / 120px 120px / 120px
4.6.2 88px / 88px 120px / 120px

<head> (stylesheets only):

<!-- 4.6.2 SSR: the shared chunk's <link> is dropped because its CSS is inlined -->
<style>.banner{background:#2e8b57;height:120px}</style>
<style>.product-card[data-v-84345cbb]{background:#2e8b57;height:120px}</style>
<style>.base-card[data-v-92b00755]{background:tomato;height:88px}</style>
<link rel="stylesheet" href="/_nuxt/entry.CL7L9PkT.css">

<!-- 4.6.2 after hydration: Vite's preload helper appends the chunk CSS to the end of <head> -->
...
<link rel="stylesheet" href="/_nuxt/entry.CL7L9PkT.css">
<link rel="stylesheet" href="/_nuxt/ProductCard.CoJAPe-B.css">

<!-- 4.5.2 SSR: the duplicate <link> was still rendered, so SSR matched the client -->
...
<link rel="stylesheet" href="/_nuxt/entry.CL7L9PkT.css">
<link rel="stylesheet" href="/_nuxt/ProductCard.CoJAPe-B.css">

Every equal-specificity conflict between an inlined rule and a linked rule, or between two inlined rules, can resolve one way in SSR and the other way once the client appends the stylesheet.

History. This isn't new in 4.6, but 4.6 widened it:

Activity

  1. coderabbitai commented on Oct 16, 2026

    @coderabbitai
    Contributor
    ⚠️ Possible Duplicate Issue(s)

    📝 Issue Planner

    Check the box below or use the @coderabbitai plan command to generate an implementation plan and prompts that you can use with your favorite coding assistant.

    • Create Plan

    🧪 Issue enrichment is currently in open beta.

    You can configure auto-planning by selecting labels in the issue_enrichment configuration.

    To disable automatic issue enrichment, add the following to your .coderabbit.yaml:

    issue_enrichment:
      auto_enrich:
        enabled: false

    💬 Have feedback or questions? Drop into our discord!

  2. pont1s commented on Oct 16, 2026

    @pont1s
    Author

    ⚠️ Possible Duplicate Issue(s)
    #33041

    Not a duplicate of #33041. That issue covers parent vs child order of inlined <style> tags, which is case B here (still open for children in shared chunks).

    The main part here is case A: in 4.6, inlined component styles render before <link rel="stylesheet"> (global CSS), and #36056 / #36060 drop the duplicate chunk link that kept SSR and client in sync. Vite appends it on hydration, so any equal-specificity override of global CSS flips. Stable on 4.5.2, flips on 4.6.2.

    This isn't a request for a specific order, only for SSR and the hydrated client to resolve the same cascade.

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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions