---
name: alouette-setup
description: >
  Wire alouette into an Expo / React Native app: withAlouetteConfig metro
  plugin, import alouette/global.css with @source globs, AlouetteProvider
  (themeVariables is a required prop — pass the map from
  alouette/defaultThemeVariables), SafeAreaProvider, and loading Sora / Chivo
  Mono font weights. Load when bootstrapping a project, when alouette classes
  render unstyled, or when fonts/bold weights look wrong. Covers ios, android
  and web.
type: lifecycle
library: alouette
library_version: "22.6.0"
sources:
  - "christophehurpeau/alouette:packages/storybook-native-app/metro.config.cjs"
  - "christophehurpeau/alouette:packages/storybook-native-app/postcss.config.mjs"
  - "christophehurpeau/alouette:packages/storybook-native-app/src/global.css"
  - "christophehurpeau/alouette:packages/storybook-native-app/src/App.tsx"
  - "christophehurpeau/alouette:packages/alouette/src/core/AlouetteProvider.tsx"
  - "christophehurpeau/alouette:packages/alouette/src/core/AlouetteProvider.web.tsx"
  - "christophehurpeau/alouette:packages/alouette/src/core/NativeThemeVariablesContext.ts"
  - "christophehurpeau/alouette:packages/alouette/metro.cjs"
---

# alouette — Setup

alouette is styled with NativeWind v5 / Tailwind CSS v4. An app needs five
things wired before any component renders correctly: the metro plugin, the
PostCSS config, the CSS entry with source globs, the provider, and the fonts.
Everything targets ios/android/web from the same code.

## Setup

`metro.config.cjs`:

```js
const { withAlouetteConfig } = require("alouette/metro.cjs");
const { getDefaultConfig } = require("expo/metro-config.js");

const config = getDefaultConfig(__dirname);

module.exports = withAlouetteConfig(config);
```

`postcss.config.mjs` at the **app package root** — this is what actually runs
Tailwind. `@tailwindcss/postcss` ships as a dependency of `alouette`, so apps do
not install it; they only add this config file:

```js
export default { plugins: { "@tailwindcss/postcss": {} } };
```

Use the `.mjs` extension so it loads as ESM regardless of the package's `"type"`
field.

`src/global.css` (imported once, at the app entry). Add an `@source` for
**alouette's source** and one for **your own app source** — both are scanned
independently of the JS bundle, and anything not covered is purged:

```css
@import "alouette/global.css";

@source "./**/*.{ts,tsx}"; /* the app's own className / tv() literals */
@source "../node_modules/alouette/src/**/*.{ts,tsx,js}"; /* alouette source */
```

`alouette/global.css` is an aggregator of `alouette/core.css` (structural,
color-free) + `alouette/default-palette.css` (the default palette, sRGB hex).
Wide-gamut color is opt-in — add `@import "alouette/default-palette-oklch.css";`
after it to give web the display-p3 ramp. To ship a custom palette instead of the
default, import `core.css` + your own generated palette CSS and pass the generated
`themeVariables` to `AlouetteProvider` — see alouette-theming/SKILL.md. The
`@source` lines are unchanged.

In a monorepo where alouette is hoisted to the **repo root** `node_modules`
(Yarn `node-modules` linker, pnpm hoisted, etc.), the path resolves from the
repo root, not the app — adjust the depth accordingly:

```css
@source "../../../node_modules/alouette/src/**/*.{ts,tsx,js}";
```

A wrong glob matches zero files and fails **silently** (no error) — utilities
are simply purged and components render unstyled.

App entry — load fonts (native), then wrap the tree in `AlouetteProvider`:

```tsx
import "./global.css";
import {
  Sora_400Regular as SoraRegular,
  Sora_700Bold as SoraBold,
  Sora_800ExtraBold as SoraExtraBold,
  useFonts,
} from "@expo-google-fonts/sora";
import { AlouetteProvider } from "alouette";
import { themeVariables } from "alouette/defaultThemeVariables";

export function App() {
  // Native font loading. On web, load the same fonts via a Google Fonts
  // <link> instead (see "Web: load fonts from Google Fonts" below).
  const [fontsLoaded] = useFonts({ SoraRegular, SoraBold, SoraExtraBold });
  if (!fontsLoaded) return null;

  return (
    <AlouetteProvider themeVariables={themeVariables}>
      <Screen />
    </AlouetteProvider>
  );
}
```

`AlouetteProvider` reads the OS color scheme (`useColorScheme`) and applies
`light` or `dark` as the root theme, so base tokens resolve app-wide.

`themeVariables` is **required** by the prop type on every platform, but it is
consumed **only on native**, where it is the mirror of the palette CSS for the
props that can't take a className (`placeholderTextColor`, native `Switch`
colors, gradient stops, SVG tint). The web build of `AlouetteProvider` ignores
it: web theming is a className the palette CSS resolves. Pass the map that
matches the palette CSS the app imports:

