@spelyco/react-native
v1.1.0
Published
React Native UI components for Spelyco, powered by Unistyles 3
Maintainers
Readme
@spelyco/react-native
React Native UI components for Spelyco, powered by Unistyles 3.
Install
bun add @spelyco/react-native \
@react-native-async-storage/async-storage \
expo-status-bar \
expo-system-ui \
react-native-unistyles \
react-native-edge-to-edge \
react-native-nitro-modules \
react-native-reanimatedThis package is published as TypeScript source. Your bundler (Metro for React Native / Expo) transpiles it directly. Make sure your Babel config processes it (see Setup below).
Setup
1. Babel plugin (required)
Unistyles 3 ships a Babel plugin that wires up the C++ shadow tree bindings. Because this package is shipped as source, you must include it in autoProcessPaths so the plugin processes our files inside node_modules.
babel.config.js:
module.exports = function (api) {
api.cache(true);
return {
presets: ["babel-preset-expo"],
plugins: [
[
"react-native-unistyles/plugin",
{
root: "src",
autoProcessPaths: ["@spelyco/react-native"],
},
],
],
};
};2. Wrap your app with SpelycoProvider
SpelycoProvider boots Unistyles, owns the color scheme state (zustand + AsyncStorage persistence), and applies the status/system bars from the theme. Put it at the root — for Expo Router that means app/_layout.tsx:
// app/_layout.tsx
import { SpelycoProvider } from "@spelyco/react-native";
import { Stack } from "expo-router";
export default function RootLayout() {
return (
<SpelycoProvider>
<Stack screenOptions={{ headerShown: false }} />
</SpelycoProvider>
);
}Pass a partial theme to override anything in DEFAULT_THEME, and defaultColorScheme to pin the first-launch preference (defaults to "auto").
<SpelycoProvider
theme={{ primaryColor: "brand", defaultRadius: "lg" }}
defaultColorScheme="dark"
>
<Stack />
</SpelycoProvider>3. New Architecture
Unistyles 3 requires React Native's New Architecture. Make sure your app.json (Expo) has:
{ "expo": { "newArchEnabled": true } }Components
Style props
Box, Text, Button, and ActionIcon accept the full Mantine-compatible
style-prop surface. Prop names and token resolution match Mantine 1:1.
<Box mt="md" px="lg" bg="blue.5" w="100%" />
<Text c="red.6" fz="lg" fw="600" ta="center" />- Spacing (
m*,p*,top/left/bottom/right,inset) — size tokens ("md"), negative tokens ("-md"), or raw numbers/percentages. - Color (
bg,c) — palette tokens ("blue.5","blue") or raw colors. - Typography —
fz(size token or number),fw,lts,ta,lh,fs,tt,td,ff,opacity. - Sizing / layout / position —
w/miw/maw/h/mih/mah,pos,display,flex. bd— object-based on RN (a partial border style), not a CSS string.
Mantine's background-image props (bgsz/bgp/bgr/bga) are not
supported — React Native has no background image. Responsive object syntax
(w={{ base, sm }}) is not supported either.
Box
A View primitive that accepts all style props (see above).
import { Box } from "@spelyco/react-native";
<Box p="md" gap="sm" bg="#f4f4f5">
{/* children */}
</Box>| Prop | Type | Description |
| --- | --- | --- |
| style props | see Style props | Full Mantine style-prop surface |
| gap | number \| string | Flex gap (Box convenience, not a Mantine prop) |
| ...rest | ViewProps | Any RN View prop |
Text
A Text primitive with size/weight tokens. Color auto-resolves from the active color scheme unless you provide one.
import { Text } from "@spelyco/react-native";
<Text size="xl" weight="bold">Heading</Text>
<Text size="sm">Body copy</Text>
<Text c="red.6">Inline override</Text>| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| size | "xs" \| "sm" \| "md" \| "lg" \| "xl" | "md" | Maps to theme.fontSizes |
| weight | "normal" \| "medium" \| "bold" | "normal" | |
| c | string | scheme-aware default | Text color (palette token or raw) |
| color | string | — | Deprecated — alias of c, will be removed in a future major |
| style props | see Style props | — | Full Mantine style-prop surface |
| ...rest | TextProps | — | Any RN Text prop |
Button
import { Button } from "@spelyco/react-native";
<Button label="Primary" variant="primary" />
<Button label="Secondary" variant="secondary" />
<Button label="Ghost" variant="ghost" />
<Button label="Small" size="sm" />
<Button label="Disabled" disabled />
<Button label="Save" onPress={() => console.log("saved")} />| Prop | Type | Default | Description |
| --- | --- | --- | --- |
| label | string | required | Button text |
| variant | "primary" \| "secondary" \| "ghost" | "primary" | Visual style |
| size | "sm" \| "md" \| "lg" | "md" | Padding/font size |
| disabled | boolean | false | Disable the button |
| ...rest | PressableProps | — | Any RN Pressable prop |
Button and ActionIcon play a built-in press animation (scale + opacity) via usePressAnimation. It is skipped automatically while disabled.
Field system
The shared skeleton for form components. Field stacks a label, description,
control, and error vertically; FieldControl is the bordered interactive box;
FieldInline lays out toggle-style controls horizontally.
import { Field, FieldControl, FieldInline } from "@spelyco/react-native";
<Field label="Email" description="We never share it." error={errorText} withAsterisk>
<FieldControl size="md" focused={focused} hasError={!!errorText}>
{/* a TextInput, Select trigger, ... */}
</FieldControl>
</Field>
<FieldInline label="Notifications" labelPosition="right">
{/* a Switch, Checkbox, ... */}
</FieldInline>| Component | Purpose |
| --- | --- |
| Field | Vertical label / description / control / error layout |
| FieldControl | Bordered box with leftSection/rightSection, size, and focused/hasError/disabled states |
| FieldInline | Horizontal layout for toggle controls (labelPosition: "left" \| "right") |
| FieldLabel / FieldDescription / FieldError | The individual parts |
FieldBaseProps (label, description, error, withAsterisk, disabled,
size) is the shared prop set form components extend.
Text fields
TextInput, PasswordInput, Textarea, and NumberInput are built on the
Field system. They all accept FieldBaseProps, the native TextInput props,
and the Box style props, and support controlled and uncontrolled usage.
import { TextInput, PasswordInput, Textarea, NumberInput } from "@spelyco/react-native";
<TextInput label="Email" placeholder="[email protected]" withAsterisk />
<PasswordInput
label="Password"
description="At least 8 characters."
onVisibilityChange={(visible) => console.log(visible)}
/>
<Textarea label="Bio" autosize minRows={3} maxRows={8} />
<NumberInput
label="Quantity"
min={0}
max={99}
step={1}
defaultValue={1}
onChange={(value) => console.log(value)}
/>| Component | Extra props |
| --- | --- |
| TextInput | leftSection / rightSection, plus all FieldBaseProps and native TextInput props |
| PasswordInput | visible / defaultVisible / onVisibilityChange for the show/hide toggle |
| Textarea | autosize, minRows (default 3), maxRows |
| NumberInput | min / max / step, withControls, and a numeric value / defaultValue / onChange |
PasswordInput and NumberInput require the @tabler/icons-react-native and
react-native-svg peer dependencies for their section icons.
Toggle controls
Switch, Checkbox, and Radio are toggle-style controls built on the
FieldInline layout. They all accept FieldBaseProps, the native Pressable
props, the Box style props, a labelPosition ("left" | "right"), and
support controlled (checked) and uncontrolled (defaultChecked) usage.
import { Switch, Checkbox, Radio } from "@spelyco/react-native";
<Switch label="Notifications" defaultChecked onChange={(on) => console.log(on)} />
<Checkbox label="Accept terms" withAsterisk />
<Checkbox label="Partial selection" indeterminate />
<Radio label="Standard plan" />Checkbox.Group and Radio.Group wrap several items and own a shared
selection — a values array for Checkbox.Group, a single value for
Radio.Group. Each child needs a unique value.
<Checkbox.Group label="Toppings" defaultValue={["cheese"]} onChange={setToppings}>
<Checkbox value="cheese" label="Cheese" />
<Checkbox value="olives" label="Olives" />
</Checkbox.Group>
<Radio.Group label="Size" defaultValue="md" orientation="horizontal">
<Radio value="sm" label="Small" />
<Radio value="md" label="Medium" />
</Radio.Group>| Component | Extra props |
| --- | --- |
| Switch | checked / defaultChecked / onChange, labelPosition, size |
| Checkbox | checked / defaultChecked / onChange, indeterminate, value (inside a group), labelPosition, size |
| Radio | checked / defaultChecked / onChange, value (inside a group), labelPosition, size |
| Checkbox.Group | value / defaultValue (string array), onChange, disabled, orientation |
| Radio.Group | value / defaultValue (string), onChange, disabled, orientation |
Switch animates its thumb and track colour via react-native-reanimated.
Checkbox requires the @tabler/icons-react-native and react-native-svg
peer dependencies for its tick icon.
Press animation
usePressAnimation drives a Reanimated scale + opacity feedback for any custom pressable.
import Animated from "react-native-reanimated";
import { usePressAnimation } from "@spelyco/react-native";
function PressableCard() {
const { animatedStyle, onPressIn, onPressOut } = usePressAnimation();
return (
<Animated.Pressable
onPressIn={onPressIn}
onPressOut={onPressOut}
style={[styles.card, animatedStyle]}
/>
);
}| Option | Type | Default | Description |
| --- | --- | --- | --- |
| scale | number | 0.97 | Scale applied while pressed |
| activeOpacity | number | 0.6 | Opacity applied while pressed |
| disabled | boolean | false | Skip the animation |
Color scheme
SpelycoProvider tracks the user preference (light/dark/auto) in a zustand store, persists it via AsyncStorage, and listens to OS appearance changes.
import { useSpelycoColorScheme } from "@spelyco/react-native";
function ThemeToggle() {
const {
colorScheme, // user preference: 'light' | 'dark' | 'auto'
computedColorScheme, // resolved scheme actually applied: 'light' | 'dark'
setColorScheme,
toggleColorScheme,
clearColorScheme, // reset to 'auto'
} = useSpelycoColorScheme();
return (
<Button
label={`Switch to ${computedColorScheme === "dark" ? "light" : "dark"}`}
onPress={toggleColorScheme}
/>
);
}Theming
Customize defaults via theme.components[ComponentName].defaultProps:
<SpelycoProvider
theme={{
components: {
Button: Button.extend({ defaultProps: { variant: "ghost" } }),
},
}}
>
{/* every <Button> now defaults to variant="ghost" */}
</SpelycoProvider>Theme tokens (colors, spacing, radius, fontSizes, lineHeights, shadows, breakpoints, systemBars, other) all flow through the same theme prop. See @spelyco/react-lib for the full SpelycoTheme contract.
Peer dependencies
| Package | Version |
| --- | --- |
| react | >=18 |
| react-native | >=0.76 |
| react-native-unistyles | ^3.2.4 |
| react-native-edge-to-edge | * |
| react-native-nitro-modules | * |
| react-native-reanimated | * |
| @react-native-async-storage/async-storage | * |
| expo-status-bar | * |
| expo-system-ui | * |
License
MIT
