alouette
v24.2.0
Published
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
Readme
Introduction
Alouette provides a comprehensive set of universal components that render on both
web and React Native, styled entirely through Tailwind className via
NativeWind v5. Themes, accents, and design tokens
ship as CSS custom properties that cascade through the tree, so components stay
declarative and consistent across platforms.
🚀 Getting Started
Prerequisites
- Node.js >= 22.18.0 (includes Corepack for package management)
- A React Native / Expo app using Metro, or a web app using NativeWind v5
Installation
npm install alouette
# or with yarn
yarn add alouettealouette-icons is installed automatically as a dependency.
Install the peer dependencies if your app does not already provide them:
npm install [email protected] tailwindcss@^4 \
react-native-reanimated react-native-svgexpo-web-browser, react-dom, and react-native-reanimated are optional peers
— add them only if your target needs them (react-dom for web, react-native-reanimated
for native animations, expo-web-browser for ExternalLink).
Configuration
Alouette relies on NativeWind v5's Metro + PostCSS pipeline. NativeWind discovers
themes and utilities from the imported global.css and scans your sources via
@source directives — there is no JS config to maintain.
- Metro — wrap your config with the Alouette helper:
// metro.config.cjs
const { withAlouetteConfig } = require("alouette/metro.cjs");
const { getDefaultConfig } = require("expo/metro-config.js");
module.exports = withAlouetteConfig(getDefaultConfig(__dirname));withAlouetteConfig currently wraps withNativewind and forwards your options
unchanged; use it anyway, so alouette can add required metro wiring later without
a breaking change on your side.
- CSS entry — create a
global.cssthat re-exports Alouette's tokens and points@sourceat the directories NativeWind should scan for class names:
/* global.css */
@import "alouette/global.css";
@source './src'; /* your own className / tv() literals */
@source '../node_modules/alouette/src'; /* alouette's source — required */Tailwind only emits classes it finds while scanning @source paths, so both
your app's source and alouette's source must be covered or the matching
utilities are silently purged. In a monorepo where alouette is hoisted to the
repo root node_modules, adjust the depth (e.g.
@source '../../../node_modules/alouette/src'); a path that resolves to nothing
fails silently with no error.
alouette/global.css is a convenience aggregator of alouette/core.css
(structural, color-free) + alouette/default-palette.css (the default palette in
sRGB hex). Wide-gamut color is opt-in — add
alouette/default-palette-oklch.css after it to give web the display-p3 ramp
(see Color format):
@import "alouette/global.css";
@import "alouette/default-palette-oklch.css"; /* optional */To ship your own palette instead of the default, import core.css + your own
generated palette CSS — see Custom palette. The
@source lines stay the same in every case.
Import it once at your app's entry point:
import "./global.css";- PostCSS — NativeWind compiles Tailwind through PostCSS on both web and
native.
@tailwindcss/postcssships as a dependency ofalouette, so you only add the config file (use.mjsso it loads as ESM regardless of your package's"type"):
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};- Babel — use the Expo preset and the Reanimated/worklets plugin:
// babel.config.js
export default function (api) {
api.cache(true);
return {
presets: [["babel-preset-expo", { reanimated: false }]],
plugins: ["react-native-worklets/plugin"],
};
}- Provider — wrap your app in
AlouetteProvider. It applies the OS light/dark scheme as the root theme so base tokens resolve app-wide:
import { AlouetteProvider } from "alouette";
export function App() {
return <AlouetteProvider>{/* your app */}</AlouetteProvider>;
}- Fonts — Alouette's typography uses Sora (body/heading) and Chivo Mono
(mono). On native, load the weight-specific font files (the standalone
font-weightutility has no effect because each weight is a distinct file):
import {
Sora_400Regular as SoraRegular,
Sora_700Bold as SoraBold,
Sora_800ExtraBold as SoraExtraBold,
useFonts,
} from "@expo-google-fonts/sora";
import {
ChivoMono_400Regular as ChivoMonoRegular,
ChivoMono_700Bold as ChivoMonoBold,
ChivoMono_800ExtraBold as ChivoMonoExtraBold,
} from "@expo-google-fonts/chivo-mono";
const [fontsLoaded] = useFonts({
SoraRegular,
SoraBold,
SoraExtraBold,
ChivoMonoRegular,
ChivoMonoBold,
ChivoMonoExtraBold,
});🎨 Core Features
Components
Alouette ships a universal component set styled through className:
- Actions —
Button,ExternalLinkButton,InternalLinkButton,IconButton - Containers —
Box(raised with thesurfaceutility),InteractiveBox,SafeAreaBox,Popover,ScopedTheme,AccentScope,PresenceOne,PresenceList - Inputs —
InputText,InputCode,InputTextAutocomplete,TextArea,Switch - Feedback —
Message,InfoMessage,ConfirmationMessage,WarningMessage - Data —
PressableBox,PressableListItem - Layout —
GradientBackground,GradientScrollView - Primitives —
View,Text,Paragraph,Icon,ScrollView,Separator - Responsive —
SwitchBreakpointsUsingDisplayNone,SwitchBreakpointsUsingNull,useCurrentBreakpointName
For detailed examples and API documentation, visit our Storybook.
Autocomplete
InputTextAutocomplete is a text input backed by a filtered listbox, built on
downshift:
import { InputTextAutocomplete } from "alouette";
<InputTextAutocomplete
aria-label="Fruit"
placeholder="Search a fruit..."
options={[
{ label: "Apple", value: "apple" },
{ label: "Banana", value: "banana" },
{ label: "Durian (sold out)", value: "durian", disabled: true },
]}
onValueChange={setFruit}
/>;Options are filtered by a case-insensitive match on their label; pass
filterOption for anything else. Selection is controllable through
value/defaultValue/onValueChange, and the text through
inputValue/defaultInputValue/onInputValueChange.
Text styling
<Text> has no variant props — style it entirely via className. Family and
weight are combined into a single utility (font-body, font-body-bold,
font-heading-extrabold, font-mono, …); size uses standard Tailwind
text-*; color uses tokens like text-sharp, text-muted, text-accent.
import { Text } from "alouette";
<Text className="text-base">Body</Text>;
<Text className="font-heading-extrabold text-4xl">Title</Text>;
<Text className="font-mono text-xs text-muted">Code</Text>;Theming and accents
Themes are sets of CSS variables (light, dark, light_brand, dark_info, …)
applied by ScopedTheme. Child components use base tokens (bg-surface,
text-accent, border-muted, …) and inherit the correct values from the nearest
theme scope. Most components take an accent prop that re-themes their subtree;
AccentScope is for grouping several children that don't take one:
import { AccentScope, Box, Text } from "alouette";
<Box accent="info" className="surface">
{/* children use base tokens */}
</Box>;
<AccentScope accent="info">
<Text className="text-accent">Title</Text>
<Text className="text-accent">Subtitle</Text>
</AccentScope>;The two platforms resolve a theme differently, and only one of them uses CSS:
web renders the theme name as a className the palette CSS resolves, while native
pushes the theme's variables through NativeWind's VariableContextProvider, from
the themeVariables map you give AlouetteProvider. The .<theme> blocks are
therefore emitted inside @supports (display: contents), which the native
compiler drops — native compiles the @theme defaults and nothing else, and gets
every themed value from the map.
That also means colors are the one part of alouette you cannot read from JS: there is no exported token hook, so style every color with a token className.
Custom palette (bring your own)
An app can generate its own coherent palette for the existing accents (brand,
danger, info, success, warning, plus grayscale) while staying on
alouette's OKLCH ramp — and ship only that palette, no default CSS.
A theme has two coupled outputs: the palette CSS (what web resolves, and the
source of every className token) and the runtime themeVariables map (what
native resolves through VariableContextProvider).
alouette/theme-generator produces both from per-accent hue params — the same
module alouette's own scripts/build-css.ts uses for the default palette.
The app generates its palette the same way: a build script calls writeTheme,
which writes both files to disk. Override only the accents you want to re-color
(the rest inherit alouette's defaults; omit overrides entirely to reproduce the
default palette):
// scripts/build-theme.ts
import { writeTheme } from "alouette/theme-generator";
writeTheme({
outDir: "src",
overrides: { brand: { type: "accent", hue: 300 } },
});// package.json
"scripts": {
"build:theme": "node --experimental-strip-types scripts/build-theme.ts"
}That writes three files, all marked DO NOT EDIT and already formatter-stable:
src/palette.css + src/themeVariables.ts in sRGB hex (complete on their own),
and src/palette-oklch.css, the OKLCH / display-p3 overlay. Re-run it whenever
the palette params change, and commit the output. Pass srgbOnly: true to skip
the OKLCH file entirely. generateTheme returns the same content in memory
({ css, oklchCss, themeVariables, oklchThemeVariables }) if the app would
rather write the files itself.
The OKLCH file is how the palette gets wide-gamut colors on web while staying
renderable on native (see Color format).
Opting in is a CSS import: add ./palette-oklch.css after ./palette.css.
There is no OKLCH counterpart to themeVariables.ts — the web build of
AlouetteProvider ignores themeVariables entirely, so the hex map native
consumes is the only one.
Import alouette/core.css + your generated palette (instead of
alouette/global.css), and pass the generated map to AlouetteProvider so
native token reads match your palette CSS:
/* global.css */
@import "alouette/core.css";
@import "./palette.css";
@source './src';
@source '../node_modules/alouette/src';import { AlouetteProvider } from "alouette";
import { themeVariables } from "./themeVariables";
export function App() {
return (
<AlouetteProvider themeVariables={themeVariables}>
{/* your app */}
</AlouetteProvider>
);
}PaletteSpec params per accent: type ("accent" | "brightAccent" |
"grayscale"), hue (0–360), optional hueHi / hueLo (hue ramp across the
lightness range) and intensity (chroma multiplier). The accent set is fixed —
generateTheme re-colors the existing accents, it does not add new ones.
Color format: hex everywhere, OKLCH as a web opt-in
Every palette is computed in OKLCH and emitted twice:
- sRGB hex — the baseline, on every platform. React Native's color parser
accepts hex / rgb / hsl / hwb only, so
oklch()must never reach it. - OKLCH with display-p3 chroma headroom — web only. Same lightness and hue ramp, more chroma, so accents are visibly more vivid on wide-gamut screens.
A theme has two halves — the palette CSS and the themeVariables map — but only
the CSS half has an OKLCH variant in play. Opting in is one extra import, not a
platform check:
| | palette CSS | themeVariables map |
| -------------- | ----------------------------------------- | -------------------------------------------- |
| hex (default) | alouette/default-palette.css | alouette/defaultThemeVariables (= …Srgb) |
| + OKLCH on web | also alouette/default-palette-oklch.css | unchanged — the overlay is CSS-only |
The OKLCH CSS re-declares the same variables inside @supports (color: oklch(0 0 0)),
so it is additive: browsers apply the override, and the native compiler drops the
feature query and keeps the hex even if the file ends up in a shared CSS entry.
The map stays on hex in every case — it is read only on native, where oklch()
cannot be parsed at all.
Because the map is unambiguously hex, code that parses a token value (a test
reading hex channels, say) can read alouette/defaultThemeVariables directly.
Icons
Icons come from the integrated alouette-icons package:
import { ArrowLeftRegularIcon } from "alouette-icons/phosphor-icons/ArrowLeft";
function MyComponent() {
return <ArrowLeftRegularIcon />;
}🎯 Examples
Basic Button
import { Button } from "alouette";
function MyComponent() {
return (
<Button
accent="brand"
text="Click me"
onPress={() => console.log("Clicked!")}
/>
);
}Button with Icon
import { Button } from "alouette";
import { ArrowLeftRegularIcon } from "alouette-icons/phosphor-icons/ArrowLeft";
function MyComponent() {
return (
<Button accent="brand" icon={<ArrowLeftRegularIcon />} text="Go Back" />
);
}🤖 Using an AI agent?
Alouette ships skills that teach AI coding agents how to use the design system correctly:
npx @tanstack/intent@latest install🏗️ Architecture
- Universal Design — components render across web and native from one API
- NativeWind v5 styling — Tailwind
className; animations are CSS@keyframes+--animate-*tokens, run on native via Reanimated - Token-based theming — CSS custom properties cascade through
ScopedTheme; light/dark + accent scopes - Accessibility — proper ARIA / accessibility attributes
- Type Safety — built with TypeScript
📚 Documentation
📄 License
ISC © Christophe Hurpeau