| Palette              | CSS                                             | `themeVariables`                                       |
| -------------------- | ----------------------------------------------- | ------------------------------------------------------ |
| default              | `alouette/global.css`                           | `themeVariables` from `alouette/defaultThemeVariables`  |
| default + wide gamut | \+ `alouette/default-palette-oklch.css`         | unchanged — the overlay is CSS-only                     |
| the app's own        | `alouette/core.css` + its generated palette CSS | its generated `themeVariables` module                   |

The default map lives at the `alouette/defaultThemeVariables` subpath — it is not
re-exported from the `alouette` root entry. It is sRGB hex on every platform,
which is the only format React Native can parse; the wide-gamut ramp is purely a
web stylesheet and needs no matching map.

An app that ships its own palette does not generate it at runtime: a build script
calls `writeTheme` from `alouette/theme-generator`, which writes **both** halves
to disk — the palette CSS (plus an optional OKLCH overlay, `palette-oklch.css`)
and a `themeVariables` module in hex — the way alouette's own
`scripts/build-css.ts` writes the default palette. The app imports the generated
files:

```ts
// scripts/build-theme.ts
import { writeTheme } from "alouette/theme-generator";

writeTheme({ outDir: "src", overrides: { brand: { type: "accent", hue: 300 } } });
```

```css
/* src/global.css — core.css + the generated palette, not alouette/global.css */
@import "alouette/core.css";
@import "./palette.css"; /* writeTheme output */
@import "./palette-oklch.css"; /* optional wide-gamut half */
```

```tsx
import { AlouetteProvider } from "alouette";
import { themeVariables } from "./themeVariables"; // writeTheme output

<AlouetteProvider themeVariables={themeVariables}>
  <Screen />
</AlouetteProvider>;
```

The generated `themeVariables` module is hex on every platform and has no OKLCH
counterpart — the web provider ignores `themeVariables` entirely, so only the hex
map matters. Pass `srgbOnly: true` to skip the OKLCH CSS.

Those two imports are the only wiring difference — everything else on this page
(metro, postcss, `@source` globs, fonts) is identical. See
alouette-theming/SKILL.md for the palette params `writeTheme` takes.

Sora (body + heading) is the only required font. Add Chivo Mono **only if** the
app uses `font-mono` utilities:

```tsx
import {
  ChivoMono_400Regular as ChivoMonoRegular,
  ChivoMono_700Bold as ChivoMonoBold,
  ChivoMono_800ExtraBold as ChivoMonoExtraBold,
} from "@expo-google-fonts/chivo-mono";

useFonts({
  SoraRegular, SoraBold, SoraExtraBold,
  ChivoMonoRegular, ChivoMonoBold, ChivoMonoExtraBold,
});
```

### SafeAreaProvider (only if needed)

Don't add `SafeAreaProvider` preemptively — many setups (e.g. expo-router)
already provide one. Add it only if a component throws a safe-area context error:

```tsx
import { SafeAreaProvider } from "alouette";

<SafeAreaProvider>
  <AlouetteProvider themeVariables={themeVariables}>
    <Screen />
  </AlouetteProvider>
</SafeAreaProvider>;
```

### Web: load fonts from Google Fonts

On web, prefer a Google Fonts stylesheet over `useFonts`. With Expo Router, add
`app/+html.tsx`:

```tsx
import { ScrollViewStyleReset } from "expo-router/html";
import type { PropsWithChildren } from "react";

export default function Root({ children }: PropsWithChildren) {
  return (
    <html lang="en">
      <head>
        <meta charSet="utf-8" />
        <link rel="preconnect" href="https://fonts.googleapis.com" />
        <link
          rel="stylesheet"
          href="https://fonts.googleapis.com/css2?family=Sora:wght@400;700;800&display=swap"
        />
        <ScrollViewStyleReset />
      </head>
      <body>{children}</body>
    </html>
  );
}
```

Append `&family=Chivo+Mono:wght@400;700;800` to the URL if you use `font-mono`.

## Common Mistakes

### CRITICAL Missing postcss.config — Tailwind never runs

Symptom: components render unstyled (only alouette's hotpink `body` fallback
shows), and the build logs spam `Warning: Unknown at rule: @utility` /
`@source` / `@theme`. Those warnings are the diagnostic signature — Tailwind
directives are reaching lightningcss un-expanded because Tailwind never ran.

Mechanism: `withAlouetteConfig` → `withNativewind` delegates CSS compilation to
Expo's Metro transform worker, which runs Tailwind **only if it finds a
`postcss.config` file at the project root**. Absent → CSS passes straight to
lightningcss verbatim and zero utilities are emitted.

Fix — add `postcss.config.mjs` at the app package root:

