Skip to main content
Available in Uniwind 1.4.0+

Overview

ScopedTheme lets you apply a theme to a specific subtree without changing the global app theme. This is useful for previews, isolated surfaces (cards, modals, panels), and mixed-theme layouts where multiple themes need to exist on screen at the same time. The nearest ScopedTheme wins, so nested scopes work naturally.
ScopedTheme is a runtime theme boundary. Components, hooks, and withUniwind wrapped components inside the boundary resolve styles against the scoped theme.

Usage

Basic Example

import { ScopedTheme } from 'uniwind'
import { Text, View } from 'react-native'

export function Example() {
  return (
    <View className="gap-4">
      <View className="p-4 rounded-xl bg-base">
        <Text className="text-default">Uses the current app theme</Text>
      </View>
      <ScopedTheme theme="dark">
        <View className="p-4 rounded-xl bg-base">
          <Text className="text-default">This subtree uses the dark theme</Text>
          <View className="mt-2 size-8 rounded bg-primary" />
        </View>
      </ScopedTheme>
    </View>
  )
}

Common Patterns

Side-by-Side Theme Previews

import { ScopedTheme } from 'uniwind'
import { Text, View } from 'react-native'

export function ThemePreviewRow() {
  return (
    <View className="flex-row gap-3">
      <ScopedTheme theme="light">
        <View className="flex-1 p-4 bg-base rounded-2xl gap-2">
          <Text className="text-default font-bold">Light</Text>
          <View className="size-8 rounded-lg bg-primary" />
          <Text className="text-default/60 text-xs">Scoped to light</Text>
        </View>
      </ScopedTheme>

      <ScopedTheme theme="dark">
        <View className="flex-1 p-4 bg-base rounded-2xl gap-2">
          <Text className="text-default font-bold">Dark</Text>
          <View className="size-8 rounded-lg bg-primary" />
          <Text className="text-default/60 text-xs">Scoped to dark</Text>
        </View>
      </ScopedTheme>

      <ScopedTheme theme="coffee">
        <View className="flex-1 p-4 bg-base rounded-2xl gap-2">
          <Text className="text-default font-bold">Coffee</Text>
          <View className="size-8 rounded-lg bg-primary" />
          <Text className="text-default/60 text-xs">Scoped to coffee (custom theme)</Text>
        </View>
      </ScopedTheme>
    </View>
  )
}

Nested Scoped Themes

import { ScopedTheme } from 'uniwind'
import { Text, View } from 'react-native'

export function NestedScopes() {
  return (
    <ScopedTheme theme="coffee">
      <View className="w-full p-4 bg-base rounded-2xl gap-3">
        <Text className="text-default font-bold">Coffee (outer custom theme)</Text>

        <ScopedTheme theme="dark">
          <View className="w-full p-4 bg-base rounded-xl gap-3">
            <Text className="text-default font-bold">Dark (middle)</Text>

            <ScopedTheme theme="light">
              <View className="w-full p-3 bg-base rounded-lg gap-2">
                <Text className="text-default font-bold">Light (inner)</Text>
                <View className="size-6 rounded bg-primary" />
              </View>
            </ScopedTheme>
          </View>
        </ScopedTheme>
      </View>
    </ScopedTheme>
  )
}
Nested scopes are great for documentation previews and component galleries where you want to compare variants without changing global theme state.

Hooks and withUniwind in a Scoped Theme

Hooks and HOCs resolve against the closest ScopedTheme boundary.
  • useUniwind() returns the scoped theme name
  • useResolveClassNames() resolves class names using scoped theme variables
  • useCSSVariable() reads CSS variables from the scoped theme
  • withUniwind() mappings use values resolved from the scoped theme

Example

import { Feather } from '@expo/vector-icons'
import { Pressable, Text, View } from 'react-native'
import {
  ScopedTheme,
  useCSSVariable,
  useResolveClassNames,
  useUniwind,
  withUniwind,
} from 'uniwind'

const StyledFeather = withUniwind(Feather, {
  size: {
    fromClassName: 'className',
    styleProperty: 'width',
  },
  color: {
    fromClassName: 'className',
    styleProperty: 'color',
  },
})

function UseUniwindExample() {
  const { theme } = useUniwind()

  return (
    <View className="w-full p-4 bg-base rounded-2xl gap-1">
      <Text className="text-default font-bold">useUniwind</Text>
      <Text className="text-primary">Current theme: {theme}</Text>
    </View>
  )
}

function UseResolveClassNamesExample() {
  const styles = useResolveClassNames('text-primary')

  return (
    <View className="w-full p-4 bg-base rounded-2xl gap-1">
      <Text className="text-default font-bold">useResolveClassNames</Text>
      <Text style={styles}>Resolved from 'text-primary'</Text>
    </View>
  )
}

function UseCSSVariableExample() {
  const primaryColor = useCSSVariable('--color-primary')

  return (
    <View className="w-full p-4 bg-base rounded-2xl gap-1">
      <Text className="text-default font-bold">useCSSVariable</Text>
      <Text className="text-primary">--color-primary: {String(primaryColor)}</Text>
    </View>
  )
}

function WithUniwindExample() {
  return (
    <View className="w-full p-4 bg-base rounded-2xl gap-2">
      <Text className="text-default font-bold">withUniwind</Text>
      <View className="flex-row gap-3 items-center">
        <StyledFeather name="star" className="size-6 text-primary" />
        <StyledFeather name="heart" className="size-6 text-primary" />
        <StyledFeather name="zap" className="size-6 text-primary" />
        <Text className="text-primary">Icons via withUniwind</Text>
      </View>
    </View>
  )
}

function ThemedButton({ title }: { title: string }) {
  return (
    <Pressable className="px-6 py-3 bg-primary rounded-xl active:scale-[0.97] active:opacity-80">
      <Text className="text-white font-semibold text-center">{title}</Text>
    </Pressable>
  )
}

export function ScopedThemeHooksExample() {
  return (
    <View className="w-full gap-3">
      <ScopedTheme theme="coffee">
        <View className="w-full gap-3">
          <ThemedButton title="Coffee Button" />
          <UseUniwindExample />
          <UseResolveClassNamesExample />
          <UseCSSVariableExample />
          <WithUniwindExample />
        </View>
      </ScopedTheme>

      <View className="w-full gap-3">
        <ThemedButton title="Current Theme Button" />
        <UseUniwindExample />
        <UseResolveClassNamesExample />
        <UseCSSVariableExample />
        <WithUniwindExample />
      </View>
    </View>
  )
}

API Reference

Component Signature

import { ScopedTheme } from 'uniwind'

<ScopedTheme theme="dark">
  {/* themed subtree */}
</ScopedTheme>

Props

theme
string
required
Theme name to apply to this subtree. The theme must exist in your configured Uniwind theme definitions (for example, light, dark, or a custom theme like coffee).
children
React.ReactNode
required
React children rendered inside the scoped theme boundary.

Behavior Notes

  • ScopedTheme affects only its descendants
  • It does not change the global app theme
  • Nested scopes override parent scopes for their own subtree
  • Multiple scopes can render side by side on the same screen