Skip to main content

Theming

neopop-rn ships a fully typed, context-driven theming system that lets you apply a consistent visual language across every component without touching individual component props.


Table of Contents

  1. NeoPopProvider
  2. The mergeDeep override system
  3. Dark vs light default themes
  4. Per-component colorConfig keys
  5. Custom theme example

NeoPopProvider

Wrap your application (or any subtree) with NeoPopProvider to establish the active color mode and supply theme defaults to every neopop-rn component rendered beneath it.

import { NeoPopProvider } from '@codecollab.co/neopop-rn';

export default function App() {
return (
<NeoPopProvider colorMode="dark">
<YourApp />
</NeoPopProvider>
);
}

Props

PropTypeDefaultDescription
colorMode'dark' | 'light''dark'Selects the base theme. All components inherit this value.
themePartial<ThemeConfig>Partial overrides deep-merged on top of the selected base theme.
childrenReact.ReactNodeRequired. The subtree that receives the theme.

How it works internally

  1. colorMode selects either defaultDarkTheme or defaultLightTheme as the base.
  2. If a theme prop is provided it is recursively deep-merged into the base using mergeDeep, so you only need to supply the keys you want to change.
  3. The resolved theme object is stored in a React context (NeoPopContext) and consumed by useNeoPopTheme() inside each component.
// Simplified implementation (src/theme/NeoPopProvider.tsx)
const base = colorMode === 'dark' ? defaultDarkTheme : defaultLightTheme;
const resolved = useMemo(() => mergeDeep({ ...base }, theme ?? {}), [colorMode, theme]);

Reading the theme in your own components

import { useNeoPopTheme } from '@codecollab.co/neopop-rn';

function MyWidget() {
const theme = useNeoPopTheme();
return (
<View style={{ backgroundColor: theme.colors?.background as string }}>
{/* ... */}
</View>
);
}

Per-component colorMode override

Individual components accept a colorMode prop that overrides the provider value for that component only. This is useful when a light-mode card needs to live inside a dark-mode screen.

<NeoPopProvider colorMode="dark">
<NeoPopButton colorMode="light" /> {/* renders in light mode */}
<NeoPopCard /> {/* inherits dark mode */}
</NeoPopProvider>

The mergeDeep override system

mergeDeep performs a recursive deep merge. Plain objects are merged key by key; all other values (primitives, arrays) are replaced.

// src/utils/helpers.ts
export function mergeDeep<T extends Record<string, unknown>>(
target: T,
...sources: Partial<T>[]
): T

Merge behaviour

ScenarioResult
Key exists in base onlyKey is kept from base
Key exists in override onlyKey is added from override
Both values are plain objectsObjects are recursively merged
Override value is a primitive/arrayOverride value replaces the base value
Override value is undefinedKey is kept from base (no-op)

Examples

Override a single token

<NeoPopProvider
colorMode="dark"
theme={{ button: { color: '#7C5CFC' } }}
>
<App />
</NeoPopProvider>

The rest of button (edgeColors, borderColor, disabledColor, disabledEdgeColor) stays unchanged from the dark default.

Override global surface colors

<NeoPopProvider
colorMode="light"
theme={{
colors: {
background: '#F5F0FF',
text: '#20104D',
},
}}
>
<App />
</NeoPopProvider>

Override nested toggle colors

<NeoPopProvider
colorMode="dark"
theme={{
toggle: {
on: { switchBackground: '#7C5CFC', switchBorder: '#7C5CFC' },
},
}}
>
<App />
</NeoPopProvider>

Because mergeDeep recurses into toggle.on, the remaining keys (buttonBackground, buttonBorder, buttonMarkBackground) keep their dark defaults. The toggle.off sub-object is untouched.


Dark vs light default themes

The table below compares every key in defaultDarkTheme and defaultLightTheme side by side.

Global surface colors (colors)

