` element. See https://lit.dev/docs/ssr/client-usage/#using-the-template-shadowroot-polyfill for inspiration on how to incorporate this into your application.
+
+## Contributing
+
+Please see [CONTRIBUTING.md](../../../CONTRIBUTING.md).
diff --git a/packages/labs/ssr-react/package.json b/packages/labs/ssr-react/package.json
new file mode 100644
index 0000000000..23791c2a2e
--- /dev/null
+++ b/packages/labs/ssr-react/package.json
@@ -0,0 +1,114 @@
+{
+ "name": "@lit-labs/ssr-react",
+ "version": "0.0.0",
+ "publishConfig": {
+ "access": "public"
+ },
+ "description": "Lit SSR integration for React",
+ "license": "BSD-3-Clause",
+ "author": "Google LLC",
+ "homepage": "https://github.com/lit/lit/tree/main/packages/labs/ssr-react",
+ "repository": {
+ "type": "git",
+ "url": "https://github.com/lit/lit.git",
+ "directory": "packages/labs/ssr-react"
+ },
+ "main": "index.js",
+ "typings": "index.d.ts",
+ "type": "module",
+ "exports": {
+ ".": {
+ "types": "./index.d.ts",
+ "node": "./node/index.js",
+ "default": "./index.js"
+ },
+ "./enable-lit-ssr.js": {
+ "types": "./enable-lit-ssr.d.ts",
+ "node": "./node/enable-lit-ssr.js",
+ "default": "./enable-lit-ssr.js"
+ },
+ "./jsx-dev-runtime": {
+ "types": "./jsx-dev-runtime.d.ts",
+ "node": "./node/jsx-dev-runtime.js",
+ "default": "./jsx-dev-runtime.js"
+ },
+ "./jsx-runtime": {
+ "types": "./jsx-runtime.d.ts",
+ "node": "./node/jsx-runtime.js",
+ "default": "./jsx-runtime.js"
+ }
+ },
+ "files": [
+ "/lib/",
+ "/node/",
+ "/enable-lit-ssr.{d.ts,d.ts.map,js,js.map}",
+ "/index.{d.ts,d.ts.map,js,js.map}",
+ "/jsx-dev-runtime.{d.ts,d.ts.map,js,js.map}",
+ "/jsx-runtime.{d.ts,d.ts.map,js,js.map}"
+ ],
+ "dependencies": {
+ "@lit-labs/ssr": "^3.0.1",
+ "lit": "^2.6.1"
+ },
+ "peerDependencies": {
+ "@types/react": "17 || 18",
+ "react": "17 || 18"
+ },
+ "scripts": {
+ "build": "wireit",
+ "build:ts": "wireit",
+ "test": "wireit"
+ },
+ "wireit": {
+ "build": {
+ "dependencies": [
+ "build:ts"
+ ]
+ },
+ "build:ts": {
+ "command": "tsc --build --pretty",
+ "clean": "if-file-deleted",
+ "dependencies": [
+ "../ssr:build:ts"
+ ],
+ "files": [
+ "src/**/*.ts{,x}",
+ "tsconfig.json"
+ ],
+ "output": [
+ "lib/",
+ "node/",
+ "test/",
+ "enable-lit-ssr.{d.ts,d.ts.map,js,js.map}",
+ "index.{d.ts,d.ts.map,js,js.map}",
+ "jsx-dev-runtime.{d.ts,d.ts.map,js,js.map}",
+ "jsx-runtime.{d.ts,d.ts.map,js,js.map}",
+ "tsconfig.tsbuildinfo"
+ ]
+ },
+ "test": {
+ "command": "uvu test _test.js$",
+ "files": [],
+ "output": [],
+ "dependencies": [
+ "build"
+ ]
+ }
+ },
+ "devDependencies": {
+ "@types/react": "^18.0.27",
+ "@types/react-dom": "^18.0.10",
+ "react": "^18.2.0",
+ "react-dom": "^18.2.0",
+ "uvu": "^0.5.6"
+ },
+ "keywords": [
+ "lit",
+ "lit-element",
+ "lit-ssr",
+ "next",
+ "react",
+ "ssr",
+ "web components"
+ ]
+}
diff --git a/packages/labs/ssr-react/src/custom_typings/react-jsx-runtime.d.ts b/packages/labs/ssr-react/src/custom_typings/react-jsx-runtime.d.ts
new file mode 100644
index 0000000000..f2a15bd13e
--- /dev/null
+++ b/packages/labs/ssr-react/src/custom_typings/react-jsx-runtime.d.ts
@@ -0,0 +1,24 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview Declarations for React jsx-runtime modules which are not
+ * provided in `@types/react` as these modules are not meant for direct
+ * consumption, only as part of JSX transform.
+ */
+
+/* eslint-disable @typescript-eslint/no-explicit-any */
+
+declare module 'react/jsx-runtime' {
+ export const Fragment: any;
+ export const jsx: any;
+ export const jsxs: any;
+}
+
+declare module 'react/jsx-dev-runtime' {
+ export const Fragment: any;
+ export const jsxDEV: any;
+}
diff --git a/packages/labs/ssr-react/src/enable-lit-ssr.ts b/packages/labs/ssr-react/src/enable-lit-ssr.ts
new file mode 100644
index 0000000000..cdb61c9977
--- /dev/null
+++ b/packages/labs/ssr-react/src/enable-lit-ssr.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview Side-effect import meant to be loaded in the **browser** before
+ * any user code is loaded. Installs hydration support for `LitElement`.
+ */
+
+import 'lit/experimental-hydrate-support.js';
diff --git a/packages/labs/ssr-react/src/index.ts b/packages/labs/ssr-react/src/index.ts
new file mode 100644
index 0000000000..2d922108cd
--- /dev/null
+++ b/packages/labs/ssr-react/src/index.ts
@@ -0,0 +1,8 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export {wrapCreateElement} from './lib/wrap-create-element.js';
+export {createElement} from './lib/create-element.js';
diff --git a/packages/labs/ssr-react/src/jsx-dev-runtime.ts b/packages/labs/ssr-react/src/jsx-dev-runtime.ts
new file mode 100644
index 0000000000..51a122dbdf
--- /dev/null
+++ b/packages/labs/ssr-react/src/jsx-dev-runtime.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview To serve as JSX import source for runtime JSX transforms in
+ * development mode. For use in browsers.
+ */
+
+import 'lit/experimental-hydrate-support.js';
+
+// eslint-disable-next-line import/extensions
+export {Fragment, jsxDEV} from 'react/jsx-dev-runtime';
diff --git a/packages/labs/ssr-react/src/jsx-runtime.ts b/packages/labs/ssr-react/src/jsx-runtime.ts
new file mode 100644
index 0000000000..8786f71dd4
--- /dev/null
+++ b/packages/labs/ssr-react/src/jsx-runtime.ts
@@ -0,0 +1,14 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview To serve as JSX import source for runtime JSX transforms in
+ * production mode. For use in browsers.
+ */
+
+import 'lit/experimental-hydrate-support.js';
+// eslint-disable-next-line import/extensions
+export {Fragment, jsx, jsxs} from 'react/jsx-runtime';
diff --git a/packages/labs/ssr-react/src/lib/create-element.ts b/packages/labs/ssr-react/src/lib/create-element.ts
new file mode 100644
index 0000000000..7024d40131
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/create-element.ts
@@ -0,0 +1,12 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * React's `createElement` function enhanced to deeply render Lit components in
+ * the server. It is simply a passthrough of `React.createElement` in the
+ * browser.
+ */
+export {createElement} from 'react';
diff --git a/packages/labs/ssr-react/src/lib/node/create-element.ts b/packages/labs/ssr-react/src/lib/node/create-element.ts
new file mode 100644
index 0000000000..af88143163
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/node/create-element.ts
@@ -0,0 +1,15 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import React from 'react';
+import {wrapCreateElement} from './wrap-create-element.js';
+
+/**
+ * React's `createElement` function enhanced to deeply render Lit components in
+ * the server. It is simply a passthrough of `React.createElement` in the
+ * browser.
+ */
+export const createElement = wrapCreateElement(React.createElement);
diff --git a/packages/labs/ssr-react/src/lib/node/render-custom-element.ts b/packages/labs/ssr-react/src/lib/node/render-custom-element.ts
new file mode 100644
index 0000000000..6b987762a4
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/node/render-custom-element.ts
@@ -0,0 +1,82 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {LitElementRenderer} from '@lit-labs/ssr/lib/lit-element-renderer.js';
+import {getElementRenderer} from '@lit-labs/ssr/lib/element-renderer.js';
+import type {RenderInfo} from '@lit-labs/ssr';
+
+const reservedReactProperties = new Set([
+ 'children',
+ 'localName',
+ 'ref',
+ 'style',
+ 'className',
+]);
+
+const attributesToProps = (attrs: NamedNodeMap) => {
+ const props: {[index: string]: string} = {};
+ for (let i = 0; i < attrs.length; i++) {
+ const attr = attrs[i];
+ props[attr.name] = attr.value;
+ }
+ return props;
+};
+
+/**
+ * Renders the shadow contents of the provided custom element type with props.
+ * Should only be called in server environments.
+ */
+export const renderCustomElement = (tagName: string, props: {} | null) => {
+ const renderInfo: RenderInfo = {
+ elementRenderers: [LitElementRenderer],
+ customElementInstanceStack: [],
+ customElementHostStack: [],
+ deferHydration: false,
+ };
+
+ const renderer = getElementRenderer(renderInfo, tagName);
+
+ if (renderer.element !== undefined && props != null) {
+ for (const [k, v] of Object.entries(props)) {
+ // Reserved React Props do not need to be set on the element
+ if (reservedReactProperties.has(k)) {
+ continue;
+ }
+
+ if (k in renderer.element) {
+ renderer.setProperty(k, v);
+ } else {
+ renderer.setAttribute(k, String(v));
+ }
+ }
+ }
+
+ renderer.connectedCallback();
+
+ renderInfo.customElementInstanceStack.push(renderer);
+
+ const shadowContents = renderer.renderShadow(renderInfo);
+
+ // elementAttributes will be provided to React as props for the host element
+ // for properly rendering reflected attributes
+ const elementAttributes =
+ renderer.element !== undefined
+ ? attributesToProps(renderer.element.attributes)
+ : {};
+
+ const {mode = 'open', delegatesFocus} = renderer.shadowRootOptions;
+ const templateAttributes = {
+ shadowroot: mode,
+ shadowrootmode: mode,
+ ...(delegatesFocus ? {shadowrootdelegatesfocus: ''} : {}),
+ };
+
+ return {
+ shadowContents,
+ elementAttributes,
+ templateAttributes,
+ };
+};
diff --git a/packages/labs/ssr-react/src/lib/node/wrap-create-element.ts b/packages/labs/ssr-react/src/lib/node/wrap-create-element.ts
new file mode 100644
index 0000000000..8604ebf5f7
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/node/wrap-create-element.ts
@@ -0,0 +1,53 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {isCustomElement} from '../utils.js';
+import {renderCustomElement} from './render-custom-element.js';
+
+import type {
+ createElement as ReactCreateElement,
+ ElementType,
+ ReactElement,
+ ReactNode,
+} from 'react';
+
+/**
+ * Wraps the provided `createElement` function to also server render Lit
+ * component's shadow DOM.
+ */
+export function wrapCreateElement(
+ // TODO(augustjk) Should the type for this param be more generic to allow
+ // non-React alternatives like preact?
+ originalCreateElement: typeof ReactCreateElement
+) {
+ return function createElement(
+ type: ElementType
,
+ props: P,
+ ...children: ReactNode[]
+ ): ReactElement {
+ if (isCustomElement(type)) {
+ const {shadowContents, elementAttributes, templateAttributes} =
+ renderCustomElement(type, props);
+
+ if (shadowContents !== undefined) {
+ const templateShadowRoot = originalCreateElement('template', {
+ ...templateAttributes,
+ dangerouslySetInnerHTML: {
+ __html: [...shadowContents].join(''),
+ },
+ });
+
+ return originalCreateElement(
+ type,
+ {...props, ...elementAttributes},
+ templateShadowRoot,
+ ...children
+ );
+ }
+ }
+ return originalCreateElement(type, props, ...children);
+ };
+}
diff --git a/packages/labs/ssr-react/src/lib/utils.ts b/packages/labs/ssr-react/src/lib/utils.ts
new file mode 100644
index 0000000000..d8fb713dd6
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/utils.ts
@@ -0,0 +1,8 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export const isCustomElement = (type: string | Object): type is string =>
+ typeof type === 'string' && !!customElements.get(type);
diff --git a/packages/labs/ssr-react/src/lib/wrap-create-element.ts b/packages/labs/ssr-react/src/lib/wrap-create-element.ts
new file mode 100644
index 0000000000..04ca9f07a2
--- /dev/null
+++ b/packages/labs/ssr-react/src/lib/wrap-create-element.ts
@@ -0,0 +1,13 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * Wraps the provided `createElement` function to also server render Lit
+ * component's shadow DOM. Has no effect when imported in browser.
+ */
+export function wrapCreateElement(createElement: T) {
+ return createElement;
+}
diff --git a/packages/labs/ssr-react/src/node/enable-lit-ssr.ts b/packages/labs/ssr-react/src/node/enable-lit-ssr.ts
new file mode 100644
index 0000000000..9ad66e65c0
--- /dev/null
+++ b/packages/labs/ssr-react/src/node/enable-lit-ssr.ts
@@ -0,0 +1,17 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview Side-effect import meant to be loaded on the **server** before
+ * any user code is loaded. Patches `React.createElement` to support deep SSR of
+ * Lit components.
+ */
+
+import React from 'react';
+import {wrapCreateElement} from '../lib/node/wrap-create-element.js';
+
+// eslint-disable-next-line @typescript-eslint/no-explicit-any
+(React.createElement as any) = wrapCreateElement(React.createElement);
diff --git a/packages/labs/ssr-react/src/node/index.ts b/packages/labs/ssr-react/src/node/index.ts
new file mode 100644
index 0000000000..1df804592e
--- /dev/null
+++ b/packages/labs/ssr-react/src/node/index.ts
@@ -0,0 +1,8 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+export {wrapCreateElement} from '../lib/node/wrap-create-element.js';
+export {createElement} from '../lib/node/create-element.js';
diff --git a/packages/labs/ssr-react/src/node/jsx-dev-runtime.ts b/packages/labs/ssr-react/src/node/jsx-dev-runtime.ts
new file mode 100644
index 0000000000..f993ccf693
--- /dev/null
+++ b/packages/labs/ssr-react/src/node/jsx-dev-runtime.ts
@@ -0,0 +1,68 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview To serve as JSX import source for runtime JSX transforms in
+ * development mode. For use in servers.
+ */
+
+// eslint-disable-next-line import/extensions
+import * as ReactJSXDevRuntime from 'react/jsx-dev-runtime';
+import {createElement, type ElementType, type ReactNode} from 'react';
+import {isCustomElement} from '../lib/utils.js';
+import {renderCustomElement} from '../lib/node/render-custom-element.js';
+
+export const Fragment = ReactJSXDevRuntime.Fragment;
+
+export const jsxDEV = (
+ type: ElementType
,
+ props: P,
+ key: string | undefined,
+ isStaticChildren: boolean,
+ source: Object,
+ self: Object
+) => {
+ if (isCustomElement(type)) {
+ const {shadowContents, elementAttributes, templateAttributes} =
+ renderCustomElement(type, props);
+
+ if (shadowContents) {
+ const templateShadowRoot = createElement('template', {
+ ...templateAttributes,
+ dangerouslySetInnerHTML: {
+ __html: [...shadowContents].join(''),
+ },
+ });
+
+ return ReactJSXDevRuntime.jsxDEV(
+ type,
+ {
+ ...props,
+ ...elementAttributes,
+ children: [
+ templateShadowRoot,
+ ...(isStaticChildren
+ ? (props.children as ReactNode[])
+ : [props.children]),
+ ],
+ },
+ key,
+ true, // hard-code to true so React won't warn about missing key
+ source,
+ self
+ );
+ }
+ }
+
+ return ReactJSXDevRuntime.jsxDEV(
+ type,
+ props,
+ key,
+ isStaticChildren,
+ source,
+ self
+ );
+};
diff --git a/packages/labs/ssr-react/src/node/jsx-runtime.ts b/packages/labs/ssr-react/src/node/jsx-runtime.ts
new file mode 100644
index 0000000000..09b206b360
--- /dev/null
+++ b/packages/labs/ssr-react/src/node/jsx-runtime.ts
@@ -0,0 +1,80 @@
+/**
+ * @license
+ * Copyright 2022 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/**
+ * @fileoverview To serve as JSX import source for runtime JSX transforms in
+ * production mode. For use in servers.
+ */
+
+// eslint-disable-next-line import/extensions
+import * as ReactJSXRuntime from 'react/jsx-runtime';
+import {createElement, type ReactNode, type ElementType} from 'react';
+import {isCustomElement} from '../lib/utils.js';
+import {renderCustomElement} from '../lib/node/render-custom-element.js';
+
+export const Fragment = ReactJSXRuntime.Fragment;
+
+export const jsx =
(
+ type: ElementType
,
+ props: P,
+ key: string | undefined
+) => {
+ if (isCustomElement(type)) {
+ const {shadowContents, elementAttributes, templateAttributes} =
+ renderCustomElement(type, props);
+
+ if (shadowContents) {
+ const templateShadowRoot = createElement('template', {
+ ...templateAttributes,
+ dangerouslySetInnerHTML: {
+ __html: [...shadowContents].join(''),
+ },
+ });
+
+ // Call `jsxs` instead of `jsx` so that React doesn't incorrectly
+ // interpret that the children array was dynamically made and warn about
+ // missing keys
+ return ReactJSXRuntime.jsxs(type, {
+ ...props,
+ ...elementAttributes,
+ children: [templateShadowRoot, props.children],
+ });
+ }
+ }
+
+ return ReactJSXRuntime.jsx(type, props, key);
+};
+
+export const jsxs =
(
+ type: ElementType
,
+ props: P,
+ key: string | undefined
+) => {
+ if (isCustomElement(type)) {
+ const {shadowContents, elementAttributes, templateAttributes} =
+ renderCustomElement(type, props);
+
+ if (shadowContents) {
+ const templateShadowRoot = createElement('template', {
+ ...templateAttributes,
+ dangerouslySetInnerHTML: {
+ __html: [...shadowContents].join(''),
+ },
+ });
+
+ return ReactJSXRuntime.jsxs(
+ type,
+ {
+ ...props,
+ ...elementAttributes,
+ children: [templateShadowRoot, ...props.children],
+ },
+ key
+ );
+ }
+ }
+ return ReactJSXRuntime.jsxs(type, props, key);
+};
diff --git a/packages/labs/ssr-react/src/test/integration/enable-lit-ssr_test.tsx b/packages/labs/ssr-react/src/test/integration/enable-lit-ssr_test.tsx
new file mode 100644
index 0000000000..1de8f4e33c
--- /dev/null
+++ b/packages/labs/ssr-react/src/test/integration/enable-lit-ssr_test.tsx
@@ -0,0 +1,146 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import '@lit-labs/ssr-react/enable-lit-ssr.js';
+import React from 'react';
+// eslint-disable-next-line import/extensions
+import ReactDOMServer from 'react-dom/server';
+
+import '../test-element.js';
+
+import {suite} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+
+const test = suite();
+
+test('single element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(),
+ `Hello, Somebody!
+ `
+ );
+});
+
+test('single element with prop', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(),
+ `Hello, World!
+ `
+ );
+});
+
+test('single element within DOM element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+
+
+ ),
+ ``
+ );
+});
+
+test('single element with string child', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+ some string child
+ ),
+ `Hello, Somebody!
+ some string child`
+ );
+});
+
+test('single element with element child', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ span child
+
+ ),
+ `Hello, Somebody!
+ span child`
+ );
+});
+
+test('single element with multiple children', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ span
+ p
+
+ ),
+ `Hello, Somebody!
+ spanp
`
+ );
+});
+
+test('single element with dynamic children', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ {[1, 2, 3].map((i) => (
+ {i}
+ ))}
+
+ ),
+ `Hello, Somebody!
+ 123`
+ );
+});
+
+test('nested element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+
+
+ ),
+ `Hello, Somebody!
+ Hello, Somebody!
+ `
+ );
+});
+
+test.run();
diff --git a/packages/labs/ssr-react/src/test/integration/runtime-jsx_test.tsx b/packages/labs/ssr-react/src/test/integration/runtime-jsx_test.tsx
new file mode 100644
index 0000000000..70241bba67
--- /dev/null
+++ b/packages/labs/ssr-react/src/test/integration/runtime-jsx_test.tsx
@@ -0,0 +1,144 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+/** @jsxImportSource @lit-labs/ssr-react */
+
+// eslint-disable-next-line import/extensions
+import ReactDOMServer from 'react-dom/server';
+
+import '../test-element.js';
+
+import {test} from 'uvu';
+// eslint-disable-next-line import/extensions
+import * as assert from 'uvu/assert';
+
+test('single element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(),
+ `Hello, Somebody!
+ `
+ );
+});
+
+test('single element with prop', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(),
+ `Hello, World!
+ `
+ );
+});
+
+test('single element within DOM element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+
+
+ ),
+ ``
+ );
+});
+
+test('single element with string child', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+ some string child
+ ),
+ `Hello, Somebody!
+ some string child`
+ );
+});
+
+test('single element with element child', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ span child
+
+ ),
+ `Hello, Somebody!
+ span child`
+ );
+});
+
+test('single element with multiple children', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ span
+ p
+
+ ),
+ `Hello, Somebody!
+ spanp
`
+ );
+});
+
+test('single element with dynamic children', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+ {[1, 2, 3].map((i) => (
+ {i}
+ ))}
+
+ ),
+ `Hello, Somebody!
+ 123`
+ );
+});
+
+test('nested element', () => {
+ assert.equal(
+ ReactDOMServer.renderToString(
+
+
+
+ ),
+ `Hello, Somebody!
+ Hello, Somebody!
+ `
+ );
+});
+
+test.run();
diff --git a/packages/labs/ssr-react/src/test/test-element.ts b/packages/labs/ssr-react/src/test/test-element.ts
new file mode 100644
index 0000000000..cba96eeda7
--- /dev/null
+++ b/packages/labs/ssr-react/src/test/test-element.ts
@@ -0,0 +1,43 @@
+/**
+ * @license
+ * Copyright 2023 Google LLC
+ * SPDX-License-Identifier: BSD-3-Clause
+ */
+
+import {html, css, LitElement} from 'lit';
+import {customElement, property} from 'lit/decorators.js';
+
+@customElement('test-element')
+export class TestElement extends LitElement {
+ static override styles = css`
+ p {
+ color: blue;
+ }
+ `;
+
+ @property()
+ name = 'Somebody';
+
+ override render() {
+ return html`Hello, ${this.name}!
+ `;
+ }
+}
+
+declare global {
+ interface HTMLElementTagNameMap {
+ 'test-element': TestElement;
+ }
+
+ // eslint-disable-next-line @typescript-eslint/no-namespace
+ namespace JSX {
+ interface IntrinsicElements {
+ 'test-element':
+ | React.DetailedHTMLProps<
+ React.HTMLAttributes,
+ TestElement
+ >
+ | Partial;
+ }
+ }
+}
diff --git a/packages/labs/ssr-react/tsconfig.json b/packages/labs/ssr-react/tsconfig.json
new file mode 100644
index 0000000000..a84a07ff3a
--- /dev/null
+++ b/packages/labs/ssr-react/tsconfig.json
@@ -0,0 +1,28 @@
+{
+ "compilerOptions": {
+ "composite": true,
+ "tsBuildInfoFile": "./tsconfig.tsbuildinfo",
+ "target": "es2020",
+ "module": "esnext",
+ "moduleResolution": "NodeNext",
+ "lib": ["es2020", "DOM"],
+ "declaration": true,
+ "declarationMap": true,
+ "sourceMap": true,
+ "inlineSources": true,
+ "rootDir": "./src",
+ "outDir": ".",
+ "strict": true,
+ "noUnusedLocals": true,
+ "noUnusedParameters": true,
+ "noImplicitReturns": true,
+ "noFallthroughCasesInSwitch": true,
+ "allowSyntheticDefaultImports": true,
+ "experimentalDecorators": true,
+ "skipLibCheck": true,
+ "noImplicitOverride": true,
+ "jsx": "react"
+ },
+ "include": ["src/**/*"],
+ "exclude": []
+}