Get assistant-ui running in an Expo or React Native app with your first chat screen.
Quick Start
The scaffold is an Expo Router app with the assistant-ui elements installed from the registry (thread and thread list, including the composer and attachment subcomponents) and the AI SDK runtime wired up.
Create a new project
npx assistant-ui@latest create --native my-chat-app
cd my-chat-appConfigure API endpoint
Create a .env file pointing to your chat API:
EXPO_PUBLIC_CHAT_ENDPOINT_URL="http://localhost:3000/api/chat"Start the app
npx expo startManual Setup
If you prefer to add assistant-ui to an existing Expo project, follow these steps.
Install dependencies
npx expo install @assistant-ui/react-native @assistant-ui/ai-sdkOne assistant-ui package per app
Install only @assistant-ui/react-native in a React Native app. @assistant-ui/react is the web distribution: it pulls Radix and the DOM-coupled primitives into a native bundle, and if the two packages ever resolve different versions of the shared core, the second copy of the store breaks useAui. The runtime, tools, model context and external store APIs are all exported from @assistant-ui/react-native.
Setup Backend Endpoint
Create a backend API route using the Vercel AI SDK. This is the same endpoint you'd use with @assistant-ui/react on the web:
import { openai } from "@ai-sdk/openai";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: openai("gpt-6-luna"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { anthropic } from "@ai-sdk/anthropic";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: anthropic("claude-sonnet-5"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}import { google } from "@ai-sdk/google";
import { convertToModelMessages, streamText } from "ai";
export const maxDuration = 30;
export async function POST(req: Request) {
const { messages } = await req.json();
const result = streamText({
model: google("gemini-3.8-flash"),
messages: await convertToModelMessages(messages),
});
return result.toUIMessageStreamResponse();
}Info
This is the same backend you'd use with @assistant-ui/react on the web. If you already have an API route, you can reuse it as-is.
Set up the runtime
import { useChatRuntime, AssistantChatTransport } from "@assistant-ui/ai-sdk";
const API_URL = process.env.EXPO_PUBLIC_API_URL ?? "http://localhost:3000";
export function useAppRuntime() {
return useChatRuntime({
transport: new AssistantChatTransport({
api: `${API_URL}/api/chat`,
}),
});
}Set up Uniwind
The elements are styled with Tailwind classes through Uniwind, using the same color tokens as the web kit. Install it together with Tailwind CSS:
npx expo install uniwind tailwindcssWrap your Metro config. If you ship "use generative" toolkits, install @assistant-ui/metro and wrap the config in its withAui inside withUniwindConfig, which has to stay outermost.
const { getDefaultConfig } = require("expo/metro-config");
const { withUniwindConfig } = require("uniwind/metro");
module.exports = withUniwindConfig(getDefaultConfig(__dirname), {
cssEntryFile: "./global.css",
dtsFile: "./uniwind-types.d.ts",
});Create the CSS entry with the assistant-ui color tokens. Declare the colors directly as --color-* custom properties under @layer theme; Uniwind resolves those on native, while an @theme inline indirection only works on web.
@import "tailwindcss";
@import "uniwind";
@theme {
--radius-sm: 6px;
--radius-md: 8px;
--radius-lg: 10px;
--radius-xl: 14px;
--radius-2xl: 18px;
--radius-3xl: 24px;
}
@layer theme {
:root {
@variant light {
--color-background: oklch(1 0 0);
--color-foreground: oklch(0.141 0.005 285.823);
--color-card: oklch(1 0 0);
--color-card-foreground: oklch(0.141 0.005 285.823);
--color-primary: oklch(0.21 0.006 285.885);
--color-primary-foreground: oklch(0.985 0 0);
--color-secondary: oklch(0.967 0.001 286.375);
--color-secondary-foreground: oklch(0.21 0.006 285.885);
--color-muted: oklch(0.967 0.001 286.375);
--color-muted-foreground: oklch(0.552 0.016 285.938);
--color-accent: oklch(0.967 0.001 286.375);
--color-accent-foreground: oklch(0.21 0.006 285.885);
--color-destructive: oklch(0.577 0.245 27.325);
--color-border: oklch(0.92 0.004 286.32);
--color-input: oklch(0.92 0.004 286.32);
--color-ring: oklch(0.705 0.015 286.067);
}
@variant dark {
--color-background: oklch(0.141 0.005 285.823);
--color-foreground: oklch(0.985 0 0);
--color-card: oklch(0.21 0.006 285.885);
--color-card-foreground: oklch(0.985 0 0);
--color-primary: oklch(0.92 0.004 286.32);
--color-primary-foreground: oklch(0.21 0.006 285.885);
--color-secondary: oklch(0.274 0.006 286.033);
--color-secondary-foreground: oklch(0.985 0 0);
--color-muted: oklch(0.274 0.006 286.033);
--color-muted-foreground: oklch(0.705 0.015 286.067);
--color-accent: oklch(0.274 0.006 286.033);
--color-accent-foreground: oklch(0.985 0 0);
--color-destructive: oklch(0.704 0.191 22.216);
--color-border: oklch(1 0 0 / 10%);
--color-input: oklch(1 0 0 / 15%);
--color-ring: oklch(0.552 0.016 285.938);
}
}
}Import it once at the root of the app, and add uniwind-types.d.ts to your tsconfig.json include so className type-checks on the React Native components.
import "../global.css";Add the elements
The elements install like shadcn components: the assistant-ui CLI detects react-native in your package.json and reads the native registry tree. Create a components.json first so the files land under components/ and lib/:
{
"$schema": "https://ui.shadcn.com/schema.json",
"style": "new-york",
"rsc": false,
"tsx": true,
"tailwind": {
"config": "",
"css": "global.css",
"baseColor": "zinc",
"cssVariables": true
},
"aliases": {
"components": "@/components",
"utils": "@/lib/utils",
"ui": "@/components/ui",
"lib": "@/lib",
"hooks": "@/hooks"
},
"iconLibrary": "lucide"
}npx assistant-ui@latest add thread thread-list
npx expo install --fixThis writes components/assistant-ui/elements/{thread,attachment,thread-list}.aui.tsx and the elements the thread renders (markdown text, reasoning, file, image, tool fallback), components/ui/icon.tsx and lib/utils.ts, and installs what they import (lucide-react-native, react-native-svg, react-native-safe-area-context, react-native-marked, expo-clipboard, expo-image-picker, expo-image-manipulator). npx expo install --fix aligns those Expo modules with your SDK version. The thread reads safe area insets, so wrap the app in SafeAreaProvider from react-native-safe-area-context unless Expo Router already provides it.
Use it in your app
Wrap your app with AssistantRuntimeProvider and render the thread. The primitives are there when you want to build your own elements instead.
import { AssistantRuntimeProvider } from "@assistant-ui/react-native";
import { Thread } from "@/components/assistant-ui/elements/thread.aui";
import { useAppRuntime } from "@/hooks/use-app-runtime";
export default function App() {
const runtime = useAppRuntime();
return (
<AssistantRuntimeProvider runtime={runtime}>
<Thread />
</AssistantRuntimeProvider>
);
}