KeyDarkLight
background#0d0d0d (COLOR_BLACK)#ffffff (COLOR_WHITE)
surface#161616 (POP_BLACK[300])#EFEFEF (POP_WHITE[300])
text#ffffff (COLOR_WHITE)#0d0d0d (COLOR_BLACK)
subtext#8A8A8A (POP_BLACK[100])#8A8A8A (POP_BLACK[100])
border#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])

NeoPopButton (button)

KeyDarkLight
color#ffffff#0d0d0d
edgeColors.bottom#8A8A8A (POP_BLACK[100])#8A8A8A (POP_BLACK[100])
edgeColors.right#8A8A8A (POP_BLACK[100])#8A8A8A (POP_BLACK[100])
borderColor#ffffff#0d0d0d
disabledColor#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])
disabledEdgeColor#3D3D3D (POP_BLACK[200])#E0E0E0 (POP_WHITE[200])

NeoPopCard (card)

KeyDarkLight
color#161616 (POP_BLACK[300])#ffffff
edgeColors.bottom#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
edgeColors.right#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
borderColor#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])

NeoPopShimmer (shimmer)

KeyDarkLight
colorrgba(255,248,229,0.49)rgba(255,255,255,0.6)

NeoPopCheckbox (checkbox)

KeyDarkLight
background#ffffff#0d0d0d
border#ffffff#0d0d0d
checkmarkColor#0d0d0d#ffffff
leftPlunk#8A8A8A (POP_BLACK[100])#3D3D3D (POP_BLACK[200])
topPlunk#8A8A8A (POP_BLACK[100])#3D3D3D (POP_BLACK[200])
disabledBackground#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])

NeoPopRadio (radio)

KeyDarkLight
background#0d0d0d#ffffff
border#ffffff#0d0d0d
plunk#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])
dotBackground#ffffff#0d0d0d

NeoPopToggle — on state (toggle.on)

KeyDarkLight
switchBackground#06C270#06C270
switchBorder#06C270#06C270
buttonBackground#ffffff#ffffff
buttonBorder#ffffff#ffffff
buttonMarkBackground#06C270#06C270

NeoPopToggle — off state (toggle.off)

KeyDarkLight
switchBackground#3D3D3D (POP_BLACK[200])#E0E0E0 (POP_WHITE[200])
switchBorder#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])
buttonBackground#E0E0E0 (POP_WHITE[200])#8A8A8A (POP_BLACK[100])
buttonBorder#E0E0E0 (POP_WHITE[200])#8A8A8A (POP_BLACK[100])
buttonMarkBackground#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])

NeoPopInputField (inputField)

KeyDarkLight
textColor#ffffff#0d0d0d
labelColor#8A8A8A (POP_BLACK[100])#8A8A8A (POP_BLACK[100])
caretColor#ffffff#0d0d0d
errorColor#EE4D37 (SEMANTIC_ERROR[4])#EE4D37 (SEMANTIC_ERROR[4])
placeholderColor#8A8A8A (POP_BLACK[100])#8A8A8A (POP_BLACK[100])
borderColor#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
activeBorderColor#ffffff#0d0d0d
backgroundColor#0d0d0d#ffffff

NeoPopDropdown (dropdown)

KeyDarkLight
border#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
text#ffffff#0d0d0d
chevron#ffffff#0d0d0d
background#0d0d0d#ffffff

NeoPopTags (tags)

KeyDarkLight
background#161616 (POP_BLACK[300])#EFEFEF (POP_WHITE[300])
color#ffffff#0d0d0d

NeoPopBottomSheet (bottomSheet)

KeyDarkLight
background#161616 (POP_BLACK[300])#ffffff
notchColor#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
overlayColorrgba(0,0,0,0.7)rgba(0,0,0,0.4)
plunkColor#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])

NeoPopFloatingButton (floatingButton)

KeyDarkLight
color#ffffff#0d0d0d
borderColor#ffffff#0d0d0d
edgeColor#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])
shadowColor#3D3D3D (POP_BLACK[200])#E0E0E0 (POP_WHITE[200])
disabledColor#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])

NeoPopTiltedButton (tiltedButton)

