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

KeyPurpose
backgroundPage / screen background
foregroundDefault text color
primaryBrand primary action color
primaryForegroundText on primary backgrounds
secondarySecondary action color
secondaryForegroundText on secondary backgrounds
mutedSubtle fill (disabled, inactive)
mutedForegroundText on muted backgrounds
accentHighlight / hover accent
accentForegroundText on accent backgrounds
destructiveError / danger color
destructiveForegroundText on destructive backgrounds
cardCard surface background
cardForegroundText on card surfaces
borderBorder and divider color
inputInput field background or border
ringFocus 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>
  );
}
FieldTypeDescription
tokensThemeTokensResolved token values for the current theme
mode'light' | 'dark'Active color mode
namestringTheme name from defineTheme
themeThemeAdapterThe full frozen adapter
setTheme(theme)voidSwap to any ThemeAdapter
toggleMode()voidToggle 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
});