diff --git a/docs/SSR.md b/docs/SSR.md
new file mode 100644
index 00000000..afd38dce
--- /dev/null
+++ b/docs/SSR.md
@@ -0,0 +1,186 @@
+# Server-Side Rendering (SSR) Compatibility Guide
+
+This guide explains how to safely load ELIXIR Cloud Components in projects that use SSR frameworks (e.g. Next.js, Remix).
+
+## Quick Start
+
+All components are **safe to import during SSR** — importing them on the server will not crash. However, Web Components are a **client-side technology**: rendering and hydration occur in the browser via the Custom Elements API.
+
+### Next.js (App Router)
+
+Use dynamic imports with `ssr: false` to load components only on the client:
+
+```tsx
+// app/components/MyComponent.tsx
+"use client";
+
+import dynamic from "next/dynamic";
+
+// Dynamic import prevents SSR from trying to define custom elements
+const EccComponents = dynamic(
+ () => import("@elixir-cloud/design").then((mod) => mod),
+ { ssr: false }
+);
+
+export default function MyComponent() {
+ return (
+
+ Click me
+
+ );
+}
+```
+
+Alternatively, mark the entire page as a Client Component:
+
+```tsx
+// app/page.tsx
+"use client";
+
+import "@elixir-cloud/design";
+
+export default function Page() {
+ return Click me;
+}
+```
+
+### Remix
+
+Use `ClientOnly` wrappers or lazy loading:
+
+```tsx
+import { ClientOnly } from "remix-utils/client-only";
+
+export default function Page() {
+ return (
+ Loading...}>
+ {() => {
+ import("@elixir-cloud/design");
+ return Click me;
+ }}
+
+ );
+}
+```
+
+---
+
+## How SSR Safety Works
+
+### Environment Guards
+
+Every `customElements.define()` call is wrapped in an environment check:
+
+```ts
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("tag-name")
+) {
+ window.customElements.define("tag-name", ComponentClass);
+}
+```
+
+This prevents:
+- **Server crashes** — `customElements` doesn't exist in Node.js
+- **Duplicate registrations** — safe for HMR and multiple imports
+
+### Deterministic IDs
+
+Components that need unique IDs (e.g., ``) use a monotonic counter instead of `Math.random()`. This avoids mismatches if the same module is loaded on both server and client.
+
+### Browser-Only Lifecycle
+
+All DOM access (`document`, `MutationObserver`, `window.setInterval`) is deferred to client-only Lit lifecycle hooks:
+
+| API | Used In | Lifecycle Hook |
+|-----|---------|----------------|
+| `document.addEventListener` | Select, MultiSelect | `connectedCallback()` |
+| `MutationObserver` | Code editor | `firstUpdated()` |
+| `document.documentElement` | Code editor | `isDarkMode()` (guarded) |
+| `window.setInterval` | Select, Collapsible sub-components | `connectedCallback()` |
+| `ace.edit()` | Code editor | `firstUpdated()` |
+
+These hooks only execute in the browser, so importing during SSR never triggers them.
+
+---
+
+## SSR Utility API
+
+The `@elixir-cloud/design` package exports SSR helpers:
+
+```ts
+import {
+ isBrowser,
+ isServer,
+ ssrSafeDefine,
+ generateDeterministicId,
+ resetIdCounter,
+} from "@elixir-cloud/design";
+```
+
+| Function | Description |
+|----------|-------------|
+| `isBrowser()` | Returns `true` in browser environments |
+| `isServer()` | Returns `true` in Node.js / SSR |
+| `ssrSafeDefine(tag, ctor)` | Safely registers a custom element |
+| `generateDeterministicId(prefix)` | Counter-based ID generation |
+| `resetIdCounter()` | Resets the ID counter (for testing) |
+
+---
+
+## Declarative Shadow DOM (DSD)
+
+Lit has experimental support for Declarative Shadow DOM via `@lit-labs/ssr`. DSD allows a server response to include shadow roots as HTML so the browser can display styled content before JavaScript loads:
+
+```html
+
+
+
+
+
+ Click me
+
+```
+
+### Current Status
+
+- **DSD-based server rendering is not yet implemented** in this library
+- Components are safe to **import** on the server (no crashes)
+- Rendering and hydration happen on the client via dynamic imports
+- Future: integrate `@lit-labs/ssr` for server-side template rendering
+
+> **Note:** DSD requires browser support (`template[shadowrootmode]`). Chrome, Edge, and Firefox 123+ support it natively. Safari 16.4+ supports the older `shadowroot` attribute.
+
+---
+
+## Constraints & Best Practices
+
+1. **Always use `"use client"` or dynamic imports** — Web Components need the browser DOM to render
+2. **Don't access `this.shadowRoot` in constructors** — defer to `connectedCallback()` or `firstUpdated()`
+3. **Avoid `Math.random()` in component state** — use `generateDeterministicId()` instead
+4. **Don't rely on `window` or `document` at module scope** — gate behind `isBrowser()`
+5. **Test SSR compatibility** — run `node -e "import('@elixir-cloud/design')"` to verify no crashes
+
+---
+
+## Troubleshooting
+
+### `ReferenceError: customElements is not defined`
+You're importing a component at the module level on the server. Use dynamic imports or `"use client"`.
+
+### Hydration mismatch warnings
+Check for `Math.random()` or `Date.now()` in component state. Use deterministic values instead.
+
+### Flash of Unstyled Content (FOUC)
+Add CSS to hide undefined custom elements until they upgrade:
+
+```css
+:not(:defined) {
+ visibility: hidden;
+}
+```
+
+
+
+
diff --git a/packages/ecc-client-elixir-cloud-registry/src/components/service-create/index.ts b/packages/ecc-client-elixir-cloud-registry/src/components/service-create/index.ts
index dbfdb0bb..f450a6cc 100644
--- a/packages/ecc-client-elixir-cloud-registry/src/components/service-create/index.ts
+++ b/packages/ecc-client-elixir-cloud-registry/src/components/service-create/index.ts
@@ -3,10 +3,17 @@ import ECCClientElixirCloudRegistryServiceCreate from "./service-create.js";
export * from "./service-create.js";
export default ECCClientElixirCloudRegistryServiceCreate;
-window.customElements.define(
- "ecc-client-elixir-cloud-registry-service-create",
- ECCClientElixirCloudRegistryServiceCreate
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-elixir-cloud-registry-service-create")
+) {
+ window.customElements.define(
+ "ecc-client-elixir-cloud-registry-service-create",
+ ECCClientElixirCloudRegistryServiceCreate
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-elixir-drs-filer/src/components/object-create/index.ts b/packages/ecc-client-elixir-drs-filer/src/components/object-create/index.ts
index c2ab07b4..6769a6db 100644
--- a/packages/ecc-client-elixir-drs-filer/src/components/object-create/index.ts
+++ b/packages/ecc-client-elixir-drs-filer/src/components/object-create/index.ts
@@ -2,10 +2,17 @@ import ECCClientElixirDrsFilerObjectCreate from "./object-create.js";
export * from "./object-create.js";
-window.customElements.define(
- "ecc-client-elixir-drs-filer-object-create",
- ECCClientElixirDrsFilerObjectCreate
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-elixir-drs-filer-object-create")
+) {
+ window.customElements.define(
+ "ecc-client-elixir-drs-filer-object-create",
+ ECCClientElixirDrsFilerObjectCreate
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-elixir-ro-crate/src/components/about/index.ts b/packages/ecc-client-elixir-ro-crate/src/components/about/index.ts
index d9f21492..df923193 100644
--- a/packages/ecc-client-elixir-ro-crate/src/components/about/index.ts
+++ b/packages/ecc-client-elixir-ro-crate/src/components/about/index.ts
@@ -3,10 +3,17 @@ import ECCClientRoCrateAbout from "./about.js";
export * from "./about.js";
export default ECCClientRoCrateAbout;
-window.customElements.define(
- "ecc-client-elixir-ro-crate-about",
- ECCClientRoCrateAbout
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-elixir-ro-crate-about")
+) {
+ window.customElements.define(
+ "ecc-client-elixir-ro-crate-about",
+ ECCClientRoCrateAbout
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-elixir-trs-filer/src/components/tool-create/index.ts b/packages/ecc-client-elixir-trs-filer/src/components/tool-create/index.ts
index 880d876e..5bc2ec34 100644
--- a/packages/ecc-client-elixir-trs-filer/src/components/tool-create/index.ts
+++ b/packages/ecc-client-elixir-trs-filer/src/components/tool-create/index.ts
@@ -3,10 +3,17 @@ import ECCClientElixirTrsToolCreate from "./tool-create.js";
export * from "./tool-create.js";
export default ECCClientElixirTrsToolCreate;
-window.customElements.define(
- "ecc-client-elixir-trs-tool-create",
- ECCClientElixirTrsToolCreate
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-elixir-trs-tool-create")
+) {
+ window.customElements.define(
+ "ecc-client-elixir-trs-tool-create",
+ ECCClientElixirTrsToolCreate
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-drs/src/components/object/index.ts b/packages/ecc-client-ga4gh-drs/src/components/object/index.ts
index 62213f1b..c97852db 100644
--- a/packages/ecc-client-ga4gh-drs/src/components/object/index.ts
+++ b/packages/ecc-client-ga4gh-drs/src/components/object/index.ts
@@ -2,8 +2,17 @@ import ECCClientGa4ghDrsObject from "./object.js";
export * from "./object.js";
-// Define the custom element
-customElements.define("ecc-client-ga4gh-drs-object", ECCClientGa4ghDrsObject);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-drs-object")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-drs-object",
+ ECCClientGa4ghDrsObject
+ );
+}
export default ECCClientGa4ghDrsObject;
diff --git a/packages/ecc-client-ga4gh-drs/src/components/objects-list/index.ts b/packages/ecc-client-ga4gh-drs/src/components/objects-list/index.ts
index 5a1f7d9e..dec95e01 100644
--- a/packages/ecc-client-ga4gh-drs/src/components/objects-list/index.ts
+++ b/packages/ecc-client-ga4gh-drs/src/components/objects-list/index.ts
@@ -3,10 +3,17 @@ import ECCClientGa4ghDrsObjects from "./objects.js";
export * from "./objects.js";
export default ECCClientGa4ghDrsObjects;
-window.customElements.define(
- "ecc-client-ga4gh-drs-objects",
- ECCClientGa4ghDrsObjects
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-drs-objects")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-drs-objects",
+ ECCClientGa4ghDrsObjects
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-service-registry/src/components/service/index.ts b/packages/ecc-client-ga4gh-service-registry/src/components/service/index.ts
index 0f798bc5..be93c811 100644
--- a/packages/ecc-client-ga4gh-service-registry/src/components/service/index.ts
+++ b/packages/ecc-client-ga4gh-service-registry/src/components/service/index.ts
@@ -2,11 +2,17 @@ import ECCClientGa4ghServiceRegistryService from "./service.js";
export * from "./service.js";
-// Define the custom element
-customElements.define(
- "ecc-client-ga4gh-service-registry-service",
- ECCClientGa4ghServiceRegistryService
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-service-registry-service")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-service-registry-service",
+ ECCClientGa4ghServiceRegistryService
+ );
+}
export default ECCClientGa4ghServiceRegistryService;
diff --git a/packages/ecc-client-ga4gh-service-registry/src/components/services/index.ts b/packages/ecc-client-ga4gh-service-registry/src/components/services/index.ts
index ba65da33..6bcd38b1 100644
--- a/packages/ecc-client-ga4gh-service-registry/src/components/services/index.ts
+++ b/packages/ecc-client-ga4gh-service-registry/src/components/services/index.ts
@@ -3,10 +3,17 @@ import { ECCClientGa4ghServiceRegistryServices } from "./services.js";
export * from "./services.js";
export default ECCClientGa4ghServiceRegistryServices;
-window.customElements.define(
- "ecc-client-ga4gh-service-registry-services",
- ECCClientGa4ghServiceRegistryServices
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-service-registry-services")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-service-registry-services",
+ ECCClientGa4ghServiceRegistryServices
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-tes/src/components/create-run/index.ts b/packages/ecc-client-ga4gh-tes/src/components/create-run/index.ts
index 5c633d03..0b399133 100644
--- a/packages/ecc-client-ga4gh-tes/src/components/create-run/index.ts
+++ b/packages/ecc-client-ga4gh-tes/src/components/create-run/index.ts
@@ -3,10 +3,17 @@ import ECCCLientGa4ghTesCreateRun from "./create-run.js";
export * from "./create-run.js";
export default ECCCLientGa4ghTesCreateRun;
-window.customElements.define(
- "ecc-client-ga4gh-tes-create-run",
- ECCCLientGa4ghTesCreateRun
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-tes-create-run")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-tes-create-run",
+ ECCCLientGa4ghTesCreateRun
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-tes/src/components/runs/index.ts b/packages/ecc-client-ga4gh-tes/src/components/runs/index.ts
index b7fa4234..9573a7a8 100644
--- a/packages/ecc-client-ga4gh-tes/src/components/runs/index.ts
+++ b/packages/ecc-client-ga4gh-tes/src/components/runs/index.ts
@@ -3,10 +3,17 @@ import ECCClientGa4ghTesRuns from "./runs.js";
export * from "./runs.js";
export default ECCClientGa4ghTesRuns;
-window.customElements.define(
- "ecc-client-ga4gh-tes-runs",
- ECCClientGa4ghTesRuns
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-tes-runs")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-tes-runs",
+ ECCClientGa4ghTesRuns
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-trs/src/components/tool/index.ts b/packages/ecc-client-ga4gh-trs/src/components/tool/index.ts
index e5f43ad9..fec3a7f2 100644
--- a/packages/ecc-client-ga4gh-trs/src/components/tool/index.ts
+++ b/packages/ecc-client-ga4gh-trs/src/components/tool/index.ts
@@ -2,8 +2,17 @@ import ECCClientGa4ghTrsTool from "./tool.js";
export * from "./tool.js";
-// Define the custom element
-customElements.define("ecc-client-ga4gh-trs-tool", ECCClientGa4ghTrsTool);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-trs-tool")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-trs-tool",
+ ECCClientGa4ghTrsTool
+ );
+}
export default ECCClientGa4ghTrsTool;
diff --git a/packages/ecc-client-ga4gh-trs/src/components/tools/index.ts b/packages/ecc-client-ga4gh-trs/src/components/tools/index.ts
index a04c2b36..feaf91a4 100644
--- a/packages/ecc-client-ga4gh-trs/src/components/tools/index.ts
+++ b/packages/ecc-client-ga4gh-trs/src/components/tools/index.ts
@@ -3,10 +3,17 @@ import ECCClientGa4ghTrsTools from "./tools.js";
export * from "./tools.js";
export default ECCClientGa4ghTrsTools;
-window.customElements.define(
- "ecc-client-ga4gh-trs-tools",
- ECCClientGa4ghTrsTools
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-trs-tools")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-trs-tools",
+ ECCClientGa4ghTrsTools
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-wes/src/components/run-create/index.ts b/packages/ecc-client-ga4gh-wes/src/components/run-create/index.ts
index bef29b6c..ad473e6d 100644
--- a/packages/ecc-client-ga4gh-wes/src/components/run-create/index.ts
+++ b/packages/ecc-client-ga4gh-wes/src/components/run-create/index.ts
@@ -3,11 +3,17 @@ import ECCClientGa4ghWesRunCreate from "./run-create.js";
export * from "./run-create.js";
export default ECCClientGa4ghWesRunCreate;
-// Define the custom element
-customElements.define(
- "ecc-client-ga4gh-wes-run-create",
- ECCClientGa4ghWesRunCreate
-);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-wes-run-create")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-wes-run-create",
+ ECCClientGa4ghWesRunCreate
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-wes/src/components/run/index.ts b/packages/ecc-client-ga4gh-wes/src/components/run/index.ts
index 146ea718..086296ad 100644
--- a/packages/ecc-client-ga4gh-wes/src/components/run/index.ts
+++ b/packages/ecc-client-ga4gh-wes/src/components/run/index.ts
@@ -3,8 +3,17 @@ import ECCClientGa4ghWesRun from "./run.js";
export * from "./run.js";
export default ECCClientGa4ghWesRun;
-// Define the custom element
-customElements.define("ecc-client-ga4gh-wes-run", ECCClientGa4ghWesRun);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-wes-run")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-wes-run",
+ ECCClientGa4ghWesRun
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-client-ga4gh-wes/src/components/runs/index.ts b/packages/ecc-client-ga4gh-wes/src/components/runs/index.ts
index ddd1d9b3..1591990f 100644
--- a/packages/ecc-client-ga4gh-wes/src/components/runs/index.ts
+++ b/packages/ecc-client-ga4gh-wes/src/components/runs/index.ts
@@ -3,8 +3,17 @@ import ECCClientGa4ghWesRuns from "./runs.js";
export * from "./runs.js";
export default ECCClientGa4ghWesRuns;
-// Define the custom element
-customElements.define("ecc-client-ga4gh-wes-runs", ECCClientGa4ghWesRuns);
+// SSR guard: customElements is not available in Node.js
+if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get("ecc-client-ga4gh-wes-runs")
+) {
+ window.customElements.define(
+ "ecc-client-ga4gh-wes-runs",
+ ECCClientGa4ghWesRuns
+ );
+}
declare global {
interface HTMLElementTagNameMap {
diff --git a/packages/ecc-utils-design/src/components/code/code.ts b/packages/ecc-utils-design/src/components/code/code.ts
index 7b550673..81f55f91 100644
--- a/packages/ecc-utils-design/src/components/code/code.ts
+++ b/packages/ecc-utils-design/src/components/code/code.ts
@@ -2,6 +2,7 @@ import { LitElement, html, css } from "lit";
import { property, state } from "lit/decorators.js";
import { ComponentStyles as TailwindStyles } from "./tw-styles.js";
import { GlobalStyles } from "../../global.js";
+import { isBrowser } from "../../ssr.js";
import "ace-builds/src-noconflict/ace.js";
import "ace-builds/src-noconflict/theme-cloud9_day.js";
import "ace-builds/src-noconflict/theme-cloud9_night.js";
@@ -646,6 +647,10 @@ export class EccUtilsDesignCode extends LitElement {
}
setupThemeObserver() {
+ // Guard: MutationObserver and document are not available on the server or in some
+ // constrained environments
+ if (!isBrowser() || typeof MutationObserver === "undefined") return;
+
// Set up mutation observer to watch for theme changes
const observer = new MutationObserver(() => {
this.updateEditorTheme();
@@ -664,6 +669,9 @@ export class EccUtilsDesignCode extends LitElement {
}
isDarkMode() {
+ // SSR guard: document is not available on the server
+ if (!isBrowser()) return false;
+
return (
document.documentElement.classList.contains("dark") ||
document.body.classList.contains("dark") ||
diff --git a/packages/ecc-utils-design/src/components/collapsible/collapsible.ts b/packages/ecc-utils-design/src/components/collapsible/collapsible.ts
index a94013f4..8d3b08e0 100644
--- a/packages/ecc-utils-design/src/components/collapsible/collapsible.ts
+++ b/packages/ecc-utils-design/src/components/collapsible/collapsible.ts
@@ -2,6 +2,7 @@ import { LitElement, html, css, PropertyValues } from "lit";
import { property, state } from "lit/decorators.js";
import { ComponentStyles as TailwindStyles } from "./tw-styles.js";
import { GlobalStyles } from "../../global.js";
+import { generateDeterministicId } from "../../ssr.js";
// Global state manager for collapsible
const collapsibleState = new Map<
@@ -24,9 +25,8 @@ export class EccUtilsDesignCollapsible extends LitElement {
`,
];
- @state() private collapsibleId = `collapsible-${Math.random()
- .toString(36)
- .substring(2, 9)}`;
+ // SSR-safe: deterministic IDs prevent hydration mismatches between server and client
+ @state() private collapsibleId = generateDeterministicId("collapsible");
@property({ type: Boolean }) open = false;
@property({ type: Boolean }) disabled = false;
diff --git a/packages/ecc-utils-design/src/components/multi-select/multi-select.ts b/packages/ecc-utils-design/src/components/multi-select/multi-select.ts
index 2f8b228d..ddb76553 100644
--- a/packages/ecc-utils-design/src/components/multi-select/multi-select.ts
+++ b/packages/ecc-utils-design/src/components/multi-select/multi-select.ts
@@ -2,6 +2,7 @@ import { LitElement, html, css, PropertyValues } from "lit";
import { property, state } from "lit/decorators.js";
import { ComponentStyles as TailwindStyles } from "./tw-styles.js";
import { GlobalStyles } from "../../global.js";
+import { generateDeterministicId } from "../../ssr.js";
import "../checkbox/index.js";
function cn(...classes: (string | undefined | false)[]) {
@@ -40,9 +41,8 @@ export class EccUtilsDesignMultiSelect extends LitElement {
`,
];
- @state() private selectId = `multi-select-${Math.random()
- .toString(36)
- .substring(2, 9)}`;
+ // SSR-safe: deterministic IDs prevent hydration mismatches between server and client
+ @state() private selectId = generateDeterministicId("multi-select");
@property({ type: Array }) value: string[] = [];
@property({ type: String }) placeholder = "Select options...";
diff --git a/packages/ecc-utils-design/src/components/select/select.ts b/packages/ecc-utils-design/src/components/select/select.ts
index e716770a..d38a41bf 100644
--- a/packages/ecc-utils-design/src/components/select/select.ts
+++ b/packages/ecc-utils-design/src/components/select/select.ts
@@ -2,6 +2,7 @@ import { LitElement, html, css, PropertyValues } from "lit";
import { property, state } from "lit/decorators.js";
import { ComponentStyles as TailwindStyles } from "./tw-styles.js";
import { GlobalStyles } from "../../global.js";
+import { generateDeterministicId } from "../../ssr.js";
function cn(...classes: (string | undefined | false)[]) {
return classes.filter(Boolean).join(" ");
@@ -30,9 +31,8 @@ export class EccUtilsDesignSelect extends LitElement {
`,
];
- @state() private selectId = `select-${Math.random()
- .toString(36)
- .substring(2, 9)}`;
+ // SSR-safe: deterministic IDs prevent hydration mismatches between server and client
+ @state() private selectId = generateDeterministicId("select");
@property({ type: String }) value = "";
@property({ type: Boolean }) disabled = false;
diff --git a/packages/ecc-utils-design/src/index.ts b/packages/ecc-utils-design/src/index.ts
index 96a7338f..5975427b 100644
--- a/packages/ecc-utils-design/src/index.ts
+++ b/packages/ecc-utils-design/src/index.ts
@@ -2,3 +2,4 @@ import "./components/index.js";
import "./events/index.js";
export * from "./components/index.js";
+export * from "./ssr.js";
diff --git a/packages/ecc-utils-design/src/ssr.ts b/packages/ecc-utils-design/src/ssr.ts
new file mode 100644
index 00000000..f71fd456
--- /dev/null
+++ b/packages/ecc-utils-design/src/ssr.ts
@@ -0,0 +1,91 @@
+/**
+ * SSR (Server-Side Rendering) Utility Helpers
+ *
+ * These utilities ensure that Web Components can be safely imported
+ * during SSR (e.g. Next.js, Remix) without crashing due to missing
+ * browser globals (`window`, `document`, `customElements`).
+ *
+ * Note: This library does not render component templates on the server.
+ * Components are safe to *load* during SSR; rendering and hydration
+ * occur on the client.
+ *
+ * @module ssr
+ */
+
+/**
+ * Returns `true` when running in a browser environment.
+ *
+ * Why: Node.js (used by SSR frameworks) does not have a `window` global.
+ * Any code that touches `window`, `document`, or `customElements` must be
+ * gated behind this check to avoid `ReferenceError` on the server.
+ */
+export function isBrowser(): boolean {
+ return typeof window !== "undefined";
+}
+
+/**
+ * Returns `true` when running on the server (Node.js / SSR).
+ */
+export function isServer(): boolean {
+ return typeof window === "undefined";
+}
+
+/**
+ * Safely registers a custom element, skipping registration on the server
+ * and preventing duplicate registrations on the client.
+ *
+ * Why:
+ * - `customElements` does not exist in Node.js → calling it crashes the import.
+ * - Calling `customElements.define()` twice with the same tag name throws a
+ * `DOMException` → the duplicate check prevents this in HMR / dev scenarios.
+ *
+ * @param tagName - The custom element tag name (e.g. "ecc-utils-design-button")
+ * @param constructor - The custom element class
+ */
+export function ssrSafeDefine(
+ tagName: string,
+ constructor: CustomElementConstructor
+): void {
+ if (
+ typeof window !== "undefined" &&
+ window.customElements &&
+ !window.customElements.get(tagName)
+ ) {
+ window.customElements.define(tagName, constructor);
+ }
+}
+
+/**
+ * Monotonically increasing counter used by `generateDeterministicId`.
+ * Resets per JS context (server request vs client page load), which is
+ * exactly what we need for SSR hydration: both server and client start
+ * counting from 0 in the same order, producing matching IDs.
+ */
+let idCounter = 0;
+
+/**
+ * Generates a deterministic, incrementing ID string.
+ *
+ * Why: `Math.random()` produces different values on server vs client,
+ * causing hydration mismatches. A monotonic counter produces the same
+ * sequence as long as components are instantiated in the same order.
+ *
+ * @param prefix - A human-readable prefix (e.g. "select", "multi-select")
+ * @returns A unique ID like "select-0", "select-1", etc.
+ */
+export function generateDeterministicId(prefix: string): string {
+ const id = idCounter;
+ idCounter += 1;
+ return `${prefix}-${id}`;
+}
+
+/**
+ * Resets the deterministic ID counter.
+ *
+ * Useful in testing or when a fresh SSR request starts.
+ * Frameworks typically create a new JS context per request, so this
+ * is rarely needed in production.
+ */
+export function resetIdCounter(): void {
+ idCounter = 0;
+}