```js
export default { plugins: { "@tailwindcss/postcss": {} } };
```

`@tailwindcss/postcss` is a dependency of `alouette`, so no install is needed.

Source: packages/storybook-native-app/postcss.config.mjs

### CRITICAL global.css missing @source, or wrong path in a monorepo

Wrong (no `@source`, or a path that resolves to nothing):

```css
@import "alouette/global.css";
```

Correct:

```css
@import "alouette/global.css";

@source "./**/*.{ts,tsx}"; /* the app's own classes */
@source "../node_modules/alouette/src/**/*.{ts,tsx,js}"; /* alouette source */
```

Tailwind v4 only emits classes it finds in scanned files. Two failure modes,
both producing the same silent unstyled result with **no error**:

1. No `@source` for alouette's source → every alouette utility is purged.
2. No `@source` for the app's own source → the app's own classes (e.g.
   arbitrary values like `from-[#f39c12]`, `bg-linear-to-t`) are purged while
   alouette's still work — easy to misdiagnose.
3. In a monorepo where alouette is hoisted to the **repo root**
   `node_modules`, `../node_modules/alouette/src` resolves to nothing. Use the
   correct depth, e.g. `@source "../../../node_modules/alouette/src/**/*.{ts,tsx,js}"`.

Note: `@source` is a text-scan of alouette's shipped `src/*.tsx` (the verbatim
`className` / `tv()` string literals) — independent of the JS bundle, which
Metro resolves to the compiled `dist`. The two pipelines are decoupled, which is
why the scan targets `src` and not the build output.

Source: packages/storybook-native-app/src/global.css

### CRITICAL Metro config omits withAlouetteConfig

Wrong:

```js
const { getDefaultConfig } = require("expo/metro-config.js");
module.exports = getDefaultConfig(__dirname);
```

Correct:

```js
const { withAlouetteConfig } = require("alouette/metro.cjs");
const { getDefaultConfig } = require("expo/metro-config.js");
module.exports = withAlouetteConfig(getDefaultConfig(__dirname));
```

`withAlouetteConfig` enables the NativeWind / react-native-css transform; without
it, `className` styles never compile on native.

Source: packages/storybook-native-app/metro.config.cjs, packages/alouette/metro.cjs

### CRITICAL App tree not wrapped in AlouetteProvider

Wrong:

```tsx
export function App() {
  return <Screen />;
}
```

Correct:

```tsx
import { AlouetteProvider } from "alouette";
import { themeVariables } from "alouette/defaultThemeVariables";

export function App() {
  return (
    <AlouetteProvider themeVariables={themeVariables}>
      <Screen />
    </AlouetteProvider>
  );
}
```

`AlouetteProvider` applies the OS light/dark scheme as the root `ScopedTheme`.
Without it, base tokens (`bg-surface`, `text-sharp`, `text-accent`) have no
resolved values and components render with missing colors.

Source: packages/alouette/src/core/AlouetteProvider.tsx

### CRITICAL AlouetteProvider without themeVariables

Wrong:

```tsx
<AlouetteProvider>
  <Screen />
</AlouetteProvider>
```

Correct:

```tsx
import { themeVariables } from "alouette/defaultThemeVariables";

<AlouetteProvider themeVariables={themeVariables}>
  <Screen />
</AlouetteProvider>;
```

`themeVariables` has no default: the provider feeds it straight into
`NativeThemeVariablesContext`, whose context default is unset. Omitting it is a
type error, and on native `ScopedTheme` then indexes into an undefined map and
throws (web survives it — the web provider never reads the prop, which is
exactly how this ships broken to device after testing fine in a browser). Pass
the map matching the palette CSS the app imports — the
default palette's map from `alouette/defaultThemeVariables` (not exported from
the `alouette` root entry), or, for a custom palette, the `themeVariables` module
`writeTheme` generated alongside the palette CSS. Don't call into
`alouette/theme-generator` here: it is a node-only build-time API, not a runtime
call.

Source: packages/alouette/src/core/AlouetteProvider.tsx, src/core/NativeThemeVariablesContext.ts

### HIGH Bold / extrabold fonts not loaded

Wrong:

```tsx
useFonts({ SoraRegular: Sora_400Regular });
```

Correct:

```tsx
useFonts({
  SoraRegular: Sora_400Regular,
  SoraBold: Sora_700Bold,
  SoraExtraBold: Sora_800ExtraBold,
});
```

On native, bold and extrabold are distinct font files. If only the regular
weight is loaded, `font-body-bold` / `font-heading-extrabold` silently fall back
to regular. (Load the matching Chivo Mono weights too, but only if the app uses
`font-mono`.)

Source: packages/storybook-native-app/src/App.tsx

See also: alouette-theming/SKILL.md — once setup is done, the token/accent model
is what you style with.
