Theme System
defineTheme()
defineTheme validates your token set with Zod and returns a frozen ThemeAdapter.
import { defineTheme } from '@objectifthunes/limestone-sdk';
const lightTheme = defineTheme({
name: 'brand-light',
mode: 'light',
tokens: {
colors: { /* 17 required keys */ },
spacing: { /* 8 required keys */ },
radii: { /* 6 required keys */ },
typography: { /* fontFamily, sizes, weights, lineHeights */ },
shadows: { /* 3 required keys */ },
motion: { /* duration, easing */ },
},
});
Zod throws at call time if any token is missing or has the wrong type. There is no silent fallback.
Token types
ColorTokens — 17 keys
| Key | Purpose |
|---|---|
background | Page / screen background |
foreground | Default text color |
primary | Brand primary action color |
primaryForeground | Text on primary backgrounds |
secondary | Secondary action color |
secondaryForeground | Text on secondary backgrounds |
muted | Subtle fill (disabled, inactive) |
mutedForeground | Text on muted backgrounds |
accent | Highlight / hover accent |
accentForeground | Text on accent backgrounds |
destructive | Error / danger color |
destructiveForeground | Text on destructive backgrounds |
card | Card surface background |
cardForeground | Text on card surfaces |
border | Border and divider color |
input | Input field background or border |
ring | Focus ring color |
All values are strings — hex, rgba, or any React Native color format.
SpacingTokens — 8 keys
xs, sm, md, lg, xl, 2xl, 3xl, 4xl. All numbers (device-independent pixels).
spacing: {
xs: 4,
sm: 8,
md: 12,
lg: 24,
xl: 32,
'2xl': 48,
'3xl': 64,
'4xl': 96,
},
RadiiTokens — 6 keys
none, sm, md, lg, xl, full. All numbers.
radii: {
none: 0,
sm: 4,
md: 8,
lg: 12,
xl: 16,
full: 9999,
},
TypographyTokens
Nested object with four sub-groups:
typography: {
fontFamily: {
display: 'Inter-Bold', // decorative / headline
body: 'Inter-Regular', // default reading text
mono: 'JetBrainsMono', // code / monospace
},
sizes: { // 8 keys: xs–4xl, all numbers
xs: 11, sm: 13, md: 15, lg: 17,
xl: 20, '2xl': 24, '3xl': 30, '4xl': 36,
},
weights: { // all strings (React Native fontWeight)
regular: '400',
medium: '500',
semibold: '600',
bold: '700',
},
lineHeights: { // 3 keys, multipliers as numbers
tight: 1.2,
normal: 1.5,
relaxed: 1.8,
},
},
ShadowTokens — 3 keys
sm, md, lg. Each shadow is an object:
shadows: {
sm: { color: '#000', offset: { width: 0, height: 1 }, radius: 2, opacity: 0.08, elevation: 2 },
md: { color: '#000', offset: { width: 0, height: 2 }, radius: 6, opacity: 0.12, elevation: 4 },
lg: { color: '#000', offset: { width: 0, height: 4 }, radius: 12, opacity: 0.16, elevation: 8 },
},
MotionTokens
motion: {
duration: {
fast: 150, // ms
normal: 300,
slow: 500,
},
easing: {
default: 'ease-in-out', // CSS easing string
spring: { damping: 20, stiffness: 300 },
bounce: { damping: 8, stiffness: 200 },
},
},
ThemeProvider
Wrap your app with ThemeProvider. Pass a darkTheme to enable mode switching.
import { ThemeProvider } from '@objectifthunes/limestone-sdk';
export default function App() {
return (
<ThemeProvider theme={lightTheme} darkTheme={darkTheme}>
<RootNavigator />
</ThemeProvider>
);
}
useTheme()
Returns the current theme context. Call it in any component inside ThemeProvider.
import { useTheme } from '@objectifthunes/limestone-sdk';
function MyComponent() {
const { tokens, mode, name, theme, setTheme, toggleMode } = useTheme();
return (
<View style={{ backgroundColor: tokens.colors.background }}>
<Text style={{ color: tokens.colors.foreground }}>
Current mode: {mode}
</Text>
</View>
);
}
| Field | Type | Description |
|---|---|---|
tokens | ThemeTokens | Resolved token values for the current theme |
mode | 'light' | 'dark' | Active color mode |
name | string | Theme name from defineTheme |
theme | ThemeAdapter | The full frozen adapter |
setTheme(theme) | void | Swap to any ThemeAdapter |
toggleMode() | void | Toggle between the light and dark pair |
Mode switching
toggleMode
Requires both theme (light) and darkTheme (dark) passed to ThemeProvider.
function DarkModeToggle() {
const { mode, toggleMode } = useTheme();
return (
<Pressable onPress={toggleMode}>
<Text>{mode === 'dark' ? 'Switch to light' : 'Switch to dark'}</Text>
</Pressable>
);
}
setTheme
Use setTheme to switch to any arbitrary adapter — useful for multi-brand apps.
function BrandSelector() {
const { setTheme } = useTheme();
return (
<Pressable onPress={() => setTheme(premiumTheme)}>
<Text>Activate Premium Theme</Text>
</Pressable>
);
}
Component overrides
ComponentOverrides lets you fine-tune token values for specific components without creating a whole new theme.
import { defineTheme } from '@objectifthunes/limestone-sdk';
import type { ComponentOverrides } from '@objectifthunes/limestone-sdk';
const overrides: ComponentOverrides = {
Button: {
colors: { primary: '#FF6B35' },
radii: { md: 24 }, // pill-shaped buttons
},
TextInput: {
colors: { border: '#CCCCCC', input: '#F9F9F9' },
spacing: { md: 16 },
},
};
const myTheme = defineTheme({
name: 'brand',
mode: 'light',
tokens: { /* full token set */ },
components: overrides,
});
Overrides are merged on top of base tokens by resolveComponentTokens. Keys not listed fall back to the base theme.
import { resolveComponentTokens } from '@objectifthunes/limestone-sdk';
const buttonTokens = resolveComponentTokens(myTheme, 'Button');
// buttonTokens.colors.primary === '#FF6B35'
// buttonTokens.colors.background === myTheme.tokens.colors.background (inherited)
Custom theme walkthrough
import { defineTheme } from '@objectifthunes/limestone-sdk';
// 1. Define light theme
const lightTheme = defineTheme({
name: 'app-light',
mode: 'light',
tokens: {
colors: {
background: '#FFFFFF',
foreground: '#0F172A',
primary: '#6366F1',
primaryForeground: '#FFFFFF',
secondary: '#F1F5F9',
secondaryForeground: '#0F172A',
muted: '#F1F5F9',
mutedForeground: '#64748B',
accent: '#F1F5F9',
accentForeground: '#0F172A',
destructive: '#EF4444',
destructiveForeground: '#FFFFFF',
card: '#FFFFFF',
cardForeground: '#0F172A',
border: '#E2E8F0',
input: '#E2E8F0',
ring: '#6366F1',
},
spacing: { xs: 4, sm: 8, md: 12, lg: 24, xl: 32, '2xl': 48, '3xl': 64, '4xl': 96 },
radii: { none: 0, sm: 4, md: 8, lg: 12, xl: 16, full: 9999 },
typography: {
fontFamily: { display: 'System', body: 'System', mono: 'Courier' },
sizes: { xs: 11, sm: 13, md: 15, lg: 17, xl: 20, '2xl': 24, '3xl': 30, '4xl': 36 },
weights: { regular: '400', medium: '500', semibold: '600', bold: '700' },
lineHeights: { tight: 1.2, normal: 1.5, relaxed: 1.8 },
},
shadows: {
sm: { color: '#000', offset: { width: 0, height: 1 }, radius: 2, opacity: 0.08, elevation: 2 },
md: { color: '#000', offset: { width: 0, height: 2 }, radius: 6, opacity: 0.12, elevation: 4 },
lg: { color: '#000', offset: { width: 0, height: 4 }, radius: 12, opacity: 0.16, elevation: 8 },
},
motion: {
duration: { fast: 150, normal: 300, slow: 500 },
easing: {
default: 'ease-in-out',
spring: { damping: 20, stiffness: 300 },
bounce: { damping: 8, stiffness: 200 },
},
},
},
});
// 2. Define dark theme (same structure, different color values)
const darkTheme = defineTheme({
name: 'app-dark',
mode: 'dark',
tokens: {
colors: {
background: '#0F172A',
foreground: '#F8FAFC',
primary: '#818CF8',
primaryForeground: '#0F172A',
// ...remaining 13 keys
},
// spacing, radii, typography, shadows, motion are typically identical
},
});
// 3. Pass both to the provider
export default function App() {
return (
<LimestoneProvider config={defineConfig({ theme: lightTheme, darkTheme })}>
<RootNavigator />
</LimestoneProvider>
);
}
Shipped themes
Two ready-to-use themes are exported from the main barrel. Drop them straight into defineConfig without defining any tokens yourself.
obsidianTheme — dark
Premium dark theme. Deep blacks (#09090b background), electric violet-600 primary (#7c3aed), indigo-500 accent (#6366f1), tight 120ms fast animations with spring easing.
import { obsidianTheme, defineConfig } from '@objectifthunes/limestone-sdk';
const config = defineConfig({ theme: obsidianTheme });
daylightTheme — light
Clean, airy light theme. White backgrounds, warm orange-500 primary (#f97316), cyan-500 accent (#06b6d4), generous 150ms fast animations with ease-out easing.
import { daylightTheme, defineConfig } from '@objectifthunes/limestone-sdk';
const config = defineConfig({ theme: daylightTheme });
Using both as a light/dark pair
import { obsidianTheme, daylightTheme, defineConfig } from '@objectifthunes/limestone-sdk';
const config = defineConfig({
theme: daylightTheme, // shown by default
darkTheme: obsidianTheme, // activated by toggleMode() or system dark mode
});