Theming
The theme is the set of tokens that every component reads: color palette, typography, spacing and radius. It is part of core, so you copy it once and then edit it in your project.
Using the theme
Wrap your app with ThemeProvider. Components read the active theme with useTheme() and the color mode with useThemeMode(), which returns the current mode and a setMode function.
ThemeProvider accepts a defaultMode prop, 'light' by default.
Palette
Each palette color has three values you use in your UI:
mainfills controls.contrastTextis the text color on top ofmain.textColoris the same hue in a variant that meets WCAG AA as text onbackground.default.
import { StyleSheet, Text, View } from 'react-native';
import { useTheme } from 'tessera';
const colorKeys = [
'primary',
'secondary',
'success',
'info',
'warning',
'error',
] as const;
export function PaletteExample() {
const theme = useTheme();
return (
<View style={styles.container}>
{colorKeys.map(key => {
const color = theme.palette[key];
return (
<View
key={key}
style={[
styles.row,
{ backgroundColor: theme.palette.background.paper },
]}>
<View style={[styles.swatch, { backgroundColor: color.main }]} />
<Text style={[styles.label, { color: theme.palette.text.primary }]}>
{key}
</Text>
<Text style={[styles.sample, { color: color.textColor }]}>
Sample text
</Text>
</View>
);
})}
</View>
);
}
const styles = StyleSheet.create({
container: {
gap: 8,
},
row: {
flexDirection: 'row',
alignItems: 'center',
gap: 12,
padding: 12,
borderRadius: 8,
},
swatch: {
width: 24,
height: 24,
borderRadius: 4,
},
label: {
flex: 1,
fontSize: 14,
fontWeight: '600',
},
sample: {
fontSize: 14,
},
});
Mode toggle
useThemeMode lets a control switch between light and dark mode.
import { Pressable, StyleSheet, Text, View } from 'react-native';
import { useTheme, useThemeMode } from 'tessera';
export function ModeToggleExample() {
const theme = useTheme();
const { mode, setMode } = useThemeMode();
const nextMode = mode === 'light' ? 'dark' : 'light';
return (
<View
style={[
styles.container,
{ backgroundColor: theme.palette.background.default },
]}>
<Text style={[styles.label, { color: theme.palette.text.primary }]}>
Current mode: {mode}
</Text>
<Pressable
accessibilityRole="button"
accessibilityLabel={`Switch to ${nextMode} mode`}
onPress={() => setMode(nextMode)}
style={({ pressed }) => [
styles.button,
{
backgroundColor: theme.palette.primary.main,
opacity: pressed ? 0.8 : 1,
},
]}>
<Text
style={[
styles.buttonLabel,
{ color: theme.palette.primary.contrastText },
]}>
Switch to {nextMode}
</Text>
</Pressable>
</View>
);
}
const styles = StyleSheet.create({
container: {
gap: 12,
padding: 16,
borderRadius: 8,
},
label: {
fontSize: 14,
},
button: {
minHeight: 48,
alignItems: 'center',
justifyContent: 'center',
paddingHorizontal: 16,
borderRadius: 8,
},
buttonLabel: {
fontSize: 14,
fontWeight: '500',
},
});
Contrast
Text colors are chosen to meet WCAG AA on background.default in both modes. When you add or change a color, check these controls in light and dark mode:
textColoronbackground.default: at least 4.5:1.contrastTextonmain: at least 4.5:1.contrastTextondark(the pressed state): at least 4.5:1.mainanddarkonbackground.default: at least 3:1, for controls.
Disabled text is exempt, as WCAG allows.
If a color does not meet a control, adjust the tones of its ramp in tokens.ts until it does.
palette.divider is decorative. It does not reach 3:1 on the background, so do not use it as the border of a control.
Adding colors
You can add a color ramp, for example tertiary:
- Add the tones from
50to900to the ramp values. - Add the color to
paletteincreateAppTheme. - Add its key to the
AppThemepalette type. - Check the contrast controls above in both modes.
Adding tokens is backward compatible. Renaming or removing a token is a breaking change for anyone who already copied core.