The Problem
Imagine a Button with an Icon inside. You want the icon size to match the button size.
Without context, you’d do this:
// Repetitive and error-prone
< Button size = "lg" >
< Icon size = "lg" />
< span > Click me </ span >
</ Button >
If you change the button size, you have to remember to change the icon size too.
The Solution
With context, the icon knows its parent’s size automatically:
// Icon inherits size from Button
< Button size = "lg" >
< Button.Icon />
< Button.Label > Click me </ Button.Label >
</ Button >
Creating a Context
Use createStyledContext() to define which variants should be shared.
import { createStyledContext } from "better-styled" ;
const ButtonContext = createStyledContext ({
size : [ "sm" , "md" , "lg" ] ,
variant : [ "primary" , "secondary" , "ghost" ] ,
});
The arrays define the possible values. TypeScript will infer the union types automatically.
Connecting Components
Pass the context to each component that should participate.
import { styled , withSlots } from "better-styled" ;
// Parent component
const ButtonRoot = styled ( "button" , {
context : ButtonContext ,
base : { className : "inline-flex items-center gap-2" } ,
variants : {
size : {
sm : { className : "h-8 px-3 text-sm" } ,
md : { className : "h-10 px-4 text-base" } ,
lg : { className : "h-12 px-6 text-lg" } ,
} ,
variant : {
primary : { className : "bg-blue-600 text-white" } ,
secondary : { className : "bg-gray-200 text-gray-900" } ,
ghost : { className : "hover:bg-gray-100" } ,
} ,
} ,
});
// Child component - inherits from context
const ButtonIcon = styled ( "span" , {
context : ButtonContext ,
base : { className : "shrink-0" } ,
variants : {
size : {
sm : { className : "w-4 h-4" } ,
md : { className : "w-5 h-5" } ,
lg : { className : "w-6 h-6" } ,
} ,
} ,
});
// Another child
const ButtonLabel = styled ( "span" , {
context : ButtonContext ,
variants : {
size : {
sm : { className : "text-sm" } ,
md : { className : "text-base" } ,
lg : { className : "text-lg" } ,
} ,
} ,
});
// Combine into compound component
export const Button = withSlots ( ButtonRoot , {
Icon : ButtonIcon ,
Label : ButtonLabel ,
});
How It Works
When you pass variant props to the parent:
The parent renders with those variants
It wraps its children in a context provider
Children with the same context read the values
Children apply matching variants automatically
< Button size = "lg" variant = "primary" >
< Button.Icon > ★ </ Button.Icon >
< Button.Label > Star </ Button.Label >
</ Button >
// ButtonIcon gets size="lg" from context
// ButtonLabel gets size="lg" from context
Default Variants Propagation
When the parent component has defaultVariants, children automatically inherit them—even without explicit props.
import { Pressable , Text } from "react-native" ;
const ButtonRoot = styled ( Pressable , {
context : ButtonContext ,
base : { className : "flex-row items-center justify-center rounded-lg" } ,
variants : {
size : {
sm : { className : "h-8 px-3" } ,
md : { className : "h-10 px-4" } ,
lg : { className : "h-12 px-6" } ,
} ,
variant : {
primary : { className : "bg-blue-600" } ,
secondary : { className : "bg-gray-200" } ,
} ,
} ,
defaultVariants : {
size : "md" ,
variant : "primary" , // These propagate to children
} ,
});
const ButtonLabel = styled ( Text , {
context : ButtonContext ,
base : { className : "font-semibold" } ,
variants : {
variant : {
primary : { className : "text-white" } ,
secondary : { className : "text-gray-900" } ,
} ,
} ,
});
Now you can use the component without any props:
// No props needed - defaultVariants are shared
< Button >
< Button.Label > Click me </ Button.Label >
</ Button >
// Button gets variant="primary" and size="md"
// Button.Label gets variant="primary" from parent's defaultVariants
// Result: white text on blue background
This is powerful for design systems where you want sensible defaults that cascade through the component tree.
Overriding Context
Children can override the context values when needed.
< Button size = "lg" >
< Button.Icon /> { /* size="lg" from context */ }
< Button.Label size = "sm" > { /* size="sm" override */ }
Small text in large button
</ Button.Label >
</ Button >
Direct props always win over context values.
Boolean Variants
For true/false variants, use the special ["boolean"] marker.
const ButtonContext = createStyledContext ({
size : [ "sm" , "md" , "lg" ] ,
isDisabled : [ "boolean" ] , // Becomes TypeScript boolean
});
const ButtonRoot = styled ( "button" , {
context : ButtonContext ,
variants : {
isDisabled : {
true : { className : "opacity-50" , disabled : true } ,
} ,
} ,
});
Just define true. You don’t need false: {}. When using the component, pass actual booleans:
< Button isDisabled > { /* Works */ }
< Button isDisabled = { true } > { /* Same */ }
< Button isDisabled = { false } > { /* Explicitly not disabled */ }
⚠️ Use isDisabled instead of disabled to avoid shadowing the native disabled prop on elements like button or Pressable.
Variant Priority
When multiple sources provide variant values, this is the priority order (highest wins):
🥇 Props passed directly to the component
🥈 Context values from parent
🥉 defaultVariants in config
const ButtonContext = createStyledContext ({
size : [ "sm" , "md" , "lg" ] ,
});
const ButtonLabel = styled ( "span" , {
context : ButtonContext ,
defaultVariants : {
size : "md" , // Lowest priority
} ,
});
< Button size = "lg" > { /* Context provides size="lg" */ }
< Button.Label /> { /* Gets size="lg" from context */ }
< Button.Label size = "sm" /> { /* Gets size="sm" from direct prop */ }
</ Button >
Local Variants
Not all variants need to propagate to children. Some behaviors are specific to a single component—like haptic feedback on a Button root, but not on its Text or Icon slots.
The Problem
Imagine you want your Button to have haptic feedback options, but this only makes sense for the Pressable root:
// ❌ This doesn't make sense
< Button haptics = "heavy" >
< Button.Text haptics = "heavy" /> { /* Text shouldn't have haptics */ }
< Button.Icon haptics = "heavy" /> { /* Icon shouldn't have haptics */ }
</ Button >
The Solution
Define variants outside of createStyledContext() to keep them local:
import { Pressable , Text } from "react-native" ;
import * as Haptics from "expo-haptics" ;
// Only "variant" propagates to children
const ButtonContext = createStyledContext ({
variant : [ "solid" , "bordered" , "ghost" ] ,
});
// Haptics is NOT in the context - it's local only
const haptics = {
soft : { onPress : () => Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Soft ) } ,
light : { onPress : () => Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Light ) } ,
heavy : { onPress : () => Haptics . impactAsync ( Haptics . ImpactFeedbackStyle . Heavy ) } ,
};
const ButtonRoot = styled ( Pressable , {
context : ButtonContext ,
base : { className : "flex-row items-center justify-center rounded-lg" } ,
variants : {
variant : {
solid : { className : "bg-blue-600" } ,
bordered : { className : "border-2 border-blue-600" } ,
ghost : { className : "bg-transparent" } ,
} ,
haptics , // ← Local variant, not in context
} ,
defaultVariants : {
variant : "solid" ,
haptics : "heavy" , // ← Has a default, but won't propagate
} ,
});
const ButtonText = styled ( Text , {
context : ButtonContext ,
base : { className : "font-semibold" } ,
variants : {
variant : { // ← Only "variant" is available here
solid : { className : "text-white" } ,
bordered : { className : "text-blue-600" } ,
ghost : { className : "text-blue-600" } ,
} ,
// No haptics here - ButtonText doesn't know about it
} ,
});
How It Works
Context variants (variant) - Defined in createStyledContext(), propagate to all children with the same context
Local variants (haptics) - Defined only in variants, stay on that component
< Button variant = "solid" haptics = "light" >
< Button.Text > Click me </ Button.Text >
</ Button >
// ButtonRoot: variant="solid", haptics="light" ✓
// ButtonText: variant="solid" (inherited), haptics=undefined (local to root)
When to Use Local Variants
✅ Use local variants for:
Platform-specific behaviors (haptics, animations)
Root-only interactions (press effects, gestures)
Variants that don’t make semantic sense on children
❌ Use context variants for:
Visual consistency (size, color, variant)
Semantic states (disabled, loading)
Anything children should know about
Function Composition
Local variants work great with function props. When you define onPress in a variant, it composes with any onPress passed to the component:
// Both haptics AND your custom handler execute
< Button haptics = "heavy" onPress = { () => console . log ( "clicked" ) } >
Submit
</ Button >
// → Haptics fire, then "clicked" logs
Shared Config
When multiple components share the same context and variants, you end up duplicating the config:
// ❌ Duplicated config
const StyledImage = styled ( UniwindImage , {
context : ImageCtx ,
variants : {
variant : {
solid : { className : "bg-black" } ,
bordered : { className : "border-2 border-gray-300" } ,
} ,
} ,
});
const StyledImageBg = styled ( UniwindImageBg , {
context : ImageCtx ,
variants : {
variant : {
solid : { className : "bg-black" } ,
bordered : { className : "border-2 border-gray-300" } ,
} ,
} ,
});
Use styledConfig() to create a single config validated against both components:
import { createStyledContext , styled , styledConfig , withSlots } from "better-styled" ;
import { Image as ExpoImage , ImageBackground } from "expo-image" ;
import { withUniwind } from "uniwind" ;
const UniwindImage = withUniwind ( ExpoImage );
const UniwindImageBg = withUniwind ( ImageBackground );
const ImageCtx = createStyledContext ({
variant : [ "solid" , "bordered" , "light" ] ,
});
// ✅ Single source of truth
const config = styledConfig ([ UniwindImage , UniwindImageBg ] , {
context : ImageCtx ,
base : { className : "rounded-lg" } ,
variants : {
variant : {
solid : { className : "bg-black" } ,
bordered : { className : "border-2 border-gray-300" } ,
light : { className : "bg-gray-100" } ,
} ,
} ,
defaultVariants : {
variant : "solid" ,
} ,
});
const ImageBase = styled ( UniwindImage , config );
const ImageBackgroundBase = styled ( UniwindImageBg , config );
export const Image = withSlots ( ImageBase , {
Background : ImageBackgroundBase ,
});
styledConfig() is an identity function — it returns the config unchanged with zero runtime overhead. It exists purely so TypeScript can validate the config against both components and give you full autocomplete.
styledConfig() uses the same type inference as styled(). No generics needed — just pass the components array and the config.
styledConfig() API Reference Signature, parameters, and type safety details
Nested Contexts
If you nest components with the same context, the nearest parent wins.
< Button size = "lg" >
< Button.Label > Large </ Button.Label >
< Button size = "sm" > { /* New context scope */ }
< Button.Label > Small </ Button.Label >
</ Button >
</ Button >
Without Context
Not every component needs context. For standalone components, just omit it:
// No context - variants are local only
const Badge = styled ( "span" , {
variants : {
color : {
gray : { className : "bg-gray-100" } ,
red : { className : "bg-red-100" } ,
} ,
} ,
});
Use context when you have parent-child relationships. Skip it for standalone elements.
Next: Slots Build compound components with dot notation