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

Theming Basics

Learn how to set up themes in Uniwind

Custom Themes

Define custom themes like coffee

useUniwind

Read the current theme and adaptive theme state

useCSSVariable

Read theme variables directly in JavaScript

useResolveClassNames

Resolve class names into style objects at runtime

withUniwind

Add className support to third-party components