alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
405 lines (310 loc) • 14.2 kB
Markdown
---
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.