KeyDarkLight
color#ffffff#0d0d0d
plunkColor#8A8A8A (POP_BLACK[100])#D2D2D2 (POP_WHITE[100])
shadowColor#3D3D3D (POP_BLACK[200])#E0E0E0 (POP_WHITE[200])
borderColor#ffffff#0d0d0d

NeoPopScoreMeter (scoreMeter)

KeyDarkLight
meterStrokeColor.excellent#06C270 (SEMANTIC_SUCCESS[4])#06C270 (SEMANTIC_SUCCESS[4])
meterStrokeColor.average#F5A623#F5A623
meterStrokeColor.poor#EE4D37 (SEMANTIC_ERROR[4])#EE4D37 (SEMANTIC_ERROR[4])
meterStrokeBackground#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])
dotColor#ffffff#0d0d0d
scoreColor#ffffff#0d0d0d
scoreContainerBackground#0d0d0d#ffffff
scoreContainerBorder#3D3D3D (POP_BLACK[200])#D2D2D2 (POP_WHITE[100])

Per-component colorConfig keys

Every entry in ThemeConfig maps directly to a typed interface exported from src/theme/types.ts. The table below lists every interface and its fields.

ThemeConfig top-level keys

KeyInterfaceRequired
colorMode'dark' | 'light'Yes
colorsglobal surface object (see below)No
buttonNeoPopButtonColorConfigNo
cardNeoPopCardColorConfigNo
shimmerNeoPopShimmerColorConfigNo
checkboxNeoPopCheckboxColorConfigNo
radioNeoPopRadioColorConfigNo
toggleNeoPopToggleColorConfigNo
inputFieldNeoPopInputFieldColorConfigNo
dropdownNeoPopDropdownColorConfigNo
tagsNeoPopTagsColorConfigNo
bottomSheetNeoPopBottomSheetColorConfigNo
floatingButtonNeoPopFloatingButtonColorConfigNo
tiltedButtonNeoPopTiltedButtonColorConfigNo
scoreMeterNeoPopScoreMeterColorConfigNo
otpInputNeoPopOTPInputColorConfigNo
progressBarNeoPopProgressBarColorConfigNo
accordionNeoPopAccordionColorConfigNo
stepperNeoPopStepperColorConfigNo
carouselNeoPopCarouselColorConfigNo
datePickerNeoPopDatePickerColorConfigNo
swipeRowNeoPopSwipeRowColorConfigNo

colors (global surface)

FieldType
backgroundColorValue
surfaceColorValue
textColorValue
subtextColorValue
borderColorValue

EdgeColors

Shared by button and card edgeColors.

FieldType
topstring
rightstring
bottomstring
leftstring

NeoPopButtonColorConfig

FieldType
colorstring
edgeColorsEdgeColors
borderColorstring
disabledColorstring
disabledEdgeColorstring

NeoPopCardColorConfig

FieldType
colorstring
edgeColorsEdgeColors
borderColorstring

NeoPopShimmerColorConfig

FieldType
colorstring

NeoPopCheckboxColorConfig

FieldType
backgroundstring
borderstring
checkmarkColorstring
leftPlunkstring
topPlunkstring
disabledBackgroundstring

NeoPopRadioColorConfig

FieldType
backgroundstring
borderstring
plunkstring
dotBackgroundstring
containerConfig.borderColorstring
containerConfig.backgroundColorstring
containerConfig.activeBackgroundColorstring
containerConfig.activeBorderColorstring
containerConfig.colorstring

NeoPopToggleColorConfig

Contains two sub-objects, each of type NeoPopToggleOnOffColors.

NeoPopToggleOnOffColors (applies to both on and off):

FieldType
switchBackgroundstring
switchBorderstring
buttonBackgroundstring
buttonBorderstring
buttonMarkBackgroundstring

NeoPopInputFieldColorConfig

FieldType
textColorstring
labelColorstring
caretColorstring
errorColorstring
placeholderColorstring
borderColorstring
activeBorderColorstring
backgroundColorstring

