Skip to content

Commit eece462

Browse files
krisantrobusserifluousnkrantzkodiakhq[bot]
authored
feat(docs): add SSR and SSG docs (#4287)
* feat(docs): add SSR and SSG docs * fix(docs): typo * Apply suggestions from code review Co-authored-by: Sarah <[email protected]> * Apply suggestions from code review Co-authored-by: Nora Krantz <[email protected]> * feat(docs): address pr comments * feat(docs): add gif to docs * feat(docs): address pr comments * feat(docs): typos fix --------- Co-authored-by: Sarah <[email protected]> Co-authored-by: Nora Krantz <[email protected]> Co-authored-by: kodiakhq[bot] <49736102+kodiakhq[bot]@users.noreply.github.com>
1 parent 88776ce commit eece462

File tree

5 files changed

+224
-1
lines changed

5 files changed

+224
-1
lines changed

cypress/integration/sitemap-vrt/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ export const SITEMAP = [
2424
"/blog/2024-07-17-paste-newsletter/",
2525
"/blog/2024-08-23-paste-newsletter",
2626
"/blog/2024-11-07-paste-newsletter/",
27+
"/blog/2025-03-20-css-variables-ssr-ssg/",
2728
"/components/account-switcher/",
2829
"/components/account-switcher/api",
2930
"/components/account-switcher/changelog",
654 KB
Loading

packages/paste-website/src/components/ResponsiveBorderedImage.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ const ResponsiveBorderedImage: React.FC<ImageProps> = (props) => {
1515
borderRadius="borderRadius20"
1616
overflow="hidden"
1717
>
18-
<Image placeholder="blur" style={{ height: "100%", minWidth: "100%" }} {...props} />
18+
<Image style={{ height: "100%", minWidth: "100%" }} {...props} />
1919
</Box>
2020
);
2121
};
Lines changed: 211 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,211 @@
1+
export const meta = {
2+
title: 'Theme switching with SSR and SSG',
3+
slug: '/blog/2025-03-20-css-variables-ssr-ssg/',
4+
date: '2025-03-20',
5+
author: 'Kristian Antrobus',
6+
avatar: 'https://avatars.githubusercontent.com/u/55083528?v=4&size=64',
7+
excerpt: 'This blog describes how the Paste website uses CSS variables to switch themes with server-side rendering (SSR) and static site generation (SSG) with our existing ThemeProvider component.',
8+
};
9+
10+
import Image from 'next/image';
11+
import {Paragraph} from '@twilio-paste/paragraph';
12+
import {Box} from '@twilio-paste/box';
13+
import {InlineCode} from '@twilio-paste/inline-code';
14+
import {Anchor} from '@twilio-paste/anchor';
15+
import {Grid, Column} from '@twilio-paste/grid';
16+
import {CodeBlock, CodeBlockHeader, CodeBlockWrapper} from '@twilio-paste/code-block';
17+
import {ResponsiveBorderedImage} from '../../components/ResponsiveBorderedImage';
18+
19+
import DefaultLayout from '../../layouts/DefaultLayout';
20+
import {getNavigationData} from '../../utils/api';
21+
import FlickerDemo from "../../assets/images/articles/2025-03-20-css-variables-ssr-ssg/flicker.gif";
22+
23+
24+
export default DefaultLayout;
25+
26+
export const getStaticProps = async () => {
27+
const navigationData = await getNavigationData();
28+
return {
29+
props: {
30+
navigationData,
31+
},
32+
};
33+
};
34+
35+
<ArticleHeader
36+
title={meta.title}
37+
description={meta.description}
38+
date={meta.date}
39+
machineDate={meta.date}
40+
author={meta.author}
41+
avatar={meta.avatar}
42+
/>
43+
44+
---
45+
46+
<contentwrapper>
47+
48+
<PageAside data={mdxHeadings} hideFeedback />
49+
50+
<ArticleContent>
51+
52+
## TL;DR
53+
By leveraging Static Site Generation (SSG) in our documentation site, we optimize performance by serving pre-rendered pages. However, this approach introduces challenges with theme switching, such as flickering due to mismatches between the pre-rendered HTML and client-side theme preferences.
54+
55+
To resolve this, we implemented a <InlineCode>data-theme</InlineCode> attribute combined with CSS variables, ensuring that the correct theme is applied before React hydrates the page. This method prevents the flash effect and provides a seamless user experience. Additionally, our implementation in <InlineCode>_document.tsx</InlineCode> and <InlineCode>_app.tsx</InlineCode>, along with the <InlineCode>ThemeProvider</InlineCode> configuration, ensures that theme switching is handled efficiently without unnecessary re-renders.
56+
57+
By following this approach, we maintain both the performance benefits of SSG and a smooth, flicker-free theme transition for users.
58+
59+
## What are SSR and SSG?
60+
61+
Server-Side Rendering (SSR) and Static Site Generation (SSG) are two different rendering strategies used in frameworks like Next.js, each with its own advantages and use cases.
62+
63+
SSR generates pages on the server for every request, ensuring that users always receive the latest data. In Next.js, this is achieved using <InlineCode>getServerSideProps</InlineCode>. This approach is useful for dynamic content that changes frequently, such as personalized dashboards or real-time data feeds. However, because the server must generate the page before sending it to the client, it can be slower than other rendering methods.
64+
65+
SSG, on the other hand, generates pages at build time, meaning the content is pre-rendered and stored as static files. In Next.js, this is done using <InlineCode>getStaticProps</InlineCode>. This method is ideal for pages where content doesn’t change often, such as blogs, marketing pages, or documentation. Since the pages are served as static files, they load much faster than SSR-generated pages.
66+
67+
As our docs site data doesn't change between builds we use SSG to generate our pages to utilize the speed benefits of static site generation.
68+
69+
### Issue with theme switching
70+
71+
<Box width="100%" maxWidth="700px" display="block" margin="0 auto">
72+
<ResponsiveBorderedImage src={FlickerDemo} alt="Preview of theme flicker issue" />
73+
</Box>
74+
75+
When loading our site previously with a dark mode preference you would see the above flicker. This is due to the style being applied at the JavaScript level on the client and the server rendering the default light theme. This means there is a brief moment where the light theme is applied before the dark theme is applied by JavaScript.
76+
77+
When using SSG (Static Site Generation) and SSR (Server-Side Rendering) in Next.js, you may sometimes experience a flashbang effect or theme flickering when loading a page. This happens due to differences in how the initial HTML is generated versus how the React client-side hydration process takes over, recognizing the client preferences. Here’s why:
78+
79+
#### Mismatch between pre-rendered HTML and client state
80+
With SSG, the page is pre-built at build time and served as static HTML. If the theme (e.g., dark mode or light mode) is determined dynamically on the client side—perhaps by checking localStorage or user preferences—there can be a mismatch between the initial HTML (which doesn't know the user's preference) and the hydrated React state. This causes a flicker when React updates the page with the correct theme after loading.
81+
82+
SSR generates the page on each request, which can help deliver the correct theme initially, but if the theme is still set on the client side, the same flickering issue can occur.
83+
84+
#### Delay in Hydration process
85+
Next.js sends pre-rendered HTML first, then React hydrates it by attaching event listeners and making it interactive. If the theme is set dynamically via JavaScript on the client, React may initially render the wrong theme (based on the server-provided HTML) before correcting it after hydration. This is particularly noticeable when using a system preference-based theme (like dark mode) or when checking user preferences stored in localStorage.
86+
87+
#### Fallback or default theme during initial render
88+
If you don’t handle the theme properly, Next.js may serve a default theme (e.g., light mode) before applying the correct one. This results in a brief flash where the wrong theme appears before React updates it.
89+
90+
### The solution: CSS variables
91+
92+
Using CSS variables with <InlineCode>data-theme</InlineCode> set on the <InlineCode>html</InlineCode> or <InlineCode>body</InlineCode> is the most effective solution as the styles are applied <strong>before</strong> React hydrates the page. This ensures that the correct theme is applied from the start, preventing any flickering or flash.
93+
94+
This works because the browser applies the correct theme instantly, even before JavaScript runs, ensuring there is no mismatch between the server-rendered page and the client-rendered one. The original behavior picked up stlying changes **after** JavaScript executes, causing a visible flicker when the correct theme is applied later.
95+
96+
To support this approach, as of <Anchor showExternal={true} href="https://www.npmjs.com/package/@twilio-paste/design-tokens">@twilio-paste/design-tokens v10.14.0</Anchor> and <Anchor showExternal={true} href="https://www.npmjs.com/package/@twilio-paste/core">@twilio-paste/core v20.23.0</Anchor>, we have added a new file to each of our themes called <InlineCode>tokens.data-theme.css</InlineCode> which contains the CSS variables for each theme when the <InlineCode>data-theme</InlineCode> is set.
97+
98+
An example of how this CSS file is structured:
99+
100+
<Box marginBottom="space60">
101+
<CodeBlockWrapper>
102+
<CodeBlockHeader>
103+
@twilio-paste/design-tokens/dist/themes/twilio-dark/tokens.data-theme.css
104+
</CodeBlockHeader>
105+
<CodeBlock language='javascript' code={`body[data-theme="twilio-dark"] {
106+
--color-background-user: rgb(34, 9, 74);
107+
--color-background-notification: rgb(214, 31, 31);
108+
--color-background-trial: rgb(5, 41, 18);
109+
--color-background-subaccount: rgb(18, 28, 45);
110+
--color-background-primary-stronger: rgb(204, 228, 255);
111+
...
112+
`}/>
113+
</CodeBlockWrapper>
114+
</Box>
115+
116+
We have also added a new prop to the <InlineCode>ThemeProvider</InlineCode> called <InlineCode>useCSSVariables</InlineCode> which will allow you to use CSS variables instead of the theme files we previously configured. Note that we only want to use CSS variables when encoutnering this issue and not as a standard. For all other use cases we recommend using the <InlineCode>theme</InlineCode> prop.
117+
118+
119+
## Paste docs site implementation
120+
121+
This section will cover how we implemented this solution on the Paste documentation site using Next.js.
122+
123+
### _documents.tsx
124+
125+
We added a script to the <InlineCode>head</InlineCode> of our <InlineCode>_documents.tsx</InlineCode> file to set the <InlineCode>data-theme</InlineCode> attribute on the <InlineCode>body</InlineCode> element using a script. This script will check to see if users have a cookie set and if not check the system preferences to determine whether users prefer dark or light themes, and apply the dark theme if they do. Otherwise we do not set a value. You will see in <InlineCode>_app.tsx</InlineCode> how we handle the default value.
126+
127+
Here is the script that is found in <Anchor showExternal={true} href="https://github.com/twilio-labs/paste/blob/main/packages/paste-website/src/pages/_document.tsx"><InlineCode>_documents.tsx</InlineCode></Anchor>:
128+
129+
<Box marginBottom="space60">
130+
<CodeBlockWrapper>
131+
<CodeBlockHeader>Setting data-theme using cookie before render</CodeBlockHeader>
132+
<CodeBlock language='javascript' code={`<script
133+
type="text/javascript"
134+
dangerouslySetInnerHTML={{
135+
__html: \`
136+
let parts = typeof document !== "undefined" && document?.cookie.split("paste-docs-theme=");
137+
let cookie = parts.length == 2 ?parts?.pop().split(";").shift() : null;
138+
if(cookie){
139+
document.body.dataset.theme = cookie;
140+
}
141+
else if(window !== "undefined" && window.matchMedia && window.matchMedia("(prefers-color-scheme: dark)").matches){
142+
document.body.dataset.theme = "twilio-dark";
143+
}
144+
\`,
145+
}}
146+
/>`}/>
147+
</CodeBlockWrapper>
148+
</Box>
149+
150+
### _app.tsx
151+
152+
In our <InlineCode>_app.tsx</InlineCode> file we import the CSS files that should be applied. We have 2: One that applies the values to root and is considered the default theme, and the other that uses the new <InlineCode>tokens.data-theme.css</InlineCode> attribute to apply the theme based on the value set in the <InlineCode>data-theme</InlineCode> attribute.
153+
154+
You can find the <Anchor showExternal={true} href="https://github.com/twilio-labs/paste/blob/main/packages/paste-website/src/pages/_app.tsx">source code for _app.tsx here</Anchor>.
155+
156+
<Box marginBottom="space60">
157+
<CodeBlockWrapper>
158+
<CodeBlockHeader>Importing CSS files</CodeBlockHeader>
159+
<CodeBlock language='javascript' code={`import "@twilio-paste/design-tokens/dist/themes/twilio-dark/tokens.data-theme.css";
160+
import "@twilio-paste/design-tokens/dist/themes/twilio/tokens.custom-properties.css";
161+
`}/>
162+
</CodeBlockWrapper>
163+
</Box>
164+
165+
Note here that the import <InlineCode>"@twilio-paste/design-tokens/dist/themes/twilio/tokens.custom-properties.css"</InlineCode> is the default theme and the <InlineCode>"@twilio-paste/design-tokens/dist/themes/twilio-dark/tokens.data-theme.css"</InlineCode> is the theme that is applied when the <InlineCode>data-theme</InlineCode> attribute is set to <InlineCode>twilio-dark</InlineCode>.
166+
167+
#### ThemeProvider
168+
169+
In this same file we set the ThemeProvider to use CSS variables instead of the theme files we previously configured. Note that we only want to use CSS variables when encountering this issue and not as a standard. For all other use cases we recommend using the <InlineCode>theme</InlineCode> prop.
170+
171+
<Grid gutter="space30" vertical={[true, true, false]} marginBottom="space60">
172+
<Column span={[12, 12, 6]}>
173+
<CodeBlockWrapper>
174+
<CodeBlockHeader>ThemeProvider CSS implementation</CodeBlockHeader>
175+
<CodeBlock language='javascript' code={`<Theme.Provider
176+
useCSSVariables={true}
177+
cacheProviderProps={{ key: "next" }}
178+
>`}/>
179+
</CodeBlockWrapper>
180+
</Column>
181+
<Column span={[12,12,6]}>
182+
<CodeBlockWrapper>
183+
<CodeBlockHeader>ThemeProvider theme implementation</CodeBlockHeader>
184+
<CodeBlock language='javascript' code={`<Theme.Provider
185+
theme={theme || 'twilio'}
186+
cacheProviderProps={{ key: "next" }}
187+
>`}/>
188+
</CodeBlockWrapper>
189+
</Column>
190+
</Grid>
191+
192+
### Switching the theme
193+
194+
We have a hook that is used to switch the themes in <Anchor showExternal={true} href="https://github.com/twilio-labs/paste/blob/main/packages/paste-website/src/hooks/useDarkMode.tsx"><InlineCode>useDarkMode.tsx</InlineCode></Anchor>. In the <InlineCode>setMode</InlineCode> function, we handle setting the data attribute on the body element. This occurs when the user intentionally changes the theme using the theme switcher in the header. As this will run client side, it cannot be used to set the default value. That comes from the script in the <InlineCode>_document.tsx</InlineCode>.
195+
196+
<Box marginBottom="space60">
197+
<CodeBlockWrapper>
198+
<CodeBlockHeader>setMode</CodeBlockHeader>
199+
<CodeBlock language='javascript' code={`const setMode = (mode: ValidThemeName): void => {
200+
setCookie(null, "paste-docs-theme", mode, { path: "/" });
201+
document.body.dataset.theme = mode;
202+
setTheme(mode);
203+
};`}/>
204+
</CodeBlockWrapper>
205+
</Box>
206+
207+
As this update the body attribute <InlineCode>theme</InlineCode> and we are importing a CSS stylesheet that switches the variable values based on that value, the new theme will be applied. This works because the <InlineCode>ThemeProvider</InlineCode> is listening for the variable values and not pulling them from a static theme via JavaScript. It also stores the preference in a cookie ready to be picked up and applied to the body before React hydrates the page resulting in no flicker.
208+
209+
</ArticleContent>
210+
211+
</contentwrapper>

packages/paste-website/src/pages/theme/changing-theme.mdx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ export const meta = {
66

77
import {PageHeaderSeparator} from '@twilio-paste/page-header'
88
import {Separator} from '@twilio-paste/separator'
9+
import {Anchor} from '@twilio-paste/anchor'
910

1011
import {SidebarCategoryRoutes} from '../../constants';
1112
import DefaultLayout from '../../layouts/DefaultLayout';
@@ -62,6 +63,16 @@ Alternatively, Paste will automatically load the default theme's font via JavaSc
6263

6364
**For other themes, see our [manual installation page](/introduction/for-engineers/manual-installation/#how-to-load-the-right-font) for more information.**
6465

66+
## SSR and SSG
67+
68+
If you are using server-side rendering (SSR) or static site generation (SSG), <strong>and</strong> you support theme switching, you will need to use CSS variables and set a `body` `data-theme` attribute to avoid theme flicker on initial renders. You can view our <Anchor href="/blog/2025-03-20-css-variables-ssr-ssg/">blog post</Anchor> on this topic, discussing the problem and demonstrating how we solved it for the Paste website.
69+
70+
```jsx
71+
<Theme.Provider
72+
useCSSVariables={true}
73+
>
74+
```
75+
6576
## FAQs
6677

6778
### Some of my styles look broken!

0 commit comments

Comments
 (0)