Style properties
Every property the style prop (and the hoverStyle/pressStyle/focusStyle
state styles) accepts, generated from the library's style registry. An app
can register its own properties on top of these.
Animatable properties accept an inline { animated: sharedValue }
binding in place of the value; "per field" means the value is an object whose
individual fields accept one.
| Property | Type | Animatable | Guide |
|---|---|---|---|
display |
"flex" | "grid" | "block" | "none" |
Flexbox | |
boxSizing |
"borderBox" | "border-box" | "contentBox" | "content-box" |
Sizing | |
positionType |
"absolute" | "relative" |
Positioning | |
overflowX |
"visible" | "clip" | "hidden" | "scroll" |
Overflow | |
overflowY |
"visible" | "clip" | "hidden" | "scroll" |
Overflow | |
scrollbarWidth |
number |
Overflow | |
left |
Length |
yes | Positioning |
right |
Length |
yes | Positioning |
top |
Length |
yes | Positioning |
bottom |
Length |
yes | Positioning |
width |
Length |
yes | Sizing |
height |
Length |
yes | Sizing |
minWidth |
Length |
yes | Sizing |
minHeight |
Length |
yes | Sizing |
maxWidth |
Length |
yes | Sizing |
maxHeight |
Length |
yes | Sizing |
aspectRatio |
number |
yes | Sizing |
alignItems |
"start" | "end" | "flexStart" | "flexEnd" | "center" | "baseline" | "stretch" |
Flexbox | |
justifyItems |
"start" | "end" | "center" | "baseline" | "stretch" |
Grid | |
alignSelf |
"auto" | "start" | "end" | "flexStart" | "flexEnd" | "center" | "baseline" | "stretch" |
Flexbox | |
justifySelf |
"auto" | "start" | "end" | "center" | "baseline" | "stretch" |
Grid | |
alignContent |
"start" | "end" | "flexStart" | "flexEnd" | "center" | "stretch" | "spaceBetween" | "spaceEvenly" | "spaceAround" |
Flexbox | |
justifyContent |
"start" | "end" | "flexStart" | "flexEnd" | "center" | "stretch" | "spaceBetween" | "spaceEvenly" | "spaceAround" |
Flexbox | |
margin |
Rect |
Spacing | |
padding |
Rect |
Spacing | |
border |
Rect |
Borders | |
flexDirection |
"row" | "column" | "rowReverse" | "columnReverse" |
Flexbox | |
flexWrap |
"nowrap" | "noWrap" | "wrap" | "wrapReverse" |
Flexbox | |
flexGrow |
number |
Flexbox | |
flexShrink |
number |
Flexbox | |
flexBasis |
Length |
yes | Flexbox |
gap |
Length |
yes | Flexbox |
rowGap |
Length |
yes | Flexbox |
columnGap |
Length |
yes | Flexbox |
gridAutoFlow |
"row" | "column" | "rowDense" | "columnDense" |
Grid | |
gridTemplateRows |
string |
Grid | |
gridTemplateColumns |
string |
Grid | |
gridAutoRows |
string |
Grid | |
gridAutoColumns |
string |
Grid | |
gridRow |
string |
Grid | |
gridColumn |
string |
Grid | |
backgroundColor |
Color |
yes | Colors |
borderColor |
Animatable<Color> | { top?: Color; right?: Color; bottom?: Color; left?: Color } |
Borders | |
borderRadius |
Rect |
yes | Borders |
outline |
{ width?: Length; offset?: Length; color?: Color } |
Borders | |
boxShadow |
BoxShadow | BoxShadow[] |
Shadows | |
filter |
FilterChainValue |
per field | Filters |
backdropFilter |
FilterChainValue |
per field | Backdrop filters |
morphFilter |
MorphFilterValue |
per field | Morph filters |
backgroundGradient |
Gradient | Gradient[] |
per field | Gradients |
borderGradient |
Gradient | Gradient[] |
per field | Gradients |
backgroundImage |
BackgroundImage |
per field | Background images |
imageRendering |
"auto" | "bilinear" | "trilinear" | "nearest" |
Image rendering | |
layoutRounding |
boolean |
Layout rounding | |
zIndex |
number |
Z-index | |
globalZIndex |
number |
Z-index | |
focusPolicy |
"block" | "pass" |
Focus policy | |
cursor |
SystemCursor | (string & {}) |
Cursors | |
scrollbar |
"none" | "default" | ScrollbarStyle |
Overflow | |
transform |
BevyTransform |
per field | Transforms |
transform3d |
BevyTransform3d |
per field | 3D transforms |
opacity |
number |
yes | Opacity |
groupAlpha |
boolean |
Opacity | |
cache |
"auto" | "always" | "never" |
Layers | |
transition |
BevyTransition |
Style transitions | |
color |
Color |
yes | |
fontSize |
FontSize |
||
fontWeight |
"thin" | "light" | "normal" | "medium" | "semibold" | "bold" | "black" | (string & {}) |
||
fontFamily |
string |
||
textAlign |
"left" | "center" | "right" | "justify" | "start" | "end" |
||
lineHeight |
number | string | { px: number } |
||
letterSpacing |
number | string | { rem: number } |
||
textShadow |
{ color?: Color; offsetX?: number; offsetY?: number } |
||
lineBreak |
"wordBoundary" | "anyCharacter" | "wordOrCharacter" | "noWrap" |
Value types
The named types used above, as declared in the bevy-react package.
Length
/** A length: a bare number is logical pixels; a string carries a unit
* (`"50%"`, `"100vw"`, `"100vh"`, `"50vmin"`, `"50vmax"`, `"10px"`, `"auto"`). */
export type Length = number | string;Rect
/** Four sides/corners: a number (uniform), a CSS shorthand string
* (`"8px"`, `"8px 16px"`, `"1px 2px 3px 4px"`), an explicit per-side object, or
* an axis pair — `horizontal` sets left + right, `vertical` sets top + bottom.
* On `borderRadius` the sides name the four corners, so `horizontal` is the
* top-right + bottom-left pair. */
export type Rect =
| number
| string
| { top?: Length; right?: Length; bottom?: Length; left?: Length }
| { horizontal?: Length; vertical?: Length };Color
/** A CSS color string. Accepts hex (`"#f00"`, `"#1e1e2e"`, `"#1e1e2eaa"`), a
* named color (`"red"`, `"rebeccapurple"`), `"transparent"`, or functional
* notation: `"rgb(255 0 0 / 50%)"` / `"rgba(255,0,0,.5)"`, `"hsl(0 100% 50%)"`,
* `"hwb(0 0% 0%)"`, `"oklab(0.7 0.1 0.05)"`, `"oklch(0.7 0.1 30)"`. An
* unrecognized value renders as a loud magenta (and logs a warning). */
export type Color = string;BoxShadow
/** One drop shadow. Offsets/radii are [`Length`]s (bare number = px). */
export type BoxShadow = {
color?: Color;
xOffset?: Length;
yOffset?: Length;
spreadRadius?: Length;
blurRadius?: Length;
};FilterChainValue
/** The `filter` style value: one [`FilterUse`] (a 1-element chain) or an
* ordered array of them — chain order is pass order. */
export type FilterChainValue = FilterUse | FilterUse[];MorphFilterValue
/** The `morphFilter` style value: a morph filter name from the separate
* [`BevyMorphFilters`] registry, its params, and the morph identity `key`.
* When `key` changes, the node's previous rendered appearance is frozen and
* the named two-input filter blends it into the live content, driven by an
* engine-owned progress eased over a built-in 300ms default (override with
* `transition: { morphFilter }`). The built-in morphs are `crossfade`,
* `linearWipe`, and `pixelize`; register customs with
* `#[react_morph_filter]` + `add_react_morph_filter` (the `morph_*` prelude
* helpers give the shader the two inputs + progress). Regular filters are
* not accepted here — the families are separate, in typing and at resolve
* time. Every param is individually optional (an omitted param takes the
* filter's Rust-side default). Expressed as its own mapped union so
* `params` narrows by `name`. */
export type MorphFilterValue = {
[K in keyof BevyMorphFilters]: {
/** Morph identity: a change (and only a change) triggers the morph. */
key: string | number;
name: K;
params?: {
[P in keyof BevyMorphFilters[K]]?: Animatable<BevyMorphFilters[K][P]>;
};
};
}[keyof BevyMorphFilters];Gradient
/** One gradient. `angle`/`start` are [`Angle`]s (bare number = degrees; `0` = to
* top, clockwise). Every numeric/color leaf accepts an `{ animated }` binding
* (degrees / px / raw `0..1` wire units; a binding parks that surface's
* `transition` channel). */
export type Gradient =
| {
type: "linear";
angle?: Animatable<Angle>;
stops: GradientStop[];
colorSpace?: ColorSpace;
}
| {
type: "radial";
position?: GradientPosition;
shape?: RadialShape;
stops: GradientStop[];
colorSpace?: ColorSpace;
}
| {
type: "conic";
start?: Animatable<Angle>;
position?: GradientPosition;
stops: AngularStop[];
colorSpace?: ColorSpace;
};BackgroundImage
/** A `backgroundImage` style value. `src` is an asset path (like an `<image>`'s
* `src`), or `{ texture }` naming a render target the app registered in
* `RenderTargets` — it binds late (transparent until registered; prefer
* fixed-resolution targets: a background never drives an auto target's size). */
export type BackgroundImage = {
/** Asset path, or `{ texture }` naming an app-registered texture
* (`RenderTargets::register`) — **static** content that binds late
* (transparent until registered). For live/continuously-updating content
* use a `<portal>` element instead; backgrounds don't participate in
* live-repaint tracking. */
src: string | { texture: string };
/** Tint multiplied with the texture; `opacity` fades it like a background
* color. Animatable via an `interpolateColor` binding — in the base style
* only (variant styles ignore bindings). */
tint?: Animatable<Color>;
/** Fit/repeat mode (default `"stretch"`). */
mode?: BackgroundImageMode;
/** Tile scale for the repeat modes, in logical px (`1` = the texture's own
* size at 1× DPI, on every display). Ignored — with a devtools warning —
* under `"stretch"`. An `{ animated }` wrapper decodes but does not drive
* the value yet. */
scale?: number;
};SystemCursor
/** The built-in system cursor keywords (winit's `SystemCursorIcon`, camelCase or CSS
* kebab-case). Used by the `cursor` style prop; a custom-cursor name (registered on
* the Rust side via `add_custom_cursor`) is any other string. */
export type SystemCursor =
| "default"
| "contextMenu"
| "help"
| "pointer"
| "progress"
| "wait"
| "cell"
| "crosshair"
| "text"
| "verticalText"
| "alias"
| "copy"
| "move"
| "noDrop"
| "notAllowed"
| "grab"
| "grabbing"
| "eResize"
| "nResize"
| "neResize"
| "nwResize"
| "sResize"
| "seResize"
| "swResize"
| "wResize"
| "ewResize"
| "nsResize"
| "neswResize"
| "nwseResize"
| "colResize"
| "rowResize"
| "allScroll"
| "zoomIn"
| "zoomOut";ScrollbarStyle
/** Configures a node's visible scrollbar (the object form of `style.scrollbar`).
* A bar appears per axis that is `overflow: scroll` **and** overflows, and
* auto-hides when its content fits. */
export interface ScrollbarStyle {
/** Styles the track (the groove behind the thumb). */
track?: ScrollbarPartStyle;
/** Styles the thumb (the draggable handle). */
thumb?: ScrollbarPartStyle;
/** Bar cross-axis size in logical px (default `12`). */
thickness?: number;
/** Minimum thumb length in logical px, so it stays grabbable on long content
* (default `24`). */
minThumbLength?: number;
/** `"gutter"` (default) reserves space so content shrinks and the bar sits in
* its own track; `"float"` reserves nothing and floats the bar over content. */
position?: "gutter" | "float";
/** Which edge the vertical bar sits on (default `"right"`). */
verticalSide?: "left" | "right";
/** Which edge the horizontal bar sits on (default `"bottom"`). */
horizontalSide?: "top" | "bottom";
}BevyTransform
/** A static 2D transform (`style.transform`). With `transition` a change
* eases instead of snapping. `translateX`/`translateY` are [`Length`]s (bare
* number = logical pixels, or a unit string like `"50%"`/`"10vw"`, resolved
* against the node's own size). `scale` is uniform; `scaleX`/`scaleY`
* override one axis. `rotate` is an [`Angle`] (bare number = degrees, e.g.
* `45`, or `"1.5rad"`). */
export interface BevyTransform {
translateX?: Animatable<Length>;
translateY?: Animatable<Length>;
scale?: Animatable<number>;
scaleX?: Animatable<number>;
scaleY?: Animatable<number>;
/** Bound values are **degrees**, like the static [`Angle`] form. */
rotate?: Animatable<Angle>;
}BevyTransform3d
/** A 3D perspective transform (`style.transform3d`), applied to the
* subtree's *rendered result* at composite time (group semantics, like
* `opacity`/`filter`). Its presence — even an empty `{}` — promotes the
* subtree to a composited layer; animating it never re-captures
* (composite-time cost, like translation). Picking, hover, and cursor follow
* the transformed visual. Field order is fixed: scale → rotateX → rotateY →
* rotateZ → translate, then the self `perspective` projection, all around
* `origin`.
*
* Units: translations and `perspective` are logical px; rotations are
* [`Angle`]s (bare number = degrees); `origin` is per-axis px-or-percent of
* the border box (default `"50%"`/`"50%"` = center). `translateZ` is only
* visible with `perspective` (positive = toward the viewer = magnify).
* Backfaces render mirrored and stay clickable.
*
* With `transition: { transform3d }` changes ease field-wise (perspective
* snaps when either endpoint is orthographic). Unsetting the whole field
* demotes the layer and **snaps** — keep an identity `{}` in the base style
* when removal should ease. Avoid hover-triggered transforms that move the
* element out from under the cursor (hover flips off → moves back →
* oscillates, as in CSS). */
export interface BevyTransform3d {
/** Focal distance in logical px (CSS `perspective(d)`); unset = orthographic. */
perspective?: Animatable<number>;
translateX?: Animatable<number>;
translateY?: Animatable<number>;
translateZ?: Animatable<number>;
rotateX?: Animatable<Angle>;
rotateY?: Animatable<Angle>;
rotateZ?: Animatable<Angle>;
scale?: Animatable<number>;
scaleX?: Animatable<number>;
scaleY?: Animatable<number>;
/** Pivot + vanishing point, relative to the border box (bound axes in px). */
origin?: { x: Animatable<Length>; y: Animatable<Length> };
}BevyTransition
/** Per-channel transition timing. Every channel is explicit — there is no
* fallback key; `transform` covers all transform channels together. */
export interface BevyTransition {
transform?: BevyTransitionSpec;
opacity?: BevyTransitionSpec;
backgroundColor?: BevyTransitionSpec;
/** Eases `backgroundGradient` between style states, whole-value. Structures
* must match STRICTLY (same kind, stop count, colorSpace, position, shape
* variant) — any mismatch snaps immediately with a devtools warning. Setting
* or unsetting the gradient always snaps: fade via a transparent-stops
* gradient in the base style instead. Stop colors ease in sRGB (the
* backgroundColor space); angles ease numerically (350 to 10 goes the long
* way, like CSS). */
backgroundGradient?: BevyTransitionSpec;
/** The `borderGradient` twin of `backgroundGradient`, independent of it. */
borderGradient?: BevyTransitionSpec;
/** Covers the size channels (`width`/`height`/`maxWidth`/`maxHeight`). These are
* layout properties, so easing one re-flows surrounding content — a real
* accordion. Needs an explicit pixel target (e.g. `maxHeight: open ? 300 : 0`);
* `auto`/unknown heights snap. Pair with `overflowY: "clip"`. */
size?: BevyTransitionSpec;
/** Eases `borderRadius` per corner between style states — every wire form
* (uniform, shorthand, per-corner object), so uniform-to-per-corner eases too.
* Same-unit corners interpolate; a corner that changes unit (or hits `auto`)
* snaps on its own. Unsetting the field eases to square corners. The radius
* is a layout property (it lives on the node), so like `size` every eased
* frame is a relayout. An `{ animated }` binding on `borderRadius` parks it. */
borderRadius?: BevyTransitionSpec;
/** Eases the scroll offset (`ScrollPosition`) of an `overflow: scroll` node
* toward its target — a controlled `scrollTop`/`scrollLeft` change or accumulated
* wheel input — instead of snapping (smooth scroll). Covers both axes. Direct
* scrollbar manipulation (thumb drag, track click) bypasses the ease and snaps.
* Don't also feed `onScroll` back into the same controlled axis, or the round-trip
* fights the ease (drive the target from buttons/state; read `onScroll` into
* separate state). */
scroll?: BevyTransitionSpec;
/** Eases the layer-based `filter` chain between style states: same-name chains
* interpolate their params; a chain extended/truncated at the end over built-in
* filters fades through identity values (hover-adds-blur fades in); anything
* else swaps wholesale at the midpoint. */
filter?: BevyTransitionSpec;
/** Eases the `backdropFilter` chain — the second, independent instance of the
* `filter` channel (same whole-value strategy and ease-to-empty snap: unsetting
* `backdropFilter` demotes and snaps, so keep an identity entry — e.g.
* `{ name: "blur", params: { radius: 0 } }` — when removal should ease). */
backdropFilter?: BevyTransitionSpec;
/** Eases the `transform3d` fields together (composite-time — animating never
* re-captures the layer). `perspective` snaps when either endpoint is
* orthographic; unsetting the whole `transform3d` style demotes and snaps —
* keep an identity `{}` in the base style when removal should ease. */
transform3d?: BevyTransitionSpec;
/** Times the `morphFilter` progress (the engine-owned 0→1 blend from the
* frozen old appearance to the live content on a `key` change). Unlike every
* other channel it has a BUILT-IN default (300ms ease-in-out) — a key change
* animates even with no `transition` at all; this entry overrides the
* timing. */
morphFilter?: BevyTransitionSpec;
/** Eases the node's LAID-OUT rect (position + size together) whenever layout
* moves or resizes it, whatever the cause — a sibling insert/remove/reorder,
* a parent resize, a re-wrap, a window resize (FLIP). The real layout still
* snaps (no relayout, no layer); the node glides from its old rect, children
* ride the translation (not the scale), picking follows. A size change scales
* only the node's OWN paint — children stay crisp at their final offsets —
* but nothing laid out AROUND the node eases (siblings snap to the final
* layout): a container whose size must re-flow its surroundings wants the
* real-layout `size` channel instead, with `layout` on its children. The
* first layout adopts silently (no enter animation;
* unmount can't animate) and `display: none` → shown grows in place. The
* node's own `size` channel or a `{ animated }` binding on a layout field
* owns its rect (the layout channel adopts, they compose); a rect moved every
* frame by anything else (an ancestor's `size` ease) lags, then catches up. */
layout?: BevyTransitionSpec;
/** Times a shared-element flight (see the `sharedTag` prop): when this node
* mounts as the incoming half of a tag pair, every channel seeded from the
* outgoing node — its rect (position via translation, size in measured px
* through real layout), background color, opacity, transforms, filters,
* gradients — eases with THIS one spec, overriding the per-channel specs
* for the flight only (ordinary later changes use their own). Required for
* the flight; explicit-only like every channel (no built-in default). */
sharedElement?: BevyTransitionSpec;
}FontSize
/** A font size: a bare number is logical pixels, or a unit string — `"24px"`,
* `"2vw"`/`"2vh"`/`"2vmin"`/`"2vmax"`, or `"1.5rem"` (relative to Bevy's `RemSize`,
* default 20px). CSS `em` is not supported (no Bevy equivalent). */
export type FontSize = number | string;FilterUse
/** One filter invocation in a chain: a registered filter name (e.g. `"blur"`,
* `"hueRotate"`) plus its parameters, typed per name via [`BevyFilters`].
* Every param is individually optional: an omitted param takes the filter's
* Rust-side default (for built-ins the shorthand-default convention: a
* *visible* effect — `{ name: "grayscale" }` is *full* grayscale like CSS
* `grayscale()`, and a bare `blur` is a visible 20px blur unlike CSS's 0 —
* and the field's `#[serde(default)]` for customs).
*
* Every param position is [`Animatable`]: `params: { radius: { animated:
* sv } }` binds it to a shared value (chain position = binding address, so
* reordering the chain keeps bindings attached). The wrapper's optional
* `seed` is the static value the resolver decodes in its place — size it
* for the animation's range when the param feeds a resolve-time derivation
* (a blur radius's capture outset). */
export type FilterUse = {
[K in keyof BevyFilters]: {
name: K;
params?: { [P in keyof BevyFilters[K]]?: Animatable<BevyFilters[K][P]> };
};
}[keyof BevyFilters];Angle
/** A CSS angle: a bare number is **degrees**, or a unit string (`"45deg"`,
* `"1.5rad"`, `"0.25turn"`, `"100grad"`). */
export type Angle = number | string;GradientStop
/** A linear/radial color stop. `position` places it along the gradient line
* (absent → auto-spaced); `hint` is the `0..1` interpolation midpoint to the
* next stop (default `0.5`). All three leaves are animatable — `color` via an
* `interpolateColor` binding, `position` in logical px, `hint` raw `0..1`
* (base-style only). */
export type GradientStop = {
color: Animatable<Color>;
position?: Animatable<Length>;
hint?: Animatable<number>;
};ColorSpace
/** Color space a gradient interpolates in (default `"oklab"`). */
export type ColorSpace =
| "oklab"
| "oklch"
| "oklchLong"
| "srgb"
| "linearRgb"
| "hsl"
| "hslLong"
| "hsv"
| "hsvLong";GradientPosition
/** Named center anchor for a radial/conic gradient (default `"center"`). */
export type GradientPosition =
| "center"
| "top"
| "bottom"
| "left"
| "right"
| "topLeft"
| "topRight"
| "bottomLeft"
| "bottomRight";RadialShape
/** Size/shape of a radial gradient (default `"closestCorner"`). The explicit
* radii are animatable (a bound radius animates in logical px; base-style
* only). */
export type RadialShape =
| "closestSide"
| "farthestSide"
| "closestCorner"
| "farthestCorner"
| { circle: Animatable<Length> }
| { ellipse: [Animatable<Length>, Animatable<Length>] };AngularStop
/** A conic color stop. `angle` is an [`Angle`] (bare number = degrees; absent →
* auto-spaced). All three leaves are animatable — `color` via an
* `interpolateColor` binding, `angle` in degrees, `hint` raw `0..1`
* (base-style only). */
export type AngularStop = {
color: Animatable<Color>;
angle?: Animatable<Angle>;
hint?: Animatable<number>;
};BackgroundImageMode
/** How a `backgroundImage` fits its node: `"stretch"` (default — fill the box
* exactly, aspect ignored) or the repeat modes (tile at the texture's logical
* size × `scale`). Unlike an `<image>`'s [`ImageMode`] there is no `"auto"` —
* a background never affects layout. */
export type BackgroundImageMode = "stretch" | "repeat" | "repeatX" | "repeatY";ScrollbarPartStyle
/** Styling for one scrollbar part (the `track` groove or the draggable `thumb`),
* with optional interaction-state overlays. `pressed` wins over `hover`, which
* overlays the base. (Scrollbars take no keyboard focus, so there is no `focused`
* state.) */
export interface ScrollbarPartStyle extends ScrollbarPartVisual {
/** Overlaid while the pointer is over this part. */
hover?: ScrollbarPartVisual;
/** Overlaid while the bar is being dragged (wins over `hover`). */
pressed?: ScrollbarPartVisual;
}BevyTransitionSpec
/** Timing for one transition channel: a timing curve (default) or, if `stiffness`
* or `damping` is given, a spring. `duration`/`delay` are [`Time`]s — a bare
* number is **milliseconds**, or a unit string (`"0.2s"`). */
export type BevyTransitionSpec = {
/** Timing duration (default `300` ms). Ignored for a spring. */
duration?: Time;
easing?: "linear" | "easeIn" | "easeOut" | "easeInOut";
/** Hold this long before easing (default `0`). */
delay?: Time;
/** Spring stiffness; its presence (with/without `damping`) selects a spring. */
stiffness?: number;
damping?: number;
mass?: number;
};ScrollbarPartVisual
/** The paint properties of one scrollbar part in one interaction state.
* Deliberately **not** a full [`BevyStyle`]: the thumb is a headless Bevy widget
* with no layout node, so only these apply. Size/placement come from the parent
* [`ScrollbarStyle`] (`thickness`) — a part never sets its own width/height. */
export interface ScrollbarPartVisual {
/** Fill color (any CSS [`Color`]). */
backgroundColor?: Color;
/** Border color: one CSS [`Color`] for all sides, or a per-side object. */
borderColor?:
| Color
| { top?: Color; right?: Color; bottom?: Color; left?: Color };
/** Corner radii (same forms as any [`Rect`]). */
borderRadius?: Rect;
/** Border thickness (same forms as any [`Rect`]). */
border?: Rect;
}Time
/** A CSS time/duration: a bare number is **milliseconds**, or a unit string
* (`"200ms"`, `"0.2s"`). */
export type Time = number | string;
bevy-react