Tween value-shape contract
The <tween> node and the @roku-sdk/components Tween wrapper animate a field on a target node. Targets and settle candidates take the shape of the animated value, determined by the field and axis. field defaults to "translation"; axis defaults to "all". A target write starts an animation; duration={0} snaps immediately, even when that destination is already armed.
Targets and stops
| Target field type | axis | Components animated | Target prop | proposedStops type | Example fields |
|---|---|---|---|---|---|
| float | ignored; omit it | 1 | targetValue: number | number[] | opacity, width, height, rotation |
| Vector2D | "x" | X; Y held at preserveOther | targetValue: number | number[] of X coordinates | translation |
| Vector2D | "y" | Y; X held at preserveOther | targetValue: number | number[] of Y coordinates | translation |
| Vector2D | "all" | X and Y | targetVector: [x, y] | [number, number][] | translation, scale, scaleRotateCenter |
preserveOther defaults to 0. A scalar targetValue on a Vector2D field with axis="all" is still accepted and expands to [v, v], for example a uniform scale. This target shorthand does not apply to stops. An explicit targetVector is used verbatim; use targetValue for single-axis motion.
The compatibility implementation recognises the Vector2D fields listed above by name. Custom scalar fields use the float path. For a custom Vector2D field, supply an explicit targetVector and keep that target shape for the lifetime of the tween; custom Vector2D single-axis scalar targets are unsupported by the compatibility implementation. Do not alternate scalar and vector target props to select a value type. Rect2D animation is reserved for future support.
Wrong-shaped stops
Entries are checked individually. A settle logs one skip count when any entries are rejected; valid entries still compete. For known fields, the FlexGrid wrapper also warns once per distinct shape problem in a development build.
| Situation | Behaviour | FlexGrid development warning |
|---|---|---|
Scalar stop on a Vector2D field with axis="all", including a legacy flat [X, Y] | Skip each scalar; never lift to [s, s] | Yes |
Array stop on a Vector2D field with axis="x" or "y" | Skip each array, including a pair | Yes |
Array of length other than 2 on a Vector2D field with axis="all" | Skip the entry | Yes |
Mixed list, such as [400, [250, 200], 300] with axis="all" | Use the valid pair; report its original index, 1 | Yes |
| No usable stop remains | No settle; continue to the tween's existing target without an onAnimationStop settle notification | No additional warning |
axis specified on a float field | Ignore axis | Yes |
| Array stop on a float field | Skip each array; never project it onto one coordinate | Yes |
Scalar targetValue on a Vector2D field with axis="all" | Expand the target to [v, v] | No |
| Custom field | Use the custom-field rules above; no development shape check | No |
Stops must contain numeric values; non-numeric tuple members are not a supported input.
Settling a vector
Each candidate is a whole [x, y] point. There is no dominant-axis selection of the destination.
- With one usable candidate, that point is the destination without a direction filter.
- With several candidates, a point must be strictly ahead on every moving component owned by
axis. The nearest eligible point by Euclidean distance over the owned components wins, and all owned components are applied together. - For a stationary span, the absolute nearest candidate wins without a direction filter.
- If valid candidates exist but none is ahead, the original destination is retained and index
0is reported (it does not identify a selected candidate). This is distinct from having no usable candidates.
The intrinsic's onAnimationStop receives [index, position, stopTime], with stopTime a [second, millisecond] pair. index is the position in proposedStops as written, before filtering. position is a scalar: the owned coordinate for "x" or "y", or the coordinate that travels further for "all" (Y only when its distance is strictly greater). Read proposedStops[index] for the full point when a candidate was selected.
Why scalar stops are not expanded
Expanding a legacy flat [X, Y] list into [X, X] and [Y, Y] invents two diagonal destinations instead of one point. A horizontal release can then settle onto another row. Requiring stops to match the animated value prevents that ambiguity without silently discarding one coordinate. Target shorthand remains useful for uniform scale, but a list of destinations cannot safely use the same shorthand.
Import Tween and TweenProps from @roku-sdk/components. The wrapper accepts targetNode as a node or getter. targetRef remains a deprecated alias for one minor release; targetNode takes precedence. The flex-grid exports forward to components for that same transition period.
If you render the <tween> intrinsic directly, clear targetNode before removing it. The compat shim releases its Animation when a span ends after the node was removed, but a tween removed while idle or paused never reaches that point, so its Animation stays in the parent. The Tween wrapper clears targetNode for you on cleanup.
Updated about 3 hours ago