Styling¶
Every element factory takes a style prop. Its value is a
Style dict, a list of them (later entries win),
or None. On top of that, PythonNative gives you three tools that keep
styles typed and consistent as an app grows:
StyleSheetnamespaces group named styles with attribute access, so a misspelled style name is a static error.- A
Themeholds the app's design tokens (colors, typography, spacing, and radii), switches with dark mode, and also styles the navigators. use_stylesderives theme-dependent styles once per theme.
Inline styles¶
Pass a style dict to any component:
pn.Text("Hello", style={"color": "#FF3366", "font_size": 24, "bold": True})
pn.Button("Tap", style={"background_color": "#FF1E88E5", "color": "#FFFFFF"})
pn.Column(pn.Text("Content"), style={"background_color": "#FFF5F5F5"})
Typed styles with pn.style()¶
pn.style(**props) returns a
pn.Style, a TypedDict. Values are plain
Python dict instances at run time, but static checkers (mypy,
pyright, Pylance) know every key, and editors autocomplete keys and
Literal values:
import pythonnative as pn
heading: pn.Style = pn.style(
font_size=28,
font_weight="700", # Literal: "100".."900" | "bold" | "normal"
text_align="center", # Literal: "left" | "center" | "right" | "justify"
color="#0F172A",
)
pn.Text("Welcome", style=heading)
Why use pn.style() over a raw dict?
- Autocomplete for every supported key (
flex_direction,align_items,transform,shadow_offset, and the rest). - Checked literals: a typo like
align_items="centre"is flagged before you run the app. - Checked keys:
pn.style()is typed withUnpack[Style], sopn.style(colour="red")is a type error, not a silent no-op.
Because Style is total=False, every key is optional; include only
the ones you need. Plain dict literals are checked the same way when
they're passed straight to style= or annotated as pn.Style. In
development builds the factories also warn once about each unknown key
or bad value they see at run time. Release builds skip that check.
Style lists¶
Pass a list to combine styles. Entries merge left to right, so later
entries win, and None entries are skipped. That makes conditional
styles a one-liner:
pn.Text("Saved", style=[Styles.label, Styles.success if saved else None])
pn.View(content, style=[Styles.card, {"opacity": 0.5}])
A list is the only composition tool you need: there's no compose or
flatten helper to learn. When you need the merged dict itself (for
example, to read one key), call
pn.resolve_style, which flattens a
style prop into a fresh dict.
Styles in your own components¶
The type every built-in factory accepts is
pn.StyleProp:
Annotate a style parameter with it when your component passes the
caller's style through unchanged. When the component layers the
caller's style over its own, take a single pn.Style instead, so the
list stays one level deep:
import pythonnative as pn
class CardStyles(pn.StyleSheet):
card = pn.style(padding=16, border_radius=12, background_color="#FFFFFF")
@pn.component
def Card(*children: pn.Node, style: pn.Style | None = None) -> pn.Node:
return pn.View(*children, style=[CardStyles.card, style])
@pn.component
def Panel(*children: pn.Node, style: pn.StyleProp = None) -> pn.Node:
return pn.ScrollView(*children, style=style)
Callers can then override any key of card without losing the keys
they don't override: Card(pn.Text("Hi"), style={"padding": 24}).
Style sheets¶
Group the styles a module uses in a
StyleSheet namespace. Subclass it and
assign styles as class attributes:
import pythonnative as pn
class Styles(pn.StyleSheet):
container = pn.style(padding=16, gap=12, align_items="stretch")
title = pn.style(font_size=28, bold=True, color="#333333")
subtitle = pn.style(font_size=14, color="#666666")
@pn.component
def Welcome() -> pn.Node:
return pn.Column(
pn.Text("Welcome", style=Styles.title),
pn.Text("Subtitle", style=Styles.subtitle),
style=Styles.container,
)
- Attribute access is typed.
Styles.titelis a static error, and your editor autocompletes style names. (A dict of styles would accept any string key.) - Styles are checked once. In development builds each style's keys are validated when the class is defined, instead of every time an element is built.
- It's a namespace, not an object. Calling
Styles()raisesTypeError; refer toStyles.titledirectly.
pn.ABSOLUTE_FILL is the common
"fill the parent" overlay style, like React Native's
StyleSheet.absoluteFill:
Style sheets hold fixed values. For styles built from theme tokens,
see use_styles below.
Themes¶
A Theme is a frozen, keyword-only dataclass of
design tokens. Components read the active one with
use_theme, and the built-in navigators draw
their headers, tab bars, and drawers from the same tokens, so one theme
styles the whole app.
| Field | Type | Tokens |
|---|---|---|
dark |
bool |
Whether the theme is meant for a dark appearance |
colors |
Colors |
primary, background, surface, text, text_secondary, border, error, success, warning |
typography |
Typography |
title, heading, body, label, caption, each a Style |
spacing |
Spacing |
xs (4), sm (8), md (16), lg (24), xl (32) |
radii |
Radii |
sm (4), md (8), lg (16), full (9999) |
@pn.component
def Section(*children: pn.Node, title: str) -> pn.Node:
theme = pn.use_theme()
return pn.View(
pn.Text(title, style=[theme.typography.heading, {"color": theme.colors.text}]),
*children,
style={
"background_color": theme.colors.surface,
"border_radius": theme.radii.md,
"padding": theme.spacing.md,
"gap": theme.spacing.sm,
},
)
Without a provider, use_theme() returns the built-in
LIGHT_THEME or
DARK_THEME for the current color scheme,
and re-renders the component when the scheme changes. Themed components
are therefore dark-mode aware by default.
Custom themes¶
Provide your own light and dark themes with
ThemeProvider at the root of the app.
It picks light or dark from the effective color scheme. Derive a
theme from a built-in one with
dataclasses.replace:
from dataclasses import replace
import pythonnative as pn
LIGHT = replace(pn.LIGHT_THEME, colors=replace(pn.LIGHT_THEME.colors, primary="#5B21B6"))
DARK = replace(pn.DARK_THEME, colors=replace(pn.DARK_THEME.colors, primary="#A78BFA"))
@pn.component
def Home() -> pn.Node:
theme = pn.use_theme()
return pn.Text("Hello", style=[theme.typography.title, {"color": theme.colors.primary}])
@pn.component
def App() -> pn.Node:
return pn.ThemeProvider(Home(), light=LIGHT, dark=DARK)
In an app with navigation, wrap the container,
pn.ThemeProvider(pn.NavigationContainer(Root), light=LIGHT, dark=DARK),
so the navigators' headers and tab bars use the same colors. Pass the same theme as both light and dark to pin it regardless of
the appearance. A nested ThemeProvider overrides the theme for its
subtree only.
Adding tokens¶
Themes are dataclasses, so you add tokens by subclassing. Keep
frozen=True, kw_only=True, give the new fields defaults, and read
the subclass with use_theme(BrandTheme), which returns a
BrandTheme to your type checker:
from dataclasses import dataclass, replace
import pythonnative as pn
@dataclass(frozen=True, kw_only=True)
class BrandTheme(pn.Theme):
accent: pn.Color = "#FF2D55"
card_elevation: float = 4
LIGHT = BrandTheme(colors=replace(pn.LIGHT_THEME.colors, primary="#5B21B6"))
DARK = BrandTheme(dark=True, colors=replace(pn.DARK_THEME.colors, primary="#A78BFA"), accent="#FF6482")
@pn.component
def Badge(count: int) -> pn.Node:
theme = pn.use_theme(BrandTheme)
return pn.Text(str(count), style={"background_color": theme.accent, "color": "#FFFFFF"})
@pn.component
def App() -> pn.Node:
return pn.ThemeProvider(Badge(count=3), light=LIGHT, dark=DARK)
use_theme(BrandTheme) raises TypeError when the active theme isn't
a BrandTheme, which usually means the app forgot its
ThemeProvider.
Theme-dependent styles with use_styles¶
Building style dicts from tokens on every render is noisy and
allocates new dicts each time. use_styles
calls a factory with the active theme once per theme and returns the
result. The usual factory is a small class whose __init__ takes the
theme and assigns styles as attributes, so the styles are typed and
misspelled names are static errors:
import pythonnative as pn
class RowStyles:
def __init__(self, theme: pn.Theme) -> None:
self.row = pn.style(
padding=theme.spacing.md,
gap=theme.spacing.xs,
border_bottom_width=1,
border_bottom_color=theme.colors.border,
)
self.title: pn.Style = {**theme.typography.label, "color": theme.colors.text}
self.subtitle: pn.Style = {**theme.typography.caption, "color": theme.colors.text_secondary}
@pn.component
def MessageRow(sender: str, preview: str) -> pn.Node:
styles = pn.use_styles(RowStyles)
return pn.Column(
pn.Text(sender, style=styles.title),
pn.Text(preview, style=styles.subtitle),
style=styles.row,
)
use_styles accepts any callable that takes a theme, including a
plain function. A factory may annotate its parameter with your Theme
subclass (def __init__(self, theme: BrandTheme)) to read custom
tokens; use_styles passes it the active theme, so provide
BrandTheme instances with ThemeProvider. When the user switches to
dark mode, the component re-renders and the factory runs again with
the dark theme; otherwise every render reuses the same style objects.
Use a StyleSheet for styles that never change and use_styles for
styles that read the theme. Both combine freely in style lists.
Dark mode¶
Three tools cover dark mode, from most to least common:
- Theme tokens. Components that read colors from the theme (with
use_themeoruse_styles) switch automatically. - Dynamic colors. A
DynamicColor, written{"light": ..., "dark": ...}, is resolved by the renderer itself, without a re-render. See Colors. - The color scheme itself.
use_color_schemereturns the effective scheme ("light"or"dark") and re-renders the component when it changes, including live when the user flips the system setting while the app is open.
@pn.component
def Wallpaper() -> pn.Node:
scheme = pn.use_color_scheme()
return pn.Image(source="night.png" if scheme == "dark" else "day.png", style={"flex": 1})
An in-app appearance toggle overrides the system setting through the
appearance module. ThemeProvider,
use_theme, and every renderer follow the override:
pn.appearance.set_color_scheme("dark") # force dark everywhere
pn.appearance.set_color_scheme(None) # follow the system again
Colors¶
Pass hex strings (#RRGGBB or #AARRGGBB) to color properties inside style:
pn.Text("Hello", style={"color": "#FF3366"})
pn.Button("Tap", style={"background_color": "#FF1E88E5", "color": "#FFFFFF"})
Every color key is typed as Color: a string, or
a DynamicColor dictionary with a light
and a dark entry. iOS, Android, and the browser preview all resolve
the pair against the current color scheme when the style is applied and
switch when the appearance changes, without a re-render:
Text styling¶
Text accepts the full typography surface inside style:
| Prop | Value | Notes |
|---|---|---|
font_size |
number | In pt (iOS) / sp (Android) |
color |
hex string | #RRGGBB or #AARRGGBB |
bold |
bool | Shorthand for font_weight: "bold" |
font_weight |
"normal", "bold", "100"–"900" |
|
font_family |
string | System font name or the family of a font under app/assets/ (see Assets) |
italic |
bool | |
text_align |
"left", "center", "right", "justify" |
|
letter_spacing |
number | Tracking in points |
line_height |
number | Multiple of font size |
text_decoration |
"underline", "line_through", or None |
|
text_transform |
"none", "uppercase", "lowercase", "capitalize" |
Applied before measurement |
max_lines |
int | Truncate after N lines |
text_shadow_color |
hex string | |
text_shadow_offset |
{"width": x, "height": y} or (x, y) |
|
text_shadow_radius |
number (blur radius) |
pn.Text(
"Headline",
style={
"font_size": 28,
"font_weight": "700",
"letter_spacing": -0.5,
"line_height": 32,
"color": "#0F172A",
},
)
text_transform is applied in Python before the string reaches the
native label, so the layout engine measures the transformed text and
rich-text spans inherit the outer element's transform. "capitalize"
upper-cases the first character of each word and leaves the rest as
written (it doesn't lower-case like str.title()).
Text shadows render through NSShadow on iOS,
TextView.setShadowLayer on Android, and CSS text-shadow in the
browser preview:
pn.Text(
"Overlay caption",
style={
"color": "#FFFFFF",
"text_transform": "uppercase",
"text_shadow_color": "#00000099",
"text_shadow_offset": {"width": 0, "height": 1},
"text_shadow_radius": 3,
},
)
Rich text (nested spans)¶
Text accepts a mix of strings and nested Text elements, which
flatten into one native label (a SpannableString on Android, an
NSAttributedString on iOS). Each nested Text styles its own run
and inherits everything it doesn't override from the outer element:
pn.Text(
"Every plan includes ",
pn.Text("unlimited builds", style={"bold": True}),
" and ",
pn.Text("priority support", style={"color": "#DC2626", "text_decoration": "underline"}),
".",
style={"font_size": 15, "color": "#0F172A"},
)
Because the result is a single label, it wraps, truncates
(max_lines), and measures as one paragraph; there's no need to
assemble rows of separate Text views to mix weights or colors
inline.
Pressed-state styles¶
Pressable's style prop also accepts a
callable, mirroring React Native's function-style prop. It receives a
PressState (a frozen dataclass with
pressed: bool) and is re-applied as the press state changes; a
single callable child receives the same state:
pn.Pressable(
lambda state: pn.Text("Saving" if state.pressed else "Save"),
on_press=save,
disabled=not valid,
android_ripple=pn.Ripple(color="#33000000"),
style=lambda state: pn.style(
padding=12,
border_radius=8,
background_color="#1D4ED8" if state.pressed else "#3B82F6",
),
)
disabled=True suppresses every press callback and sets the disabled
accessibility state. Ripple draws Android's
material ripple while pressed and is ignored on iOS and in the browser,
where pressed_opacity or the callable style provides the feedback.
Borders, shadows, and shape¶
Every element accepts these visual props in style:
| Prop | Value |
|---|---|
border_radius |
number (uniform) |
border_top_left_radius, border_top_right_radius, border_bottom_left_radius, border_bottom_right_radius |
number (per corner) |
border_width |
number (in pt / dp) |
border_color |
hex string |
border_left_width, border_top_width, border_right_width, border_bottom_width |
number (per side) |
border_left_color, border_top_color, border_right_color, border_bottom_color |
hex string (per side) |
border_style |
"solid" (default), "dashed", or "dotted", drawn the same way on every renderer |
shadow_color |
hex string (iOS and browser) |
shadow_offset |
{"width": x, "height": y} or (x, y) (iOS and browser) |
shadow_opacity |
0.0 – 1.0 (iOS and browser) |
shadow_radius |
number (blur radius; iOS and browser) |
elevation |
number (Android Material shadow) |
opacity |
0.0 – 1.0 |
tint_color |
hex string (Image only) |
Per-corner radius props override the uniform border_radius for the
corners they name, so a "speech bubble" or "top-rounded sheet" shape
needs no images:
pn.View(
content,
style={
"border_top_left_radius": 16,
"border_top_right_radius": 16,
"background_color": "#FFFFFF",
},
)
Borders take up space inside the element's frame, exactly like
padding: a child inside a border_width: 4 parent starts 4 points in
from the parent's edge, and a content-sized parent grows by its border
on every side. This matches Yoga's box model, so styles ported from
React Native line up without adjustment.
Per-side border props override the uniform border_width /
border_color for the sides they name, so an "underline" card is
just:
pn.View(
pn.Text("Active tab"),
style={"border_bottom_width": 2, "border_bottom_color": "#007AFF"},
)
Shadows follow React Native's platform split: the shadow_* keys draw
on iOS and in the browser preview, elevation draws Android's material
shadow, and each platform ignores the other's keys (Android no longer
silently accepts shadow_offset). A card that looks right everywhere
sets both:
pn.View(
pn.Text("Card"),
style={
"padding": 20,
"background_color": "#FFFFFF",
"border_radius": 16,
"border_width": 1,
"border_color": "#E5E7EB",
"shadow_color": "#000000",
"shadow_offset": {"width": 0, "height": 4},
"shadow_opacity": 0.08,
"shadow_radius": 12,
"elevation": 4,
},
)
Transforms¶
transform (TransformSpec) is a single
operation or an ordered list of operations, each a one-key mapping:
pn.View(
pn.Text("Tilted"),
style={
"transform": [{"rotate": 15}, {"scale": 1.1}, {"translate_x": 10}],
},
)
pn.View(
card,
style={"transform": [{"perspective": 800}, {"rotate_y": "35deg"}]},
)
Supported operations on every renderer: rotate (degrees, or a string
with a deg or rad suffix), rotate_x, rotate_y, rotate_z,
scale, scale_x, scale_y, translate_x, translate_y, skew_x,
skew_y, and perspective (the distance for the 3D rotations that
follow it in the list). iOS composes the list into a CATransform3D;
Android applies skew through the animation matrix on API 29 and later
and ignores it below that with a one-time log. For animated transforms,
see Animations.
Flex layout¶
PythonNative uses the Yoga flexbox engine inside the shared renderer core,
compiled into each mobile app and provided as WebAssembly in the browser
preview (see
Layout engine). View is
the universal flex container, and Column/Row are convenience
wrappers that fix the direction.
Flex container properties¶
These go in the style dict of View, Column, or Row:
flex_direction:"column"(default),"row","column_reverse","row_reverse"(only forView;ColumnandRowhave fixed directions).justify_content: main-axis distribution:"flex_start","center","flex_end","space_between","space_around","space_evenly".align_items: cross-axis alignment:"stretch","flex_start","center","flex_end","baseline".overflow:"visible"(default),"hidden".gap: space between children (dp / pt).row_gapandcolumn_gapset one axis.padding: inner spacing (int for all sides, or dict).
align_items: "baseline" (rows only) lines children up along a shared
baseline instead of their top edges. Text reports the position of
its first line's baseline; other leaves use their height (they align
along their bottom edges), and a container's baseline is that of its
first in-flow child. Columns treat "baseline" as "flex_start", matching
Yoga.
Child layout properties¶
All components accept these in style:
width,height: fixed dimensions (number in dp / pt, or percentage string like"50%").min_width,min_height,max_width,max_height: size constraints.aspect_ratio: derive the unknown axis from the known one (width / height).flex: shorthand forflex_grow: N, flex_shrink: 1, flex_basis: 0.flex_grow,flex_shrink,flex_basis: explicit flex properties.margin: outer spacing (number for all sides, a dict, or"auto"; per-edge keys such asmargin_leftalso accept"auto").align_self: override parent alignment:"auto","flex_start","center","flex_end","stretch","baseline".display:"flex"(default) or"none". A"none"element and its subtree are removed from layout entirely: no size, gap, or margin is reserved for it and every frame in the subtree is zero. Toggle it to hide content without unmounting it (state and native views are kept).position:"relative"(default) or"absolute".top,right,bottom,left: edge offsets whenposition: "absolute"(number or percentage string).inset,inset_horizontal,inset_vertical: shorthands for all four edges, the horizontal pair, or the vertical pair, mirroring the padding vocabulary; explicit edge keys win.pn.style(position="absolute", inset=0)is the same overlay aspn.ABSOLUTE_FILL.z_index: stacking order among siblings. Higher values render on top regardless of declaration order; siblings without one keep document order. Essential for absolutely positioned overlays like collapsing headers and floating action buttons.
Layout examples¶
Centering content:
pn.View(
pn.Text("Centered!"),
style={"flex": 1, "justify_content": "center", "align_items": "center"},
)
Horizontal row with spacer:
pn.Row(
pn.Text("Left"),
pn.Spacer(flex=1),
pn.Text("Right"),
style={"padding": 16, "align_items": "center"},
)
Auto margins instead of a spacer:
Auto margins absorb the free space on the main axis before
justify_content is applied, following CSS and Yoga. When any child
on a line has an auto main-axis margin, the remaining space is split
equally among every auto margin on that line and justify_content has
no effect. On the cross axis, margin_top: "auto" and friends center
or push the child and override align_items / align_self (including
stretch):
pn.Row(
pn.Text("Left"),
pn.Text("Right", style={"margin_left": "auto"}), # pushed to the trailing edge
style={"padding": 16},
)
pn.Column(
pn.Text("Centered", style={"margin_horizontal": "auto"}), # centered without align_items
style={"flex": 1},
)
Child with flex grow:
pn.Column(
pn.Text("Header", style={"font_size": 20, "bold": True}),
pn.View(pn.Text("Content area"), style={"flex": 1}),
pn.Text("Footer"),
style={"flex": 1, "gap": 8},
)
Horizontal button bar:
pn.Row(
pn.Button("Cancel", style={"flex": 1}),
pn.Button("OK", style={"flex": 1, "background_color": "#007AFF", "color": "#FFF"}),
style={"gap": 8, "padding": 16},
)
Absolute positioning:
pn.View(
pn.View(style={"position": "absolute", "top": 0, "left": 0,
"width": 40, "height": 40, "background_color": "#F00"}),
pn.View(style={"position": "absolute", "bottom": 0, "right": 0,
"width": 40, "height": 40, "background_color": "#0A0"}),
pn.Text("Centered overlay", style={
"position": "absolute",
"top": "50%", "left": "10%", "right": "10%",
"text_align": "center",
}),
style={"width": 240, "height": 160, "background_color": "#EEE"},
)
Aspect-ratio thumbnail grid cell:
pn.View(
pn.Image(source="cover.jpg", style={"flex": 1}),
style={"width": "33%", "aspect_ratio": 1.0, "padding": 4},
)
Layout with Column and Row¶
Column (vertical) and Row (horizontal) are convenience wrappers for View:
pn.Column(
pn.Text("Username"),
pn.TextInput(placeholder="Enter username"),
pn.Text("Password"),
pn.TextInput(placeholder="Enter password", secure=True),
pn.Button("Login", on_press=handle_login),
style={"gap": 8, "padding": 16, "align_items": "stretch"},
)
Alignment properties¶
Column and Row support align_items and justify_content inside
style:
align_items: cross-axis alignment:"stretch","flex_start","center","flex_end","baseline","leading","trailing".justify_content: main-axis distribution:"flex_start","center","flex_end","space_between","space_around","space_evenly".
pn.Row(
pn.Text("Left"),
pn.Spacer(flex=1),
pn.Text("Right"),
style={"align_items": "center", "justify_content": "space_between", "padding": 16},
)
Gap¶
gapsets the space between children in dp (Android) and points (iOS).row_gapandcolumn_gapset one axis. Use the theme's spacing scale to keep gaps consistent.
Padding¶
padding: 16: all sides.padding: {"horizontal": 12, "vertical": 8}: per axis.padding: {"left": 8, "top": 16, "right": 8, "bottom": 16}: per side.
Interaction surface¶
A few props shape how views participate in touch handling and layout measurement.
pointer_events¶
The pointer_events style key controls whether a view (and its
subtree) takes part in hit testing:
"auto"(default): normal hit testing."none": the view and its children are invisible to touches; taps pass through to whatever is underneath."box_none": the view itself ignores touches but its children still receive them, the right setting for full-screen overlay containers that host a few interactive widgets."box_only": the view receives touches but its children don't.
pn.View(
style=[pn.ABSOLUTE_FILL, {
"background_color": "#00000022",
"pointer_events": "none", # decorative scrim; taps pass through
}],
)
hit_slop¶
hit_slop expands a pressable area beyond the view's visual bounds,
so small controls stay comfortably tappable. Pass a number for a
uniform expansion or a dict with top / left / bottom / right:
pn.Pressable(
pn.Image(source="close.png", style={"width": 16, "height": 16}),
on_press=dismiss,
hit_slop=12, # 40 x 40 effective target
)
View, Column, Row, and Pressable all accept it.
on_layout¶
The on_layout prop reports the element's computed frame after each
layout pass in which it changed. The callback receives a
LayoutEvent with x, y, width, and
height in the parent's coordinate space:
def handle_layout(frame: pn.LayoutEvent) -> None:
set_width(frame.width)
pn.View(content, on_layout=handle_layout)
The same frame is available without a callback as ref.current.frame
on the element's handle.
The callback runs post-commit, so setting state inside it is safe and schedules a normal re-render. Use it for measure-then-position patterns (tooltips, anchored popovers) or container-driven item sizing.
ScrollView¶
Wrap content in a ScrollView. Style the
frame with style and the scrollable content with
content_container_style; the children are wrapped in one inner
View carrying that style, so padding, gap, and alignment work on
every renderer:
pn.ScrollView(
pn.Text("Item 1"),
pn.Text("Item 2"),
horizontal=False,
content_container_style=pn.style(padding=16, gap=8),
content_inset=pn.EdgeInsets(bottom=80), # keep the last row clear of a footer
style=pn.style(flex=1),
)
horizontal=True scrolls along the x axis. scroll_enabled=False
freezes user scrolling while imperative scrolling through the
ScrollViewHandle still works;
snap_to_interval and snap_to_alignment build carousels;
keyboard_should_persist_taps decides whether a tap inside the scroll
view dismisses the keyboard ("never", "always", or "handled");
and deceleration_rate ("normal", "fast", or a float) tunes the
fling. on_scroll, on_scroll_begin_drag, on_scroll_end_drag, and
on_momentum_scroll_end each receive a
ScrollEvent; scroll_event_throttle (in
milliseconds) caps how often on_scroll fires.
Next steps¶
- See it in practice: Forms, Lists.
- Browse the API: Style, Components.
- Forward typed styles through your own widgets: Custom native components.
- Learn about reconciliation and how style props are diffed: Reconciliation.