develop

Styling and themes

Styles are dicts with React Native keys. Themed style sheets follow light and dark mode, safe area insets and breakpoints by themselves, with no hook and no re-render.

StyleSheet

StyleSheet.create takes a dict of named styles, or keyword arguments. pyxpo checks the sheet when it is created: a bad value, such as a color it cannot read, raises StyleError, and an unknown key prints a warning.

app/index.py
from pyxpo import StyleSheet, Text, View

styles = StyleSheet.create({
    "card": {"padding": 16, "borderRadius": 16, "backgroundColor": "#fff"},
    "title": {"fontSize": 18, "fontWeight": "700"},
})

def Card(title):
    return View(Text(title, style=styles.title), style=[styles.card, {"marginTop": 8}])
  • Keys work in camelCase (backgroundColor) and snake_case (background_color).
  • A list of styles merges from left to right. False and None entries are skipped, so [styles.row, active and styles.active] works.
  • Flexbox works as in React Native: flexDirection, gap, flexWrap, alignContent, percent sizes and more, on every target.

Themes

The theme API follows react-native-unistyles 3. Register your themes once, then give StyleSheet.create a function of theme and rt. Components only use styles.name.

app/_theme.py
from pyxpo import StyleSheet

LIGHT = {"colors": {"bg": "#F4F5FB", "card": "#FFFFFF", "text": "#12142B", "accent": "#6C5CE7"}}
DARK = {"colors": {"bg": "#0C0D1A", "card": "#171933", "text": "#F2F3FF", "accent": "#8B7CFF"}}

StyleSheet.configure(themes={"light": LIGHT, "dark": DARK})
app/index.py
from pyxpo import StyleSheet, Text, View, component
import app._theme  # registers the themes

styles = StyleSheet.create(lambda theme, rt: {
    "screen": {"flex": 1, "backgroundColor": theme.colors.bg, "paddingTop": rt.insets.top},
    "card": {"padding": 16, "borderRadius": 16, "backgroundColor": theme.colors.card},
    "title": {"fontSize": 20, "fontWeight": "700", "color": theme.colors.text},
})

@component
def Home():
    return View(View(Text("Hello", style=styles.title), style=styles.card), style=styles.screen)

default = Home

With themes named light and dark, the app follows the system setting. When the user switches to dark mode, pyxpo rebuilds the sheet for the dark theme and sends new styles only to the views that use it. Home does not run again.

Themes can hold anything, not only colors: spacing, radii, font sizes. Read them as attributes (theme.space.md) or as keys (theme["space"]["md"]). A missing key raises AttributeError with its name.

Pick a theme in code

python
StyleSheet.setTheme("dark")          # show dark and stop following the system
StyleSheet.setAdaptiveThemes(True)   # follow the system again

# More than two themes, or start on one:
StyleSheet.configure(
    themes={"light": LIGHT, "dark": DARK, "sepia": SEPIA},
    adaptiveThemes=False,
    initialTheme="sepia",
)

The rt argument

rt holds device values that styles often need. When one of them changes, the styles that read it update.

FieldValue
rt.insetsSafe area insets in dp: rt.insets.top, .right, .bottom, .left.
rt.breakpointThe active breakpoint, such as "md".
rt.color_schemeThe system setting: "light" or "dark".
rt.theme_nameThe name of the theme on screen.
rt.font_scaleThe text size the user picked in the system settings.

Dynamic styles

A style that depends on a value from the component is a function entry. Call it with the value. Each set of arguments is checked once and cached.

python
styles = StyleSheet.create(lambda theme, rt: {
    "avatar": lambda size: {"width": size, "height": size, "borderRadius": size / 2,
                            "backgroundColor": theme.colors.accent},
})

View(style=styles.avatar(48))

Function entries work in a plain StyleSheet.create({...}) as well.

Breakpoint values

Any style value can be a breakpoint map. It takes the value of the largest breakpoint at or below the current window width, mobile first. See Responsive layout for the full guide.

python
styles = StyleSheet.create(lambda theme, rt: {
    "title": {"fontSize": {"base": 20, "lg": 28}, "color": theme.colors.text},
    "list": {"flexDirection": {"base": "column", "md": "row"}, "gap": 12},
})

useAppTheme

Styles update without any hook. For values that are not styles, such as an icon color, read the theme with useAppTheme(). The component re-renders when the theme changes.

python
from pyxpo import Pressable, component, useAppTheme
from pyxpo.vector_icons import Feather
from pyxpo.router import router

@component
def BackButton():
    theme = useAppTheme()
    return Pressable(Feather(name="arrow-left", size=22, color=theme.colors.text),
                     onPress=router.back, style=styles.back)

useTheme in pyxpo.router is the navigation theme (header and tab bar colors). The app theme from StyleSheet.configure is useAppTheme.

Themed styles are dicts

styles.card is a dict with the values of the current theme. Lists, {**styles.card} and StyleSheet.flatten work as with any style.

Reference

CallWhat it does
StyleSheet.configure(themes, breakpoints, adaptive_themes, initial_theme)Register the themes once, before the app renders. camelCase keywords (adaptiveThemes, initialTheme) work too. Calling it again replaces the themes.
StyleSheet.create(lambda theme, rt: {...})A themed sheet. lambda theme: {...} works when the sheet does not need rt.
StyleSheet.setTheme(name)Show a theme and stop following the system. An unknown name raises ValueError.
StyleSheet.setAdaptiveThemes(on)Follow the system light / dark setting again, or stop following it.
useAppTheme()The current theme in a component. Re-renders the component when the theme changes.
StyleSheet.flatten(style)Merge a list of styles into one dict. Works with themed styles.

Every call has a snake_case name too (set_theme, set_adaptive_themes, use_app_theme). The same functions live in pyxpo.themes. A complete app that uses all of this is examples/imagekit in the pyxpo repository.