Setting up a project

What init changes, what it leaves alone, and the three things it hands back to you.

bunx delacour@latest init

Most projects never run this

add does everything on this page the first time it is used, with the layout inferred. Run init yourself for the two decisions inference cannot make: putting the components in a package several apps share, and a source directory this project spells its own way.

Two questions, both skippable with --defaults: where the components should live, and what your source directory is called. Everything else is read off the project — the package manager from the lockfile, the Expo SDK from package.json, the path aliases from tsconfig.json.

You can add components in the same run:

bunx delacour@latest init button input field

On a project that already has a native-components.json, init says so and does not rewrite it — --force is how you mean it. Component names given alongside are still added, as a plain add: a file you have edited is a conflict it asks about, rather than one it replaces.

What it changes

native-components.json

Written next to your components. In a monorepo that is the package you ran it from — see Monorepos.

metro.config.js

Wrapped with Uniwind's config, outermost:

metro.config.js
const { withUniwindConfig } = require("uniwind/metro");
const { getDefaultConfig } = require("expo/metro-config");

const config = getDefaultConfig(__dirname);

module.exports = withUniwindConfig(config, {
  cssEntryFile: "./src/styles/global.css",
  dtsFile: "./src/uniwind-types.d.ts",
});

If there is no Metro config, one is written. If the export is not a shape the CLI can rewrite safely, it prints the snippet and asks you to add it rather than guessing at an unfamiliar file.

Outermost is load-bearing

withUniwindConfig installs the transformer that compiles className into styles. A wrapper applied after it can replace config.transformer, and every component then renders unstyled with nothing logged. delacour doctor checks the ordering, not just the presence.

Your Tailwind entry

An @import for the theme and one @source per directory holding copied code, inside a marked block:

src/styles/global.css
@import "tailwindcss";
@import "uniwind";

@source "../**/*.{ts,tsx}";

/* delacour:start — managed by `delacour init`, edit outside this block */
@import "./index.css";

@source "../components/ui";
@source "../hooks";
@source "../lib";
/* delacour:end */

Anything you write outside the markers is left alone, and re-running init replaces the block rather than appending a second one.

The theme, and the provider

init adds the styles item — tokens.css, theme.css, base.css and the uniwind-env.d.ts type shim — and installs tailwindcss and uniwind. It adds the provider item too, so DelacourProvider is in your ui directory before you mount it.

theme.css is the one of those that is yours. Replace it whole with the theme.css tab from Theme, or paste a shadcn globals.css over it and run delacour theme. The rest is plumbing and re-runs of init leave it alone.

What it does not change

tsconfig.json and app.config.ts are read, never written.

For tsconfig.json that is a deliberate trade. The CLI looks for a wildcard alias like "@/*": ["./src/*"] and, if it finds one, records the matching alias per directory. If it finds none, imports are written as relative paths instead — which Metro resolves whether or not experiments.tsconfigPaths is on. Creating an alias you did not ask for is a larger change than the imports it saves, and in a monorepo the same alias often already means something else.

The three things left for you

init prints these, and delacour doctor checks all of them afterwards.

Import the CSS entry

app/_layout.tsx
import "@/styles/global.css";

First statement of the root layout. withUniwindConfig's cssEntryFile names the file for the transformer; it does not put it in the bundle. Skip this and the app builds, boots and renders every component completely unstyled, with nothing logged.

Mount DelacourProvider

Every pressable in the library is a Gesture Handler detector, and a detector outside a GestureHandlerRootView never receives a touch. Nothing throws — the button just does not respond. DelacourProvider is that root, plus the other layers the components read from, and init has already copied it in.

app/_layout.tsx
import "@/styles/global.css";
import { DelacourProvider } from "@/components/ui/provider";
import { Stack } from "expo-router";

export default function RootLayout() {
  return (
    <DelacourProvider>
      <Stack />
    </DelacourProvider>
  );
}

See Provider for what each of its layers is doing. delacour doctor passes on either DelacourProvider or a hand-mounted GestureHandlerRootView.

Turn on tsconfigPaths — only if you use aliases

app.config.ts
experiments: {
  tsconfigPaths: true,
}

Metro does not read tsconfig path mappings without it, so @/components/ui/button fails to resolve. If native-components.json has an empty aliases object, your imports are relative and this does not apply.

Rebuild after a native module lands

add tells you when it installed one. A JavaScript reload will not pick it up — the module is not in the binary yet.

npx expo run:ios     # or run:android

Verify

bunx delacour@latest add button
bunx delacour@latest doctor
import { Button } from "@/components/ui/button";

<Button onPress={() => console.log("pressed")}>Press me</Button>;

A string child is wrapped in a Button.Label for you. If the button renders but does not respond, DelacourProvider is missing above it.

On this page