alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
410 lines (315 loc) • 14.2 kB
Markdown
---
name: alouette-setup
description: >
Wire alouette into an Expo / React Native app: the metro plugin that compiles
its styles, the CSS entry point and the source globs that decide which classes
survive, AlouetteProvider and the theme variables it needs to render colors on
native, the safe-area provider, and loading the Sora and Chivo Mono weights so
bold text is the right typeface. 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.11.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 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"
/>
</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.