Skip to content
Alea
Pro
  1. Alea
  2. Development
  3. Framework guides

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 stylesheetImport it once in the entry file, src/main.tsx.
The providerWrap 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 codeAll of it: nothing needs a directive.
Server renderingNone, so ThemeScript is not needed.
// src/main.tsx
import '@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 stylesheetImport it in the root layout, app/layout.tsx.
The providerA 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 renderingPut 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.tsx
import '@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 stylesheetImport it in pages/_app.tsx: Next.js allows global CSS from a package there.
The providerWrap the page component in ThemeProvider in pages/_app.tsx.
Client codeAll of it: nothing needs a directive.
Server renderingPut 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.tsx
import '@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.tsx
import { 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 stylesheetImport it in app/root.tsx.
The providerIn 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 codeAll of it: nothing needs a directive.
Server renderingPut ThemeScript in the head of the Layout export of the same file, with suppressHydrationWarning on the html element.
// app/root.tsx
import '@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 stylesheetImport it in the island component: Astro adds the CSS to the pages that use it.
The providerInside 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:loadThe 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 renderingPrint 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.tsx
import '@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 stylesheetImport it in the root route, src/routes/__root.tsx.
The providerIn 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 codeAll of it: nothing needs a directive.
Server renderingPut ThemeScript next to HeadContent in the head of the document component, with suppressHydrationWarning on the html element.
// vite.config.ts
import { 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.tsx
import '@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 themeThe 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 windowAlea 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 nameAn 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" />