Skip to content

feat(reanimated): css transitions, focus pseudo selectors and outlines - #123

Merged
RuudBurger merged 5 commits into
mainfrom
ruud/reanimated-pseudo-selectors
Sep 8, 2026
Merged

feat(reanimated): css transitions, focus pseudo selectors and outlines#123
RuudBurger merged 5 commits into
mainfrom
ruud/reanimated-pseudo-selectors

Conversation

@RuudBurger

@RuudBurger RuudBurger commented Sep 8, 2026

Copy link
Copy Markdown
Member

Reanimated 4 lets you drive focus styling from the style object itself, with css transitions and pseudo selectors: :focus on the focusable, :focus-within on an ancestor. That means a focus change doesn't wait for a React render, which matters on a TV. Lightning supported none of it, so apps had to keep a React-state fallback. This adds the css side to plugin-reanimated, plus outlines, so the focus ring can work the same way.

A CSSStyle gets split the way reanimated splits it: the transition props become a Lightning transition on the node (opacity -> alpha, backgroundColor -> color, a transform expanded onto the axes it actually uses, border/outline colour -> shaderProps), and the per-property default / :focus / :focus-within values are swapped straight onto the node as focus moves. Resting values of pseudo props are held back from the rendered style and pushed imperatively, otherwise a re-render clobbers an active state. :focus-within can't be read off the focus path (a plain wrapper View never appears in it), so one tracker per FocusManager walks up from the focused leaf on focusPathChanged and only notifies the subscribers whose state changed. Outlines turned out to need no extra node: the border shader already draws outside the node with align: 'outside' and gap, so outlineWidth / outlineColor / outlineOffset map onto the element's own shader.

Verified on the example app's new Pseudo Selectors page: the settled node values match the targets (:focus-within shift on the column, scale and colour on the card, border-align: outside with a 2px gap) and the ring cross-fades instead of snapping.

Caveats: :hover, :active and :active-deepest are ignored with a dev warning (no pointer, and press state doesn't live on the element), and so are css animations, which I haven't built yet. A border and an outline can't share the one shader a node gets, so the border wins and dev says so. Also in here: a partial style push used to resolve its shader from the payload alone, so pushing a lone borderColor or outlineColor rebuilt the shader without its width; it now resolves against the merged style. And the example app didn't boot on main (reanimated pulls react-native-is-edge-to-edge, whose module entry is CJS), so there's a small stub for that :hide:

@RuudBurger
RuudBurger marked this pull request as ready for review September 8, 2026 14:58
@RuudBurger
RuudBurger requested a review from DouweBos September 8, 2026 14:58
@RuudBurger
RuudBurger merged commit a70e336 into main Sep 8, 2026
2 checks passed
@RuudBurger
RuudBurger deleted the ruud/reanimated-pseudo-selectors branch September 8, 2026 15:02
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants