diff --git a/apps/tests/package.json b/apps/tests/package.json
index 387c3378c..006f07e82 100644
--- a/apps/tests/package.json
+++ b/apps/tests/package.json
@@ -14,7 +14,7 @@
"test:all": "npm run unit:ci && npm run e2e"
},
"dependencies": {
- "@solidjs/meta": "^0.29.4",
+ "@kobalte/core": "^0.13.11",
"@solidjs/router": "^0.15.3",
"@solidjs/start": "workspace:*",
"@solidjs/testing-library": "^0.8.10",
@@ -24,6 +24,7 @@
"@vitest/ui": "^4.0.10",
"jsdom": "^25.0.1",
"lodash": "^4.17.21",
+ "lucide-solid": "^0.577.0",
"solid-js": "next",
"vite": "^7.1.10",
"vite-plugin-solid": "^3.0.0-next.0",
diff --git a/apps/tests/src/app.css b/apps/tests/src/app.css
index a4d2e55c4..94c75beb9 100644
--- a/apps/tests/src/app.css
+++ b/apps/tests/src/app.css
@@ -1,5 +1,6 @@
body {
- font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
+ font-family:
+ Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif;
}
a {
@@ -59,3 +60,168 @@ p {
.increment:active {
background-color: rgba(68, 107, 158, 0.2);
}
+
+.hydration-scroll-root {
+ color-scheme: light;
+ color: #1e2b21;
+ background: radial-gradient(circle at top, #e8fff5 0%, #f7f6f0 35%, #efe9de 100%);
+}
+
+.hydration-scroll-root a {
+ margin-right: 0;
+}
+
+.hydration-scroll-shell {
+ min-height: 100vh;
+ padding: 16px;
+}
+
+.hydration-scroll-frame {
+ max-width: 960px;
+ margin: 0 auto;
+}
+
+.hydration-scroll-header {
+ position: sticky;
+ top: 12px;
+ z-index: 10;
+ display: flex;
+ flex-direction: column;
+ gap: 12px;
+ padding: 16px;
+ border: 1px solid rgba(30, 43, 33, 0.12);
+ border-radius: 20px;
+ background: rgba(255, 253, 247, 0.9);
+ backdrop-filter: blur(14px);
+ box-shadow: 0 18px 40px rgba(42, 53, 43, 0.1);
+}
+
+.hydration-scroll-header-row {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 16px;
+}
+
+.hydration-scroll-brand {
+ font-size: 0.85rem;
+ font-weight: 700;
+ letter-spacing: 0.26em;
+ text-transform: uppercase;
+}
+
+.hydration-scroll-desktop-nav {
+ display: none;
+ gap: 10px;
+}
+
+.hydration-scroll-desktop-nav a,
+.hydration-scroll-mobile-nav a {
+ padding: 10px 14px;
+ border-radius: 999px;
+ background: rgba(30, 43, 33, 0.06);
+}
+
+.hydration-scroll-trigger {
+ display: inline-flex;
+ align-items: center;
+ gap: 10px;
+ padding: 11px 14px;
+ border: 1px solid rgba(30, 43, 33, 0.15);
+ border-radius: 999px;
+ background: #fff;
+ font-size: 0.75rem;
+ font-weight: 700;
+ letter-spacing: 0.18em;
+ text-transform: uppercase;
+ color: #3a4b40;
+}
+
+.hydration-scroll-trigger-label {
+ display: inline-flex;
+ align-items: center;
+}
+
+.hydration-scroll-chevron {
+ width: 16px;
+ height: 16px;
+ flex: none;
+}
+
+.hydration-scroll-panel {
+ padding-top: 12px;
+ border-top: 1px solid rgba(30, 43, 33, 0.12);
+}
+
+.hydration-scroll-mobile-nav {
+ display: flex;
+ flex-direction: column;
+ gap: 10px;
+}
+
+.hydration-scroll-main {
+ padding: 32px 0 64px;
+}
+
+.hydration-scroll-hero {
+ padding: 24px;
+ border-radius: 28px;
+ background: rgba(255, 253, 247, 0.78);
+ border: 1px solid rgba(30, 43, 33, 0.12);
+}
+
+.hydration-scroll-eyebrow {
+ margin: 0 0 12px;
+ font-size: 0.78rem;
+ font-weight: 700;
+ letter-spacing: 0.22em;
+ text-transform: uppercase;
+ color: #54675b;
+}
+
+.hydration-scroll-hero h1 {
+ margin: 0 0 16px;
+ font-size: clamp(2.4rem, 6vw, 4.6rem);
+ line-height: 0.95;
+}
+
+.hydration-scroll-hero p {
+ margin: 0;
+ max-width: 48rem;
+ line-height: 1.7;
+}
+
+.hydration-scroll-stack {
+ display: grid;
+ gap: 16px;
+ margin-top: 24px;
+}
+
+.hydration-scroll-panel-card {
+ padding: 20px 22px;
+ border-radius: 22px;
+ background: rgba(255, 255, 255, 0.66);
+ border: 1px solid rgba(30, 43, 33, 0.1);
+}
+
+.hydration-scroll-panel-card h2 {
+ margin: 0 0 10px;
+ font-size: 1.1rem;
+}
+
+.hydration-scroll-panel-card p {
+ margin: 0;
+ max-width: none;
+ line-height: 1.6;
+}
+
+@media (min-width: 960px) {
+ .hydration-scroll-desktop-nav {
+ display: flex;
+ }
+
+ .hydration-scroll-trigger,
+ .hydration-scroll-panel {
+ display: none;
+ }
+}
diff --git a/apps/tests/src/app.tsx b/apps/tests/src/app.tsx
index 83532f5ee..0124207fe 100644
--- a/apps/tests/src/app.tsx
+++ b/apps/tests/src/app.tsx
@@ -1,7 +1,7 @@
-import { MetaProvider, Title } from "@solidjs/meta";
import { Router } from "@solidjs/router";
import { FileRoutes } from "@solidjs/start/router";
import { Loading } from "solid-js";
+import { MetaProvider, Title } from "./meta";
import "./app.css";
export default function App() {
@@ -56,6 +56,9 @@ export default function App() {
Text Plain Response
+
+ Hydration Scroll Repro
+
referencing multiple export named functions in the same file
diff --git a/apps/tests/src/e2e/hydration-scroll.test.ts b/apps/tests/src/e2e/hydration-scroll.test.ts
new file mode 100644
index 000000000..1c6fd9419
--- /dev/null
+++ b/apps/tests/src/e2e/hydration-scroll.test.ts
@@ -0,0 +1,41 @@
+import { expect, test } from "@playwright/test";
+
+const isHydrationMismatch = (text: string) =>
+ text.includes("Hydration Mismatch") ||
+ text.includes("Hydration mismatch") ||
+ text.includes("Unable to find DOM nodes for hydration key");
+
+test.describe("SSR Hydration Scroll Repro", () => {
+ test("should not emit hydration mismatches on the first downward scroll", async ({ page }) => {
+ const mismatchMessages: string[] = [];
+
+ page.on("console", msg => {
+ if (msg.type() === "error" || msg.type() === "warning") {
+ const text = msg.text();
+ if (isHydrationMismatch(text)) {
+ mismatchMessages.push(text);
+ }
+ }
+ });
+
+ page.on("pageerror", error => {
+ if (isHydrationMismatch(error.message)) {
+ mismatchMessages.push(error.message);
+ }
+ });
+
+ await page.setViewportSize({ width: 390, height: 844 });
+ await page.goto("/hydration-scroll-repro");
+
+ await expect(page.getByTestId("hydration-scroll-trigger")).toBeVisible();
+ await expect(page.getByTestId("hydration-scroll-chevron")).toBeVisible();
+
+ await page.mouse.wheel(0, 1000);
+ await page.waitForTimeout(250);
+
+ expect(
+ mismatchMessages,
+ "Expected no hydration mismatch after the first downward scroll",
+ ).toEqual([]);
+ });
+});
diff --git a/apps/tests/src/meta.tsx b/apps/tests/src/meta.tsx
new file mode 100644
index 000000000..6c7a87072
--- /dev/null
+++ b/apps/tests/src/meta.tsx
@@ -0,0 +1,20 @@
+import { children, createRenderEffect, type JSX, type ParentProps } from "solid-js";
+
+export function MetaProvider(props: ParentProps) {
+ return props.children;
+}
+
+export function Title(props: { children?: JSX.Element }) {
+ const resolved = children(() => props.children);
+
+ if (!import.meta.env.SSR) {
+ createRenderEffect(() => {
+ const value = resolved.toArray().join("");
+ if (value) {
+ document.title = value;
+ }
+ });
+ }
+
+ return null;
+}
diff --git a/apps/tests/src/routes/[...404].tsx b/apps/tests/src/routes/[...404].tsx
index f1d7221c8..493e40c4b 100644
--- a/apps/tests/src/routes/[...404].tsx
+++ b/apps/tests/src/routes/[...404].tsx
@@ -1,4 +1,4 @@
-import { Title } from "@solidjs/meta";
+import { Title } from "../meta";
import { HttpStatusCode } from "@solidjs/start";
import type { APIEvent } from "@solidjs/start/server";
diff --git a/apps/tests/src/routes/hydration-scroll-repro.tsx b/apps/tests/src/routes/hydration-scroll-repro.tsx
new file mode 100644
index 000000000..8f7945fec
--- /dev/null
+++ b/apps/tests/src/routes/hydration-scroll-repro.tsx
@@ -0,0 +1,109 @@
+import * as Collapsible from "@kobalte/core/collapsible";
+import { A, useLocation } from "@solidjs/router";
+import { ChevronDown } from "lucide-solid";
+import { createEffect, createSignal, For, splitProps, type ParentProps } from "solid-js";
+import { Title } from "../meta";
+
+const navigation = [
+ { href: "/", label: "Home" },
+ { href: "/hydration-scroll-repro", label: "Repro" },
+] as const;
+
+const cards = Array.from({ length: 24 }, (_, index) => index + 1);
+
+function ScrollReproTrigger(
+ props: ParentProps,
+) {
+ const [local, rest] = splitProps(props, ["children", "class"]);
+
+ return (
+
+ {local.children}
+
+
+ );
+}
+
+function ScrollReproContent(
+ props: ParentProps,
+) {
+ const [local, rest] = splitProps(props, ["children", "class"]);
+
+ return (
+
+ {local.children}
+
+ );
+}
+
+export default function HydrationScrollRepro() {
+ const location = useLocation();
+ const [isMobileNavOpen, setIsMobileNavOpen] = createSignal(false);
+
+ createEffect(() => {
+ location.pathname;
+ setIsMobileNavOpen(false);
+ });
+
+ return (
+
+ );
+}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 0cddbd5f1..3a81f986c 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -270,9 +270,9 @@ importers:
apps/tests:
dependencies:
- '@solidjs/meta':
- specifier: ^0.29.4
- version: 0.29.4(solid-js@2.0.0-beta.2)
+ '@kobalte/core':
+ specifier: ^0.13.11
+ version: 0.13.11(solid-js@2.0.0-beta.2)
'@solidjs/router':
specifier: ^0.15.3
version: 0.15.4(solid-js@2.0.0-beta.2)
@@ -300,6 +300,9 @@ importers:
lodash:
specifier: ^4.17.21
version: 4.17.21
+ lucide-solid:
+ specifier: ^0.577.0
+ version: 0.577.0(solid-js@2.0.0-beta.2)
solid-js:
specifier: next
version: 2.0.0-beta.2
@@ -4077,6 +4080,11 @@ packages:
lru-cache@5.1.1:
resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==}
+ lucide-solid@0.577.0:
+ resolution: {integrity: sha512-r/rsauBlyNjFlUhXCkD544tOH1GgcFFupw9oP2zZT4BiFkHoO3MTr12QfKBrS5zCRIhktc/qY2tRr925hFlNuQ==}
+ peerDependencies:
+ solid-js: ^1.4.7
+
luxon@3.6.1:
resolution: {integrity: sha512-tJLxrKJhO2ukZ5z0gyjY1zPh3Rh88Ej9P7jNrZiHMUXHae1yvI2imgOZtL1TO8TW6biMMKfTtAOoEJANgtWBMQ==}
engines: {node: '>=12'}
@@ -6389,11 +6397,21 @@ snapshots:
'@floating-ui/dom': 1.6.11
solid-js: 1.9.9
+ '@corvu/utils@0.2.0(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@floating-ui/dom': 1.6.11
+ solid-js: 2.0.0-beta.2
+
'@corvu/utils@0.4.2(solid-js@1.9.9)':
dependencies:
'@floating-ui/dom': 1.6.11
solid-js: 1.9.9
+ '@corvu/utils@0.4.2(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@floating-ui/dom': 1.6.11
+ solid-js: 2.0.0-beta.2
+
'@dabh/diagnostics@2.0.3':
dependencies:
colorspace: 1.1.4
@@ -6758,6 +6776,18 @@ snapshots:
solid-presence: 0.1.8(solid-js@1.9.9)
solid-prevent-scroll: 0.1.7(solid-js@1.9.9)
+ '@kobalte/core@0.13.11(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@floating-ui/dom': 1.6.11
+ '@internationalized/date': 3.5.4
+ '@internationalized/number': 3.5.3
+ '@kobalte/utils': 0.9.1(solid-js@2.0.0-beta.2)
+ '@solid-primitives/props': 3.1.11(solid-js@2.0.0-beta.2)
+ '@solid-primitives/resize-observer': 2.0.26(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+ solid-presence: 0.1.8(solid-js@2.0.0-beta.2)
+ solid-prevent-scroll: 0.1.7(solid-js@2.0.0-beta.2)
+
'@kobalte/utils@0.9.1(solid-js@1.9.9)':
dependencies:
'@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9)
@@ -6769,6 +6799,17 @@ snapshots:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@kobalte/utils@0.9.1(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2)
+ '@solid-primitives/keyed': 1.2.2(solid-js@2.0.0-beta.2)
+ '@solid-primitives/map': 0.4.11(solid-js@2.0.0-beta.2)
+ '@solid-primitives/media': 2.2.9(solid-js@2.0.0-beta.2)
+ '@solid-primitives/props': 3.1.11(solid-js@2.0.0-beta.2)
+ '@solid-primitives/refs': 1.1.2(solid-js@2.0.0-beta.2)
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@manypkg/find-root@1.1.0':
dependencies:
'@babel/runtime': 7.28.4
@@ -7525,15 +7566,29 @@ snapshots:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/event-listener@2.3.3(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/keyed@1.2.2(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.9
+ '@solid-primitives/keyed@1.2.2(solid-js@2.0.0-beta.2)':
+ dependencies:
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/map@0.4.11(solid-js@1.9.9)':
dependencies:
'@solid-primitives/trigger': 1.0.11(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/map@0.4.11(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/trigger': 1.0.11(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/media@2.2.9(solid-js@1.9.9)':
dependencies:
'@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9)
@@ -7542,16 +7597,34 @@ snapshots:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/media@2.2.9(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2)
+ '@solid-primitives/rootless': 1.4.5(solid-js@2.0.0-beta.2)
+ '@solid-primitives/static-store': 0.0.8(solid-js@2.0.0-beta.2)
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/props@3.1.11(solid-js@1.9.9)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/props@3.1.11(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/refs@1.1.2(solid-js@1.9.9)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/refs@1.1.2(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/resize-observer@2.0.26(solid-js@1.9.9)':
dependencies:
'@solid-primitives/event-listener': 2.3.3(solid-js@1.9.9)
@@ -7560,16 +7633,34 @@ snapshots:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/resize-observer@2.0.26(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/event-listener': 2.3.3(solid-js@2.0.0-beta.2)
+ '@solid-primitives/rootless': 1.4.5(solid-js@2.0.0-beta.2)
+ '@solid-primitives/static-store': 0.0.8(solid-js@2.0.0-beta.2)
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/rootless@1.4.5(solid-js@1.9.9)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/rootless@1.4.5(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/static-store@0.0.8(solid-js@1.9.9)':
dependencies:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/static-store@0.0.8(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/transition-group@1.1.2(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.9
@@ -7579,10 +7670,19 @@ snapshots:
'@solid-primitives/utils': 6.3.2(solid-js@1.9.9)
solid-js: 1.9.9
+ '@solid-primitives/trigger@1.0.11(solid-js@2.0.0-beta.2)':
+ dependencies:
+ '@solid-primitives/utils': 6.3.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
'@solid-primitives/utils@6.3.2(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.9
+ '@solid-primitives/utils@6.3.2(solid-js@2.0.0-beta.2)':
+ dependencies:
+ solid-js: 2.0.0-beta.2
+
'@solidjs/meta@0.29.4(solid-js@1.9.9)':
dependencies:
solid-js: 1.9.9
@@ -9640,6 +9740,10 @@ snapshots:
dependencies:
yallist: 3.1.1
+ lucide-solid@0.577.0(solid-js@2.0.0-beta.2):
+ dependencies:
+ solid-js: 2.0.0-beta.2
+
luxon@3.6.1: {}
lz-string@1.5.0: {}
@@ -10820,11 +10924,21 @@ snapshots:
'@corvu/utils': 0.4.2(solid-js@1.9.9)
solid-js: 1.9.9
+ solid-presence@0.1.8(solid-js@2.0.0-beta.2):
+ dependencies:
+ '@corvu/utils': 0.4.2(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
solid-prevent-scroll@0.1.7(solid-js@1.9.9):
dependencies:
'@corvu/utils': 0.2.0(solid-js@1.9.9)
solid-js: 1.9.9
+ solid-prevent-scroll@0.1.7(solid-js@2.0.0-beta.2):
+ dependencies:
+ '@corvu/utils': 0.2.0(solid-js@2.0.0-beta.2)
+ solid-js: 2.0.0-beta.2
+
solid-refresh@0.6.3(solid-js@1.9.9):
dependencies:
'@babel/generator': 7.28.5