Get started
Framework guides
Where the stylesheet, the provider and the theme script go in Vite, Next.js, React Router, Astro and TanStack Start, and what each one needs from the client and the server.
The setup is the same three things everywhere: import @alea-design/ui/styles.css once, wrap the app in ThemeProvider, and, when a server renders the page, add ThemeScript to the head. What changes is the file each one goes in. How to create a project and install the packages is in the Installation guide.
Vite
A client-only app: the page is built in the browser, so there is nothing to render on a server.
What | Where and how |
|---|---|
| The stylesheet | Import it once in the entry file, src/main.tsx. |
| The provider | Wrap the app in ThemeProvider where it is rendered. It reads the saved theme on the first render, so there is no flash after React starts. |
| Client code | All of it: nothing needs a directive. |
| Server rendering | None, so ThemeScript is not needed. |
// src/main.tsximport '@alea-design/ui/styles.css';import { createRoot } from 'react-dom/client';import { ThemeProvider } from '@alea-design/ui';import { App } from './App';createRoot(document.getElementById('root')!).render(<ThemeProvider defaultTheme="system"><App /></ThemeProvider>,);
A working project with this setup: examples/vite.
Next.js App Router
Server components by default. Every export of @alea-design/ui is marked as a client component, so a server component can render Alea components directly.
What | Where and how |
|---|---|
| The stylesheet | Import it in the root layout, app/layout.tsx. |
| The provider | A small file marked "use client", app/providers.tsx, holds ThemeProvider; the layout stays a server component and renders it around the children. Props that are functions (navigate, useHref) have to be set in that client file. |
| Needs "use client" | Any file of yours that uses a hook (useTheme, useState) or passes a function such as onPress. Components copied with the CLI are plain source files without the directive, so import them from a file that has it. |
| Server rendering | Put ThemeScript in the head of the root layout and suppressHydrationWarning on the html element, so the saved theme is on the page before the first paint. |
// app/layout.tsximport '@alea-design/ui/styles.css';import { ThemeScript } from '@alea-design/ui';import { Providers } from './providers';export default function RootLayout({ children }: { children: React.ReactNode }) {return (<html lang="en" suppressHydrationWarning><head><ThemeScript /></head><body><Providers>{children}</Providers></body></html>);}
// app/providers.tsx'use client';import { ThemeProvider } from '@alea-design/ui';export function Providers({ children }: { children: React.ReactNode }) {return <ThemeProvider defaultTheme="system">{children}</ThemeProvider>;}
A working project with this setup: examples/next.
Next.js Pages Router
Every page is a client component that is also rendered on the server, so no directive is involved.
What | Where and how |
|---|---|
| The stylesheet | Import it in pages/_app.tsx: Next.js allows global CSS from a package there. |
| The provider | Wrap the page component in ThemeProvider in pages/_app.tsx. |
| Client code | All of it: nothing needs a directive. |
| Server rendering | Put ThemeScript inside Head in pages/_document.tsx and suppressHydrationWarning on Html. The repository has no Pages Router project; this setup was built and rendered on the server with the same packages. |
// pages/_app.tsximport '@alea-design/ui/styles.css';import type { AppProps } from 'next/app';import { ThemeProvider } from '@alea-design/ui';export default function App({ Component, pageProps }: AppProps) {return (<ThemeProvider defaultTheme="system"><Component {...pageProps} /></ThemeProvider>);}
// pages/_document.tsximport { Html, Head, Main, NextScript } from 'next/document';import { ThemeScript } from '@alea-design/ui';export default function Document() {return (<Html lang="en" suppressHydrationWarning><Head><ThemeScript /></Head><body><Main /><NextScript /></body></Html>);}
The App Router example is in examples/next.
React Router
Framework mode with server rendering on (ssr: true in react-router.config.ts). Components are plain React: no directives.
What | Where and how |
|---|---|
| The stylesheet | Import it in app/root.tsx. |
| The provider | In the default export of app/root.tsx, around the Outlet. Passing navigate and useHref makes Alea links move through the router without reloading the page. |
| Client code | All of it: nothing needs a directive. |
| Server rendering | Put ThemeScript in the head of the Layout export of the same file, with suppressHydrationWarning on the html element. |
// app/root.tsximport '@alea-design/ui/styles.css';import { Links, Meta, Outlet, Scripts, useHref, useNavigate } from 'react-router';import { ThemeProvider, ThemeScript } from '@alea-design/ui';export function Layout({ children }: { children: React.ReactNode }) {return (<html lang="en" suppressHydrationWarning><head><Meta /><Links /><ThemeScript /></head><body>{children}<Scripts /></body></html>);}export default function App() {const navigate = useNavigate();return (<ThemeProvider defaultTheme="system" navigate={navigate} useHref={useHref}><Outlet /></ThemeProvider>);}
A working project with this setup: examples/react-router.
Astro
Alea runs in React islands. The page itself is Astro; everything Alea draws lives inside an island.
What | Where and how |
|---|---|
| The stylesheet | Import it in the island component: Astro adds the CSS to the pages that use it. |
| The provider | Inside the island, around the Alea components. Each island is its own React tree, so every island that needs the theme has its own ThemeProvider; the choice is saved and a new island reads it when it loads. |
| Needs client:load | The island needs a client directive to become interactive; client:load does it as soon as the page loads. The server still renders its HTML first. |
| Server rendering | Print themeScript() in the head of the page with is:inline and set:html, so the saved theme is on the page before the first paint. |
// src/components/Hello.tsximport '@alea-design/ui/styles.css';import { ThemeProvider, Button } from '@alea-design/ui';export default function Hello() {return (<ThemeProvider defaultTheme="system"><Button>Hello</Button></ThemeProvider>);}
<!-- src/pages/index.astro -->---import Hello from '../components/Hello';import { themeScript } from '@alea-design/ui';---<html lang="en"><head><script is:inline set:html={themeScript()} /></head><body><Hello client:load /></body></html>
A working project with this setup: examples/astro.
TanStack Start
File routing with server rendering by default. Components are plain React: no directives. Start needs Vite 8 or newer, and its plugin goes before the React plugin.
What | Where and how |
|---|---|
| The stylesheet | Import it in the root route, src/routes/__root.tsx. |
| The provider | In the root component, around the Outlet. Passing navigate and useHref (from the router history) makes Alea links move through the router without reloading the page. |
| Client code | All of it: nothing needs a directive. |
| Server rendering | Put ThemeScript next to HeadContent in the head of the document component, with suppressHydrationWarning on the html element. |
// vite.config.tsimport { defineConfig } from 'vite';import { tanstackStart } from '@tanstack/react-start/plugin/vite';import viteReact from '@vitejs/plugin-react';export default defineConfig({ plugins: [tanstackStart(), viteReact()] });
// src/routes/__root.tsximport '@alea-design/ui/styles.css';import { HeadContent, Outlet, Scripts, createRootRoute, useRouter } from '@tanstack/react-router';import { ThemeProvider, ThemeScript } from '@alea-design/ui';export const Route = createRootRoute({ component: Root });function Root() {const router = useRouter();return (<html lang="en" suppressHydrationWarning><head><HeadContent /><ThemeScript /></head><body><ThemeProvider defaultTheme="system" navigate={(to) => router.history.push(to)} useHref={(href) => router.history.createHref(href)}><Outlet /></ThemeProvider><Scripts /></body></html>);}
A working project with this setup: examples/tanstack-start.
Server rendering, in every setup
What | How it works |
|---|---|
| The theme | The server renders the theme you set as defaultTheme (dark if you set none), because it cannot know the saved choice. ThemeScript runs before the first paint and puts the saved choice, or the system one, on the html element. After hydration ThemeProvider reads the saved choice and re-renders with it, with no hydration warning; suppressHydrationWarning on the html element covers the attribute the script set. |
| The window | Alea never reads window, document or localStorage while it renders: it uses effects and subscriptions that only run in the browser. Do the same in your own components. |
| Icons by name | An Icon with name="search" loads its glyph the first time it is used and draws an empty box of the same size until then. It needs no Suspense boundary. The server renders that box too, unless you await preloadIcons(['search']) before rendering. An icon given as a component (icon={Search}) renders at once everywhere. |
import { Icon, preloadIcons } from '@alea-design/ui';await preloadIcons(['search']); // before rendering on the server<Icon name="search" />