AZMARA PLATFORM
Guide

Install in React Router

Add @azmr/ui to a React Router app in framework mode (the successor to Remix) with Tailwind CSS v4, a first component and dark mode.

View as Markdown for coding assistants, or see llms.txt.

This guide adds @azmr/ui to a React Router app in framework mode, which is the successor to Remix and is built with Vite. It takes about five minutes. If you do not have an app yet, create one with npx create-react-router@latest my-app; everything below is what you add or change. For a Remix v2 app, upgrade to React Router first.

1. Install

pnpm add @azmr/ui @azmr/core
# or
npm install @azmr/ui @azmr/core

react, react-dom and react-router are already in your app. @azmr/core is a peer dependency, so install it yourself: see Installation.

2. Set up Tailwind CSS v4

The React Router template already uses Tailwind's Vite plugin. If yours does not, install it (pnpm add -D tailwindcss @tailwindcss/vite) and add it to the Vite config:

vite.config.ts
import { reactRouter } from "@react-router/dev/vite";
import tailwindcss from "@tailwindcss/vite";
import { defineConfig } from "vite";

export default defineConfig({
  plugins: [tailwindcss(), reactRouter()],
});

3. Import the stylesheet

In your global CSS, import Tailwind and then the package's stylesheet:

app/app.css
@import "tailwindcss";
@import "@azmr/ui/styles.css";

Load it once in the root route. The root below also sets up dark mode, explained in step 5:

app/root.tsx
import { ThemeProvider } from "@azmr/ui/react";
import {
  DENSITY_COOKIE,
  htmlAttributes,
  parseDensity,
  parseThemePreference,
  readCookie,
  THEME_COOKIE,
} from "@azmr/ui/theme";
import { Links, Meta, Outlet, Scripts, ScrollRestoration, useRouteLoaderData } from "react-router";
import type { Route } from "./+types/root";
import "./app.css";

// Read the saved choice on the server, so the first paint is already in the right mode.
export function loader({ request }: Route.LoaderArgs) {
  const cookie = request.headers.get("cookie");
  return {
    theme: parseThemePreference(readCookie(cookie, THEME_COOKIE)),
    density: parseDensity(readCookie(cookie, DENSITY_COOKIE)),
  };
}

export function Layout({ children }: { children: React.ReactNode }) {
  const data = useRouteLoaderData<typeof loader>("root");
  return (
    <html lang="en-NZ" {...htmlAttributes({ theme: data?.theme, density: data?.density })}>
      <head>
        <meta charSet="utf-8" />
        <meta name="viewport" content="width=device-width, initial-scale=1" />
        <Meta />
        <Links />
      </head>
      <body>
        {children}
        <ScrollRestoration />
        <Scripts />
      </body>
    </html>
  );
}

export default function App({ loaderData }: Route.ComponentProps) {
  return (
    <ThemeProvider defaultTheme={loaderData.theme} defaultDensity={loaderData.density}>
      <Outlet />
    </ThemeProvider>
  );
}

4. Use a component

A route is an ordinary React component, so use @azmr/ui in it directly:

app/routes/home.tsx
import { Button } from "@azmr/ui/button";
import { Input } from "@azmr/ui/input";
import { ThemeToggle } from "../theme-toggle";

export default function Home() {
  return (
    <main className="mx-auto flex max-w-sm flex-col gap-3 p-6">
      <h1 className="text-2xl font-semibold">Hello</h1>
      <label htmlFor="name" className="text-sm font-medium">
        Your name
      </label>
      <Input id="name" placeholder="Aroha" />
      <Button>Say hello</Button>
      <ThemeToggle />
    </main>
  );
}

Run pnpm dev and open the page. You should see a heading, a labelled field and a button, styled with the design tokens. The page is rendered on the server and hydrated in the browser, and the components work in both.

5. Add dark mode

With no setup at all the page already follows the operating system. To let people choose, the root above reads a saved choice from the cookie in its loader and renders it onto <html>, so the first paint is already in the right mode. It also wraps the routes in ThemeProvider, which remembers a new choice in the cookie. A component changes it with useTheme():

app/theme-toggle.tsx
import { Button } from "@azmr/ui/button";
import { useTheme } from "@azmr/ui/react";

export function ThemeToggle() {
  const { resolvedTheme, setTheme } = useTheme();
  const next = resolvedTheme === "dark" ? "light" : "dark";
  return (
    <Button variant="outline" onClick={() => setTheme(next)}>
      Switch to {next} mode
    </Button>
  );
}

Density works the same way: useTheme() also returns density and setDensity, with the values compact, comfortable and spacious.

What was tested

The files above are the files of an app that CI builds. On every change it packs @azmr/ui into a tarball, installs it into that app, type-checks (including React Router's generated route types) and builds it, starts the server and checks that:

  • the page renders the Button and the label on the server;
  • Tailwind found the package's classes inside node_modules, with nothing added to the config;
  • the colour tokens are in the stylesheet;
  • a dark cookie renders data-theme="dark" on <html>, and no cookie renders no attribute.

Next steps

On this page