NeoPopDropdownColorConfig

FieldType
borderstring
textstring
chevronstring
backgroundstring

NeoPopTagsColorConfig

FieldType
backgroundstring
colorstring

NeoPopToastColorConfig

FieldType
backgroundstring
colorstring

NeoPopBottomSheetColorConfig

FieldType
backgroundstring
notchColorstring
overlayColorstring
plunkColorstring

NeoPopFloatingButtonColorConfig

FieldType
colorstring
borderColorstring
edgeColorstring
shadowColorstring
disabledColorstring

NeoPopTiltedButtonColorConfig

FieldType
colorstring
plunkColorstring
shadowColorstring
borderColorstring

NeoPopOTPInputColorConfig

FieldType
borderColorstring
activeBorderColorstring
errorBorderColorstring
backgroundColorstring
textColorstring

NeoPopProgressBarColorConfig

FieldType
trackColorstring
fillColorstring
labelColorstring

NeoPopAccordionColorConfig

FieldType
headerBackgroundstring
headerTextColorstring
chevronColorstring
bodyBackgroundstring
edgeColorstring

NeoPopStepperColorConfig

FieldType
backgroundstring
buttonColorstring
buttonEdgeColorstring
textColorstring
disabledColorstring

NeoPopCarouselColorConfig

FieldType
dotColorstring
activeDotColorstring

NeoPopDatePickerColorConfig

FieldType
backgroundstring
textColorstring
selectedTextColorstring
selectedBackgroundstring
separatorColorstring

NeoPopSwipeRowColorConfig

FieldType
backgroundstring
leftActionBackgroundstring
rightActionBackgroundstring

NeoPopScoreMeterColorConfig

FieldType
meterStrokeColor.excellentstring
meterStrokeColor.averagestring
meterStrokeColor.poorstring
meterStrokeBackgroundstring
meterBorderColorstring
dotColorstring
indicatorColors.neutralstring
indicatorColors.incrementstring
indicatorColors.decrementstring
scoreContainerBackgroundstring
scoreContainerBorderstring
scoreColorstring

Custom theme example

The example below shows a complete brand-specific theme (the fictional "Polaris" brand uses deep purple as its primary color).

import {
NeoPopProvider,
defaultDarkTheme,
type ThemeConfig,
} from '@codecollab.co/neopop-rn';

// 1. Define your brand palette constants.
const BRAND_PRIMARY = '#7C5CFC'; // Polaris purple
const BRAND_SECONDARY = '#5B35D5';
const BRAND_BG = '#0E0A1F';
const BRAND_SURFACE = '#1A133A';
const BRAND_BORDER = '#2D1F6E';
const BRAND_TEXT = '#F5F0FF';
const BRAND_SUBTEXT = '#9B8FCC';
const SUCCESS = '#06C270';
const ERROR = '#EE4D37';
const WARNING = '#F5A623';

