| 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 | 20.8.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/ThemeVariablesContext.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:
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:
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:
@import "alouette/global.css";
@source "./**/*.{ts,tsx}";
@source "../node_modules/alouette/src/**/*.{ts,tsx,js}";
alouette/global.css is an aggregator of alouette/core.css (structural,
color-free) + alouette/default-palette.css (the default palette). 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:
@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:
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() {
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 — it is the JS mirror of the palette CSS
(gradient stops, placeholderTextColor, native Switch colors, SVG tint), and
the two halves must come from the same palette. Which map you pass follows which
palette CSS the app imports:
| Palette | CSS | themeVariables |
|---|
| default | alouette/global.css | themeVariables from alouette/defaultThemeVariables |
| 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.
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 and a themeVariables module — the way alouette's own
scripts/build-css.ts writes the default palette. The app imports those two
generated files:
import { writeTheme } from "alouette/theme-generator";
writeTheme({ outDir: "src", overrides: { brand: { type: "accent", hue: 300 } } });
@import "alouette/core.css";
@import "./palette.css";
import { AlouetteProvider } from "alouette";
import { themeVariables } from "./themeVariables";
<AlouetteProvider themeVariables={themeVariables}>
<Screen />
</AlouetteProvider>;
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:
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:
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:
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:
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):
@import "alouette/global.css";
Correct:
@import "alouette/global.css";
@source "./**/*.{ts,tsx}";
@source "../node_modules/alouette/src/**/*.{ts,tsx,js}";
Tailwind v4 only emits classes it finds in scanned files. Two failure modes,
both producing the same silent unstyled result with no error:
- No
@source for alouette's source → every alouette utility is purged.
- 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.
- 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:
const { getDefaultConfig } = require("expo/metro-config.js");
module.exports = getDefaultConfig(__dirname);
Correct:
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:
export function App() {
return <Screen />;
}
Correct:
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:
<AlouetteProvider>
<Screen />
</AlouetteProvider>
Correct:
import { themeVariables } from "alouette/defaultThemeVariables";
<AlouetteProvider themeVariables={themeVariables}>
<Screen />
</AlouetteProvider>;
themeVariables has no default: the provider feeds it straight into
ThemeVariablesContext, whose context default is unset. Omitting it is a type
error, and at runtime ScopedTheme and useThemeToken index into an undefined
map and throw. 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/ThemeVariablesContext.ts
HIGH Bold / extrabold fonts not loaded
Wrong:
useFonts({ SoraRegular: Sora_400Regular });
Correct:
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.