-
-
Notifications
You must be signed in to change notification settings - Fork 3.4k
Add NativeWind and Expo Router cursorrules under Mobile Development #298
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
sanketsahu
wants to merge
1
commit into
PatrickJS:main
Choose a base branch
from
sanketsahu:add-nativewind-expo-router-rules
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
Open
Changes from all commits
Commits
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,94 @@ | ||
| --- | ||
| description: "Cursor rules for file-based routing with Expo Router in React Native and Expo apps (web + native)." | ||
| globs: ["app/**/*.{ts,tsx,js,jsx}", "**/_layout.{ts,tsx}"] | ||
| alwaysApply: false | ||
| --- | ||
|
|
||
| # Expo Router | ||
|
|
||
| Expo Router is file-based routing for React Native. Every file inside `app/` becomes a route. Layouts wrap nested routes. The same code runs on iOS, Android, and web. | ||
|
|
||
| ## Directory structure | ||
|
|
||
| - `app/_layout.tsx` is the root layout. It runs once at app start and must render `<Slot />`, `<Stack />`, `<Tabs />`, or `<Drawer />`. | ||
| - `app/index.tsx` is the home route (`/`). | ||
| - Nested folders create nested URLs: `app/settings/profile.tsx` → `/settings/profile`. | ||
| - `app/_layout.tsx` inside a subfolder defines a layout scoped to that subtree. | ||
| - Files starting with `_` are not routes (they're layouts or helpers). | ||
| - Files starting with `+` are special (e.g., `+not-found.tsx`, `+native-intent.tsx`). | ||
|
|
||
| ## Layouts: Stack, Tabs, and groups | ||
|
|
||
| ```tsx | ||
| // app/_layout.tsx | ||
| import { Stack } from "expo-router"; | ||
| export default function RootLayout() { | ||
| return ( | ||
| <Stack> | ||
| <Stack.Screen name="index" options={{ title: "Home" }} /> | ||
| <Stack.Screen name="(tabs)" options={{ headerShown: false }} /> | ||
| <Stack.Screen name="modal" options={{ presentation: "modal" }} /> | ||
| </Stack> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| - Use `<Tabs>` for bottom tab navigators, `<Stack>` for pushed screens, `<Drawer>` for side drawers (requires `expo-router/drawer`). | ||
| - Wrap a layout in a route group `(name)` to share a layout without affecting the URL: `app/(tabs)/home.tsx` → `/home`. Groups are how you avoid `/tabs/home` URLs. | ||
|
|
||
| ## Dynamic and catch-all routes | ||
|
|
||
| - `app/post/[id].tsx` → `/post/123`. Read with `useLocalSearchParams<{ id: string }>()`. | ||
| - `app/blog/[...slug].tsx` is a catch-all, captures `slug` as `string[]`. | ||
| - Optional catch-all: `app/blog/[[...slug]].tsx` matches both `/blog` and `/blog/a/b`. | ||
|
|
||
| ## Linking and navigation | ||
|
|
||
| - `<Link href="/settings">` is the declarative way. It compiles to `<a>` on web, `<Pressable>` on native. | ||
| - `useRouter()` gives imperative `router.push()`, `router.replace()`, `router.back()`, `router.dismiss()`. | ||
| - Prefer `<Link>` for static destinations — it gets correct accessibility on web. | ||
| - Use `router.replace()` for auth flows so the user can't swipe-back to the login screen. | ||
|
|
||
| ## Search params and typed routes | ||
|
|
||
| - Read query params with `useLocalSearchParams<{ q?: string }>()`. They are always strings. | ||
| - For type safety across the whole router, enable typed routes in `app.json`: `"experiments": { "typedRoutes": true }`. Then `href` is autocompleted and unknown paths are type errors. | ||
| - `useGlobalSearchParams` (rarely needed) reads params from the closest matching route, not the current screen. | ||
|
|
||
| ## Modals and presentation styles | ||
|
|
||
| - Modal: `<Stack.Screen name="modal" options={{ presentation: "modal" }} />`. On iOS this slides up; on web it renders as a normal page (style yourself if you want overlay behavior). | ||
| - Transparent modal: `presentation: "transparentModal"` with a Pressable backdrop. | ||
| - Always provide a close affordance — modals on Android close on hardware back, but not all web browsers respect that. | ||
|
|
||
| ## Authentication patterns | ||
|
|
||
| - Use `<Redirect href="/login" />` inside layouts to gate subtrees. | ||
| - For an auth context, wrap providers in `app/_layout.tsx` so they're available to every route. | ||
| - After login, call `router.replace("/(tabs)")` so the auth stack isn't in history. | ||
| - Don't read auth state inside the layout's render and conditionally render `<Redirect>` and routes in the same pass — extract a guard component to avoid flicker. | ||
|
|
||
| ## Web compatibility | ||
|
|
||
| - Files must compile for native AND web. Don't import native-only APIs at the top of a route file — lazy-load with `Platform.OS === 'web'` checks or use `.web.tsx` / `.native.tsx` extensions. | ||
| - For SEO on web, set `<head>` tags inside the route: `import { Stack } from "expo-router"; <Stack.Screen options={{ title: "Page title" }} />` translates to `<title>` on web. | ||
| - Deep linking: configure `scheme` in `app.json`. The router uses the same URL structure on web and native — `/post/123` matches both. | ||
|
|
||
| ## Common pitfalls | ||
|
|
||
| - "Route not found": every leaf route file must default-export a component. Layouts must default-export too. | ||
| - "useLocalSearchParams undefined": params are strings only. Parse to numbers manually. They are `undefined` on the first render of a freshly-pushed route — guard for it. | ||
| - "Two routes match": avoid having both `app/post/[id].tsx` and `app/post/index.tsx` if the index should redirect — use `<Redirect>` explicitly. | ||
| - "Tabs render briefly during auth check": move the auth check into a layout that renders a loading state, not into the tab screens themselves. | ||
| - "Web hot reload loses scroll": expected — use `scrollRestoration` patterns or `react-native-screens` scroll restoration on web. | ||
|
|
||
| ## Don'ts | ||
|
|
||
| - Don't use React Navigation `useNavigation()` types directly. Use `useRouter()` from `expo-router` so types match. | ||
| - Don't manually call `useEffect(() => router.push(...))` for redirects on first render. Use `<Redirect>` instead — it's synchronous and SSR-safe. | ||
| - Don't put business logic in `_layout.tsx`. Layouts should be thin and only set up navigators, providers, and gates. | ||
|
|
||
| ## Reference | ||
|
|
||
| - Docs: https://docs.expo.dev/router/introduction/ | ||
| - API: https://docs.expo.dev/router/reference/ |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,84 @@ | ||
| --- | ||
| description: "Cursor rules for styling React Native and Expo apps with NativeWind (Tailwind CSS for RN)." | ||
| globs: ["**/*.{ts,tsx,js,jsx}"] | ||
| alwaysApply: false | ||
| --- | ||
|
|
||
| # NativeWind | ||
|
|
||
| NativeWind brings Tailwind CSS to React Native. You write `className="px-4 bg-primary"` on RN components, and NativeWind compiles to RN styles at build time. | ||
|
|
||
| ## Setup essentials | ||
|
|
||
| - Babel: add `"nativewind/babel"` to plugins in `babel.config.js`. | ||
| - Metro: wrap the config with `withNativeWind(config, { input: "./global.css" })`. | ||
| - `global.css` must contain the three Tailwind directives (`@tailwind base; @tailwind components; @tailwind utilities;`) and be imported once from the root layout. | ||
| - `tailwind.config.js` must include `content: ["./app/**/*.{ts,tsx}", "./components/**/*.{ts,tsx}"]` — missing paths means classes are tree-shaken away in production. | ||
| - Add `nativewind-env.d.ts` referencing `nativewind/types` so `className` is typed on RN components. | ||
|
|
||
| ## className vs style | ||
|
|
||
| - Prefer `className` for everything that can be expressed statically. The compiler hoists styles, so it's faster than inline `style={...}`. | ||
| - Use `style` only for runtime-computed values (Reanimated shared values, dynamic positioning from gestures, theme tokens not yet expressed in Tailwind). | ||
| - Don't mix the two for the same property — pick one source of truth per style attribute. | ||
|
|
||
| ## What does and doesn't translate to RN | ||
|
|
||
| NativeWind covers a strict subset of Tailwind. These work: | ||
|
|
||
| - Layout: `flex`, `flex-row`, `items-center`, `justify-between`, `gap-2` | ||
| - Spacing: `p-*`, `m-*`, `px-*`, `py-*` | ||
| - Sizing: `w-*`, `h-*`, `w-full`, `max-w-md` | ||
| - Colors and typography: `bg-*`, `text-*`, `font-bold`, `text-lg` | ||
| - Borders and radius: `border`, `border-2`, `rounded-md`, `border-border` | ||
| - Opacity, shadow (web only — for native use `shadow-*` with explicit color) | ||
| - State variants on `Pressable`: `active:bg-primary/80` | ||
|
|
||
| These do NOT work natively (web only): | ||
|
|
||
| - Pseudo-classes like `:hover`, `:focus-visible`, `:before`, `:after` | ||
| - CSS Grid (`grid`, `grid-cols-*`) | ||
| - `transition-*` / `animate-*` (use Reanimated instead) | ||
| - `cursor-*`, `select-*` | ||
|
|
||
| When you need cross-platform: guard with the `web:` prefix or `Platform.OS === 'web'`. | ||
|
|
||
| ## Dark mode | ||
|
|
||
| - Class strategy is the default. Set `darkMode: 'class'` in `tailwind.config.js` (or `'media'` to auto-track system). | ||
| - Use `dark:bg-background-900` style classes — no per-theme component forking. | ||
| - Read the active scheme with `useColorScheme()` from `nativewind` (not from `react-native`), so the class toggles correctly. | ||
|
|
||
| ## Theme tokens | ||
|
|
||
| - Define semantic tokens in `tailwind.config.js` `theme.extend.colors` — e.g., `primary`, `foreground`, `muted`, `card`. Reference these everywhere instead of raw `blue-500`, `gray-700`. | ||
| - Spacing scale uses Tailwind defaults (`0`, `0.5`, `1`, `2`, ... `96`). Don't introduce arbitrary values like `p-[13px]` — extend the scale instead. | ||
| - Tokens map to CSS variables on web automatically, so the same `bg-primary` works in browser and native. | ||
|
|
||
| ## Platform-specific classes | ||
|
|
||
| NativeWind supports platform variants: | ||
|
|
||
| ```tsx | ||
| <View className="p-4 ios:pt-6 android:pt-4 web:hover:bg-muted" /> | ||
| ``` | ||
|
|
||
| Useful for status-bar offsets, hover states (web only), and Android-specific tap feedback. | ||
|
|
||
| ## Performance | ||
|
|
||
| - Keep classNames static where possible. Conditional classes via `cn()` (clsx + tailwind-merge) are fine, but constructing className strings inside render hot paths with template literals leaks compile-time optimization. | ||
| - Lists: don't put complex className on every row in a large `FlatList` if the row recomputes. Extract a memoized row component. | ||
| - Animations: NativeWind cannot animate `bg-*` or other utility classes. Use Reanimated's `useAnimatedStyle` and inline `style` for the animated properties. | ||
|
|
||
| ## Common pitfalls | ||
|
|
||
| - "Class doesn't apply": check that the file path is in `tailwind.config.js` `content` array, and that `global.css` is imported once from the root layout. | ||
| - "Type error on className": missing `nativewind-env.d.ts` or `tsconfig.json` not including it. | ||
| - "Colors look different on web vs native": likely raw RGB / hex vs CSS variable mismatch. Define colors with `hsl(var(--primary))` pattern to share definitions cleanly. | ||
| - "Reanimated values don't animate": don't try to animate Tailwind classes. Use `style={useAnimatedStyle(...)}` for the animated parts. | ||
|
|
||
| ## Reference | ||
|
|
||
| - Docs: https://www.nativewind.dev | ||
| - Repo: https://github.com/nativewind/nativewind | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
🧩 Analysis chain
🌐 Web query:
In current NativeWind docs, are opacity utilities supported on React Native, and which shadow utilities/behaviors differ between native and web?💡 Result:
In current NativeWind documentation, opacity utilities (e.g., opacity-{n}, opacity-[n]) are supported on React Native [1]. Regarding shadow utilities, there are distinct differences between native and web behaviors due to React Native's underlying styling constraints [2][3][4]: 1. Supported Utilities: Standard shadow utilities such as shadow, shadow-{n}, and shadow-none are supported [2]. 2. Web-Only Utilities: Certain complex shadow utilities, including shadow-inner and arbitrary values (shadow-[n]), are explicitly marked as web-only in the documentation [2]. 3. Native-Specific Behavior: - Shadows on native platforms often require an explicit background color to render correctly [2][3]. - NativeWind maps Tailwind shadow classes to React Native-specific properties, primarily utilizing shadowOffset, shadowColor, and shadowOpacity on iOS, and elevation on Android [5][3][6]. - Users are advised that shadow inconsistencies across platforms can occur [7][6]. While newer React Native versions (0.79+) have introduced a cross-platform boxShadow prop, NativeWind's integration of these features is evolving as the ecosystem transitions toward the new architecture [5][6].
Citations:
Fix misleading “web only” wording on line 34 (scope it to shadow, not opacity).
Line 34 currently reads as if both
opacityandshadoware web-only; NativeWind opacity utilities are supported on React Native. Split/reword so “web only” applies to shadow-specific semantics/utilities (e.g.,shadow-inner/ arbitraryshadow-[...]), while keepingopacity-*platform-appropriate (e.g., its own bullet).🤖 Prompt for AI Agents