// 2. Compose the partial override.
// You only need to supply the keys that differ from the dark defaults.
const polarisTheme: Partial<ThemeConfig> = {
colors: {
background: BRAND_BG,
surface: BRAND_SURFACE,
text: BRAND_TEXT,
subtext: BRAND_SUBTEXT,
border: BRAND_BORDER,
},

button: {
color: BRAND_PRIMARY,
edgeColors: { bottom: BRAND_SECONDARY, right: BRAND_SECONDARY },
borderColor: BRAND_PRIMARY,
disabledColor: BRAND_BORDER,
disabledEdgeColor: BRAND_SURFACE,
},

card: {
color: BRAND_SURFACE,
edgeColors: { bottom: BRAND_BORDER, right: BRAND_BORDER },
borderColor: BRAND_BORDER,
},

shimmer: { color: 'rgba(124,92,252,0.3)' },

checkbox: {
background: BRAND_PRIMARY,
border: BRAND_PRIMARY,
checkmarkColor: BRAND_TEXT,
leftPlunk: BRAND_SECONDARY,
topPlunk: BRAND_SECONDARY,
disabledBackground: BRAND_BORDER,
},

radio: {
background: BRAND_BG,
border: BRAND_PRIMARY,
plunk: BRAND_SECONDARY,
dotBackground: BRAND_PRIMARY,
},

toggle: {
on: {
switchBackground: BRAND_PRIMARY,
switchBorder: BRAND_PRIMARY,
buttonBackground: BRAND_TEXT,
buttonBorder: BRAND_TEXT,
buttonMarkBackground: BRAND_PRIMARY,
},
off: {
switchBackground: BRAND_SURFACE,
switchBorder: BRAND_BORDER,
buttonBackground: BRAND_BORDER,
buttonBorder: BRAND_BORDER,
buttonMarkBackground: BRAND_SURFACE,
},
},

inputField: {
textColor: BRAND_TEXT,
labelColor: BRAND_SUBTEXT,
caretColor: BRAND_PRIMARY,
errorColor: ERROR,
placeholderColor: BRAND_SUBTEXT,
borderColor: BRAND_BORDER,
activeBorderColor: BRAND_PRIMARY,
backgroundColor: BRAND_BG,
},

dropdown: {
border: BRAND_BORDER,
text: BRAND_TEXT,
chevron: BRAND_PRIMARY,
background: BRAND_BG,
},

tags: {
background: BRAND_SURFACE,
color: BRAND_TEXT,
},

bottomSheet: {
background: BRAND_SURFACE,
notchColor: BRAND_BORDER,
overlayColor: 'rgba(14,10,31,0.85)',
plunkColor: BRAND_BORDER,
},

floatingButton: {
color: BRAND_PRIMARY,
borderColor: BRAND_PRIMARY,
edgeColor: BRAND_SECONDARY,
shadowColor: BRAND_BORDER,
disabledColor: BRAND_BORDER,
},

tiltedButton: {
color: BRAND_PRIMARY,
plunkColor: BRAND_SECONDARY,
shadowColor: BRAND_BORDER,
borderColor: BRAND_PRIMARY,
},

scoreMeter: {
meterStrokeColor: {
excellent: SUCCESS,
average: WARNING,
poor: ERROR,
},
meterStrokeBackground: BRAND_BORDER,
dotColor: BRAND_TEXT,
scoreColor: BRAND_TEXT,
scoreContainerBackground: BRAND_BG,
scoreContainerBorder: BRAND_BORDER,
},

otpInput: {
borderColor: BRAND_BORDER,
activeBorderColor: BRAND_PRIMARY,
errorBorderColor: ERROR,
backgroundColor: BRAND_BG,
textColor: BRAND_TEXT,
},

progressBar: {
trackColor: BRAND_BORDER,
fillColor: BRAND_PRIMARY,
labelColor: BRAND_TEXT,
},

accordion: {
headerBackground: BRAND_SURFACE,
headerTextColor: BRAND_TEXT,
chevronColor: BRAND_PRIMARY,
bodyBackground: BRAND_BG,
edgeColor: BRAND_BORDER,
},

stepper: {
background: BRAND_SURFACE,
buttonColor: BRAND_PRIMARY,
buttonEdgeColor: BRAND_SECONDARY,
textColor: BRAND_TEXT,
disabledColor: BRAND_BORDER,
},

carousel: {
dotColor: BRAND_BORDER,
activeDotColor: BRAND_PRIMARY,
},

datePicker: {
background: BRAND_BG,
textColor: BRAND_TEXT,
selectedTextColor: BRAND_TEXT,
selectedBackground: BRAND_PRIMARY,
separatorColor: BRAND_BORDER,
},

swipeRow: {
background: BRAND_SURFACE,
leftActionBackground: SUCCESS,
rightActionBackground: ERROR,
},
};

// 3. Apply it.
export default function App() {
return (
<NeoPopProvider colorMode="dark" theme={polarisTheme}>
<RootNavigator />
</NeoPopProvider>
);
}

Any key you omit continues to use the dark default. You can also start from defaultLightTheme by setting colorMode="light" and supplying the same partial override.