@medway-ui/native
v1.1.2
Published
Medway UI components for React Native (iOS/Android via Expo)
Maintainers
Readme
@medway-ui/native
React Native components for iOS and Android (Expo). Same visual API as @medway-ui/core, built on NativeWind + @rn-primitives.
Minimum recommended version for npm brownfield setup: 1.1.2+
Installation
npm install @medway-ui/native @medway-ui/core @medway-ui/icons \
class-variance-authority clsx tailwind-merge nativewind \
react-native-reanimated react-native-svg
# Expo (aligns versions with your SDK)
npx expo install react-native-reanimated react-native-svgBrownfield apps that already use babel-plugin-module-resolver with @/ aliases pointing at the app src/ also need:
npm install -D babel-plugin-module-resolverBrownfield setup (Expo + legacy UI / Restyle)
NativeWind applies only to @medway-ui/native + screens/components that use the library (e.g. src/pages/medway-ui/, partially migrated features).
Reference implementation: test-prep-app (metro.config.js, babel.config.js, tailwind.config.js, src/pages/medway-ui/).
1. metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withMedwayUi } = require("@medway-ui/native/metro");
module.exports = withMedwayUi(getDefaultConfig(__dirname), {
projectRoot: __dirname,
// linkedMedwayUiRoot: auto-detected (npm or monorepo)
});| Scenario | linkedMedwayUiRoot |
| --- | --- |
| npm packages (npm install) | Omit — auto-detects node_modules/@medway-ui |
| Monorepo with local clone | "../medway-ui" (optional; also auto-detected) |
| Symlink file:../medway-ui/native | Auto-detected as monorepo |
Do not set
resolveNativeSource: false— the compiled lib still references internal@/aliases. The default (true) resolves tonative/srcand works with npm.
2. tailwind.config.js
const { createMedwayUiTailwindConfig } = require("@medway-ui/native/tailwind");
module.exports = createMedwayUiTailwindConfig({
projectRoot: __dirname,
content: [
"./src/pages/medway-ui/**/*.{ts,tsx}",
// add extra folders that use className + @medway-ui/native:
// "./src/pages/MyFeature/**/*.{ts,tsx}",
],
});createMedwayUiTailwindConfig already includes globs for node_modules/@medway-ui/native/src and loads tokens from @medway-ui/core (Node 22+ compatible).
3. babel.config.js
The app default preset must not enable NativeWind globally. Use createBrownfieldOverride to scope NativeWind to the library + migrated screens folder.
If the app defines @/ aliases for ./src/* via babel-plugin-module-resolver, you must redirect @/ imports from @medway-ui/native files to the library source — otherwise imports like @/components/Badge resolve to the app src/ and break the bundle.
const {
createBrownfieldOverride,
createMedwayUiAliasResolver,
} = require("@medway-ui/native/babel-brownfield");
const resolveMedwayUiAlias = createMedwayUiAliasResolver(__dirname);
module.exports = function (api) {
api.cache(true);
const appPlugins = [
[
"module-resolver",
{
root: ["."],
extensions: [".ios.ts", ".android.ts", ".ts", ".tsx", ".js", ".jsx", ".json"],
alias: {
"@/components": "./src/components",
"@/pages": "./src/pages",
// ... other app aliases
},
resolvePath(sourcePath, currentFile) {
return resolveMedwayUiAlias(sourcePath, currentFile);
},
},
],
// other app plugins (no reanimated here)
];
return {
presets: ["babel-preset-expo"],
plugins: appPlugins,
overrides: [
createBrownfieldOverride({
projectRoot: __dirname,
appPagesDir: "medway-ui", // src/pages subfolder with NativeWind screens
plugins: [
// repeat any plugins needed in the medway-ui scope
"react-native-reanimated/plugin", // always last
],
}),
],
};
};Apps without @/ Babel aliases do not need createMedwayUiAliasResolver.
4. Provider + CSS
Wrap the subtree that uses @medway-ui/native with MedwayUiProvider and import CSS once:
import { MedwayUiProvider } from "@medway-ui/native/setup";
import "@medway-ui/native/setup/global.css";
function MyScreen() {
return (
<MedwayUiProvider theme="system">
{/* Button, CardNews, etc. */}
</MedwayUiProvider>
);
}Dark mode: pass colorScheme="light" | "dark" when the app already resolves theme (Redux, Zustand, etc.):
<MedwayUiProvider colorScheme={appColorScheme}>
{children}
</MedwayUiProvider>MedwayUiProvider includes PortalHost (required for Select, Dialog, Popover, Tooltip).
Do not import
@medway-ui/core/tokens.cssdirectly — incompatible with NativeWind v4. Tokens come fromsetup/global.css+setup/tokens.native.css.
5. Auto navigator (catalog / isolated screens)
Create once src/pages/medway-ui/navigator.tsx:
import { createMedwayUiNavigator, MedwayUiProvider } from "@medway-ui/native/setup";
import "@medway-ui/native/setup/global.css";
const { MedwayUiNavigator } = createMedwayUiNavigator(
require.context(".", true, /^\.\/[^/]+\/index\.tsx$/),
);
function MedwayUiRoot() {
return (
<MedwayUiProvider theme="system">
<MedwayUiNavigator />
</MedwayUiProvider>
);
}
export { MedwayUiRoot, MedwayUiNavigator };Register in the app stack:
<Stack.Screen name="MedwayUi" component={MedwayUiRoot} options={{ headerShown: false }} />New screen = new folder src/pages/medway-ui/MyScreen/index.tsx — no manual route registration.
6. Using components in existing screens (Restyle + Medway UI mix)
Pattern used in Content Hub and partially migrated features:
import { Button, CardNews } from "@medway-ui/native";
import { MedwayUiProvider } from "@medway-ui/native/setup";
import "@medway-ui/native/setup/global.css";
function ContentHubScreen() {
return (
<Box> {/* Restyle */}
<MedwayUiProvider colorScheme={colorScheme}>
<CardNews>{/* ... */}</CardNews>
</MedwayUiProvider>
</Box>
);
}Add the feature folder to content in tailwind.config.js.
Monorepo development (file:../medway-ui/native)
{
"@medway-ui/core": "file:../medway-ui",
"@medway-ui/native": "file:../medway-ui/native"
}Metro and Babel auto-detect the local clone. After library changes:
cd ../medway-ui/native && npm run build:lib # if testing the compiled libRestart Metro with a clean cache: npx expo start --clear.
Troubleshooting
| Error | Cause | Fix |
| --- | --- | --- |
| Unable to resolve .../src/components/Badge | App @/ alias overrides lib @/ | Add createMedwayUiAliasResolver in Babel |
| Unable to resolve @/lib/medwayIcons | Bug in @medway-ui/[email protected] | Upgrade to 1.1.2+ |
| Unable to resolve ../../../../../src/... from lib/module/ | resolveNativeSource: false | Remove the option (use default true) |
| Tokens / tailwind fail to load (Node 22+) | require of .ts inside node_modules | Use @medway-ui/[email protected]+ (createMedwayUiTailwindConfig uses jiti fallback) |
| NativeWind styles not applied | File outside brownfield scope | Move to src/pages/medway-ui/ or add a tailwind glob |
| Select/Dialog not opening | Missing PortalHost | Use MedwayUiProvider |
Greenfield setup (100% NativeWind app)
For new apps without legacy UI:
// babel.config.js
module.exports = function (api) {
api.cache(true);
return {
presets: [
["babel-preset-expo", { jsxImportSource: "nativewind" }],
"nativewind/babel",
],
plugins: ["react-native-reanimated/plugin"],
};
};// metro.config.js
const { getDefaultConfig } = require("expo/metro-config");
const { withMedwayUi } = require("@medway-ui/native/metro");
module.exports = withMedwayUi(getDefaultConfig(__dirname), {
projectRoot: __dirname,
});Import @medway-ui/native/setup/global.css in the root layout.
Usage
import {
Alert,
AlertContainer,
AlertContent,
AlertTitle,
AlertDescription,
Badge,
Button,
Input,
} from "@medway-ui/native";
export default function Screen() {
return (
<>
<Button variant="regular" size="md" onPress={() => {}}>
Continue
</Button>
<Input
label="Email"
type="email"
size="lg"
value={email}
onChangeText={setEmail}
placeholder="[email protected]"
/>
<Alert variant="success" onClose={() => {}}>
<AlertContainer>
<AlertContent>
<AlertTitle>Done</AlertTitle>
<AlertDescription>Operation completed successfully.</AlertDescription>
</AlertContent>
</AlertContainer>
</Alert>
<Badge variant="regular" size="md">9+</Badge>
</>
);
}Icons
The @medway-ui/icons package includes a native entry point:
import { Check, CloseMd } from "@medway-ui/icons/native";Components — status
| Component | Status | Notes | | --- | --- | --- | | Button | ✅ | Variants, loading, badge, alert dot | | Input | ✅ | text, email, password, numeric | | Alert | ✅ | Sub-components, all variants | | Badge | ✅ | Variants, icon-only | | Loading | ✅ | Reanimated spin | | Checkbox | ✅ | @rn-primitives/checkbox | | RadioGroup | ✅ | @rn-primitives/radio-group | | Switch | ✅ | @rn-primitives/switch | | Select | ✅ | @rn-primitives/select | | Label | ✅ | Pressable + Text | | Separator | ✅ | View 1px | | Dialog | ✅ | @rn-primitives/dialog | | AlertDialog | ✅ | @rn-primitives/alert-dialog | | Sheet | ✅ | @rn-primitives/dialog (side-aware) | | Tag | ✅ | 16+ variants | | Toast | ✅ | useToast + toast() | | Tooltip | ✅ | @rn-primitives/tooltip | | Popover | ✅ | @rn-primitives/popover | | Skeleton | ✅ | Shimmer | | Avatar | ✅ | @rn-primitives/avatar | | FallbackImage | ✅ | react-native-svg | | CardNews | ✅ | Composable card (Content Hub) | | Tabs | 🔜 | @rn-primitives/tabs | | Progress | 🔜 | @rn-primitives/progress | | ProgressCircle | 🔜 | react-native-svg | | Cards | 🔜 | CardModule, CardLesson, etc. | | DatePicker | 🔜 | @react-native-community/datetimepicker |
