Component architecture

Understand the runtime behavior of code components

Code components run as isolated React applications. Each one mounts in its own Shadow DOM container with a separate React root, creating a sandboxed environment that prevents conflicts with the main page or other components.

Component architectureComponent architecture

Because of this isolation, each imported React component manages its own dependencies, state, and context. When building, it’s important to consider how your components handle state and communicate with each other.

Key concepts

  • Shadow DOM isolation - Styles and DOM elements are contained.
  • Separate React roots - No shared state or context between components.
  • Server-side rendering - SSR provides initial HTML
  • Client-side execution - All interactivity runs in the browser

This architecture affects how you handle state management, component communication, data fetching, and styling. Use the following patterns to manage these constraints when building your React components for import into Webflow.

Shadow DOM and React roots

Each code component runs in its own Shadow DOM container with a separate React root. This sandboxed environment prevents conflicts with the main page and other components:

  • Your component’s styles won’t leak to the page
  • Page styles won’t override your component’s styles
  • You must explicitly import external styles (site variables, tag selectors, etc.)

Composing components with slots

When composing code components using slots, parent and child components may not share state through React context. Each child component renders in its own Shadow DOM container, which isolates component state sharing.

Shadow DOM impacts how you style components as well as your ability to use third-party libraries. To learn more about styling components within the Shadow DOM, see the styling components guide and frameworks and libraries guide.

Server-side rendering (SSR)

Webflow supports server-side rendering (SSR) for code components. SSR generates initial HTML for the component on the server, which can improve perceived performance and SEO. After the page loads, Webflow automatically hydrates the component in the browser so that it becomes fully interactive.

Webflow enables SSR by default, but you can disable it by setting ssr to false in the component’s definition file.

Chart.webflow.tsx
1declareComponent(Component, {
2 name: "Chart",
3 description: "An interactive chart component",
4 group: "Data Visualization",
5 options: {
6 ssr: false
7 },
8});

When to disable SSR

You’ll want to turn off SSR for code components that rely on client-only behavior or that don’t benefit from server-rendered HTML. Common cases include:

  • Browser APIs: Components that use window, document, localStorage, or other APIs not available during SSR.
  • Dynamic or personalized content: User-specific dashboards, authenticated views, or components that need client data to render correctly.
  • Heavy or interactive UI: Charts, 3D scenes, maps, or animation-driven elements that would bloat the server-rendered HTML and be re-rendered anyway.
  • Non-deterministic output: Anything that renders differently on the server vs. client (for example, random numbers, time-based values).

If the HTML output helps with SEO or improves the first paint, keep SSR on. If the component is purely interactive, client-specific, or browser-dependent, disable SSR.

React Server Components are not supported

React Server Components aren’t supported in code components. All code components must use standard React components.

Communicating between components

Because each code component runs in its own React root, they can’t share React Context or state directly. Instead, use one of the following patterns to manage state across multiple components.

Sharing state across components

URL parameters

Store state in the URL using URLSearchParams for shareable, bookmarkable state. This is useful for search queries, filters, navigation state, or pagination.

Filter.tsx
1// Set state
2const url = new URL(window.location.href);
3url.searchParams.set('filter', 'active');
4window.history.pushState({}, '', url);
5
6// Read state
7const params = new URLSearchParams(window.location.search);
8const filter = params.get('filter'); // 'active'

Browser storage

Use localStorage for persistent data or sessionStorage for session-only data. Only store non-sensitive information since this data is visible to users.

ThemePreference.tsx
1// localStorage - persists across browser sessions
2window.localStorage.setItem('userPreferences', JSON.stringify({ theme: 'dark' }));
3const prefs = JSON.parse(localStorage.getItem('userPreferences'));
4
5// sessionStorage - cleared when tab closes
6window.sessionStorage.setItem('tempData', 'value');

Best for: user preferences, form data, temporary state.

Nano Stores

Nano stores is a lightweight state management library for cross-component communication, and is a useful alternative to React Context for sharing state between components.

1

Install Nano Stores

In your React project, install Nano Stores by running the following command in your terminal:

$npm install nanostores @nanostores/react
2

Create a store

A store represents is external shared state: any component can access, modify, or subscribe to it. Create a file to create the store. Use Nano Stores’ atom() to make a shared, reactive variable.

Store.tsx
1import { atom } from 'nanostores';
2
3// Shared state store - any component can read/write to this
4export const $counter = atom(0);

Note: This example uses an atomic store with a single value. See the Nano Stores documentation for more information on the different types of stores.

3

Read a store in a component

In your component, subscribe to the store using useStore() hook to automatically update when the value changes:

Counter.tsx
1import React from 'react';
2
3import {useStore} from '@nanostores/react';
4import {$counter} from './store';
5
6// Displays the current count - automatically updates when store changes
7export const Counter = () => {
8 const count = useStore($counter); // Subscribe to store changes
9
10 return(
11 <div style={{backgroundColor: 'lightblue', padding: '10px', borderRadius: '5px'}}>
12 <p>Count: {count}</p>
13 </div>
14 )
15};
4

Update the store from a separate component

In the Clicker component, update the store using set() to change the value:

Clicker.tsx
1import React from 'react';
2import {Button} from './Button';
3import {$counter} from './Store';
4
5interface ClickerProps {
6 text: string;
7 variant: 'primary' | 'secondary';
8}
9
10// Button that increments the shared counter when clicked
11export const Clicker = ({text, variant}: ClickerProps) => {
12 const clicked = React.useCallback(() => $counter.set($counter.get() + 1), []); // Update shared state
13 return <Button
14 text={text}
15 variant={variant}
16 onClick={clicked}/>;
17};

Note: This example uses the useCallback hook to update the store when the button once it’s clicked.

Custom events

To notify a component of an event or update another component, use custom events to communicate across React components in Webflow:

1import React from 'react';
2
3// Theme toggle component dispatches events
4export const ThemeToggle = () => {
5 const handleClick = () => {
6 // Dispatch custom event that other components can listen to
7 window.dispatchEvent(new CustomEvent('theme-changed', {
8 detail: { theme: 'dark' },
9 }));
10 };
11
12 return <button onClick={handleClick}>Switch to Dark Mode</button>;
13 };
14

Data fetching

Code components support client-side data fetching. This means your React component can request live or real-time data from a public API after it renders in the browser.

To fetch data, use React’s useEffect hook when the component mounts:

MyComponent.tsx
1import React, { useEffect, useState } from "react";
2
3interface ApiResponse {
4 message: string;
5}
6
7export const MyComponent = () => {
8 const [data, setData] = useState<ApiResponse | null>(null);
9 const [loading, setLoading] = useState(true);
10
11 useEffect(() => {
12 fetch("/api/public-data")
13 .then((res) => res.json())
14 .then((json: ApiResponse) => setData(json))
15 .catch((err) => console.error("Fetch failed:", err))
16 .finally(() => setLoading(false));
17 }, []);
18
19 if (loading) return <div>Loading...</div>;
20 if (!data) return <div>No data available</div>;
21
22 return <div>{data.message}</div>;
23};

Key considerations

  • Public APIs only: Never include secrets or sensitive API keys in your component code. All JavaScript runs in the browser and is visible to users.
  • CORS support required: The API must accept cross-origin requests from your Webflow-hosted site.
  • No environment variables: .env files aren’t supported. If you need to pass configuration values (like endpoint URLs, IDs, or feature flags), provide them as props instead of embedding them directly.