alouette
Version:
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
411 lines (316 loc) • 14.7 kB
Markdown
<h1 align="center">
alouette
</h1>
<p align="center">
A modern, customizable design system built on top of NativeWind v5 with configurable defaults
</p>
<p align="center">
<a href="https://npmjs.org/package/alouette"><img src="https://img.shields.io/npm/v/alouette.svg?style=flat-square" alt="npm version"></a>
<a href="https://npmjs.org/package/alouette"><img src="https://img.shields.io/npm/dw/alouette.svg?style=flat-square" alt="npm downloads"></a>
<a href="https://npmjs.org/package/alouette"><img src="https://img.shields.io/node/v/alouette.svg?style=flat-square" alt="node version"></a>
<a href="https://npmjs.org/package/alouette"><img src="https://img.shields.io/npm/types/alouette.svg?style=flat-square" alt="types"></a>
</p>
## 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](https://www.nativewind.dev/). 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
```bash
npm install alouette
# or with yarn
yarn add alouette
```
`alouette-icons` is installed automatically as a dependency.
Install the peer dependencies if your app does not already provide them:
```bash
npm install nativewind@5.0.0-preview.4 tailwindcss@^4 \
react-native-reanimated react-native-svg
```
`expo-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.
1. **Metro** — wrap your config with the Alouette helper:
```js
// 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.
2. **CSS entry** — create a `global.css` that re-exports Alouette's tokens and
points `@source` at the directories NativeWind should scan for class names:
```css
/* 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](#color-format-hex-everywhere-oklch-as-a-web-opt-in)):
```css
@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](#custom-palette-bring-your-own). The
`@source` lines stay the same in every case.
Import it once at your app's entry point:
```ts
import "./global.css";
```
3. **PostCSS** — NativeWind compiles Tailwind through PostCSS on both web and
native. `@tailwindcss/postcss` ships as a dependency of `alouette`, so you
only add the config file (use `.mjs` so it loads as ESM regardless of your
package's `"type"`):
```js
// postcss.config.mjs
export default {
plugins: {
"@tailwindcss/postcss": {},
},
};
```
4. **Babel** — use the Expo preset and the Reanimated/worklets plugin:
```js
// babel.config.js
export default function (api) {
api.cache(true);
return {
presets: [["babel-preset-expo", { reanimated: false }]],
plugins: ["react-native-worklets/plugin"],
};
}
```
5. **Provider** — wrap your app in `AlouetteProvider`. It applies the OS
light/dark scheme as the root theme so base tokens resolve app-wide:
```tsx
import { AlouetteProvider } from "alouette";
export function App() {
return <AlouetteProvider>{/* your app */}</AlouetteProvider>;
}
```
6. **Fonts** — Alouette's typography uses Sora (body/heading) and Chivo Mono
(mono). On native, load the weight-specific font files (the standalone
`font-weight` utility has no effect because each weight is a distinct file):
```tsx
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`, `InteractiveBox`, `SafeAreaBox`, `Surface`, `ScopedTheme`, `AccentScope`, `PresenceOne`, `PresenceList`
- **Inputs** — `InputText`, `TextArea`, `Switch`
- **Feedback** — `Message`, `InfoMessage`, `ConfirmationMessage`, `WarningMessage`
- **Data** — `PressableBox`, `PressableListItem`
- **Layout** — `GradientBackground`, `GradientScrollView`
- **Primitives** — `View`, `Text`, `Paragraph`, `Icon`, `ScrollView`, `Stack`, `HStack`, `VStack`, `Separator`
- **Responsive** — `SwitchBreakpointsUsingDisplayNone`, `SwitchBreakpointsUsingNull`, `useCurrentBreakpointName`
For detailed examples and API documentation, visit our [Storybook](https://www.chromatic.com/library?appId=679f9e8df3edc5f07975b64a).
### 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`.
```tsx
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. Components that introduce an accent wrap their children in
`AccentScope`:
```tsx
import { AccentScope, Surface } from "alouette";
<AccentScope accent="info">
<Surface>{/* children use base tokens */}</Surface>
</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):
```ts
// scripts/build-theme.ts
import { writeTheme } from "alouette/theme-generator";
writeTheme({
outDir: "src",
overrides: { brand: { type: "accent", hue: 300 } },
});
```
```json
// 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](#color-format-hex-everywhere-oklch-as-a-web-opt-in)).
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:
```css
/* global.css */
@import "alouette/core.css";
@import "./palette.css";
@source './src';
@source '../node_modules/alouette/src';
```
```tsx
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:
```tsx
import { ArrowLeftRegularIcon } from "alouette-icons/phosphor-icons/ArrowLeftRegularIcon";
function MyComponent() {
return <ArrowLeftRegularIcon />;
}
```
## 🎯 Examples
### Basic Button
```tsx
import { Button } from "alouette";
function MyComponent() {
return (
<Button
accent="brand"
text="Click me"
onPress={() => console.log("Clicked!")}
/>
);
}
```
### Button with Icon
```tsx
import { Button } from "alouette";
import { ArrowLeftRegularIcon } from "alouette-icons/phosphor-icons/ArrowLeftRegularIcon";
function MyComponent() {
return (
<Button accent="brand" icon={<ArrowLeftRegularIcon />} text="Go Back" />
);
}
```
## 🤖 Using an AI agent?
Alouette ships [skills](https://www.npmjs.com/package/@tanstack/intent) that teach
AI coding agents how to use the design system correctly:
```bash
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
- [Component Documentation](https://www.chromatic.com/library?appId=679f9e8df3edc5f07975b64a)
- [GitHub Repository](https://github.com/christophehurpeau/alouette)
## 📄 License
ISC © [Christophe Hurpeau](https://christophe.hurpeau.com)