Skip to content

feat: Support :hover and :active on SVG elements#9879

Draft
MatiPl01 wants to merge 6 commits into
mainfrom
@matipl01/css-hover-svg-pointer
Draft

feat: Support :hover and :active on SVG elements#9879
MatiPl01 wants to merge 6 commits into
mainfrom
@matipl01/css-hover-svg-pointer

Conversation

@MatiPl01

@MatiPl01 MatiPl01 commented Jul 5, 2026

Copy link
Copy Markdown
Member

Adds pointer :hover for react-native-svg, and makes overlapping SVG shapes resolve :hover/:active to the front-most one (they share one SvgView canvas, so the topmost shape wins). Also fixes touch :active on SVG on Android, where the per-view listener could miss ACTION_DOWN on a real finger; the press now comes from the window touch observer instead.

Demo

:hover on overlapping circles (the front one wins in the overlap):

svg-hover-showcase.mp4

:active on an SVG circle (press darkens it, dragging past the slop reverts):

svg-active-showcase.mp4

Same taps, base vs this PR - on the base the touch is missed and nothing reacts:

svg-hover-before-after.mp4
Example source (drop-in EmptyExample.tsx)
import React, { type ComponentType } from 'react';
import { StyleSheet, Text, View } from 'react-native';
import Animated from 'react-native-reanimated';
/* eslint-disable-next-line @typescript-eslint/ban-ts-comment */
// @ts-ignore RNSVG doesn't export types for web
import { Circle, Svg } from 'react-native-svg';

const AnimatedCircle = Animated.createAnimatedComponent(
  Circle
) as ComponentType<Record<string, unknown>>;

export default function EmptyExample() {
  return (
    <View style={styles.container}>
      <Text style={styles.label}>
        :hover on overlapping SVG circles - tap the lens where they overlap and
        only the front (blue) circle changes, since RNSVG hit-tests the top-most
        shape
      </Text>
      <Svg height={180} width={240}>
        {/* back circle - drawn first, so it sits underneath */}
        <AnimatedCircle
          cx={95}
          cy={90}
          r={65}
          fill="#fca5a5"
          onStartShouldSetResponder={() => true}
          style={{
            fill: { default: '#fca5a5', ':hover': '#dc2626' },
            transitionDuration: '150ms',
          }}
        />
        {/* front circle - drawn last, so it sits on top */}
        <AnimatedCircle
          cx={145}
          cy={90}
          r={65}
          fill="#93c5fd"
          onStartShouldSetResponder={() => true}
          style={{
            fill: { default: '#93c5fd', ':hover': '#2563eb' },
            transitionDuration: '150ms',
          }}
        />
      </Svg>

      <Text style={styles.label}>
        :active on an SVG element - press and hold to darken, drag past the slop
        to revert
      </Text>
      <Svg height={80} width={80}>
        <AnimatedCircle
          cx={40}
          cy={40}
          r={36}
          fill="#3b82f6"
          onStartShouldSetResponder={() => true}
          style={{
            fill: { default: '#3b82f6', ':active': '#1e3a8a' },
            transitionDuration: '200ms',
          }}
        />
      </Svg>
    </View>
  );
}

const styles = StyleSheet.create({
  container: {
    flex: 1,
    alignItems: 'center',
    justifyContent: 'center',
    gap: 32,
  },
  label: {
    fontSize: 14,
    color: '#334155',
    textAlign: 'center',
    paddingHorizontal: 24,
  },
});

@MatiPl01 MatiPl01 self-assigned this Jul 5, 2026
@MatiPl01 MatiPl01 changed the title feat: Support pointer :hover on SVG elements feat: Support :hover and :active on SVG elements Jul 7, 2026
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-modal-clear branch from 3342ada to df014ea Compare July 8, 2026 11:24
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-svg-pointer branch from 4d5bfb2 to 81e9003 Compare July 8, 2026 11:24
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-modal-clear branch from df014ea to a5819d9 Compare July 8, 2026 13:35
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-svg-pointer branch from 81e9003 to e37cbc6 Compare July 8, 2026 13:35
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-modal-clear branch from a5819d9 to e222b3e Compare July 8, 2026 22:10
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-svg-pointer branch from e37cbc6 to e80db20 Compare July 8, 2026 22:10
Base automatically changed from @matipl01/css-hover-modal-clear to main July 9, 2026 10:51
MatiPl01 added 6 commits July 9, 2026 12:57
Route the pointer (mouse/stylus) hover listener onto the touch host so svg
elements, virtual children drawn on a shared SvgView canvas, receive hover at
all. Gate the pointer reconcile to real enter/leave edges and beyond-slop moves
so the canvas-wide hover-move flood can neither clobber a touch-set sticky hover
nor flicker against the RNSVG re-render hit-test quirk.

Fold the focus and press selector attaches, track each host's gesture in one
map, and trim the coordinator and manager comments.
The :active/:active-deepest set was seeded from a per-view OnTouchListener on
the touch host. A compound host such as an SvgView intermittently never
delivers ACTION_DOWN to that listener on a real finger, so :active mostly never
set. Drive the whole press from the per-window observer instead: it sees every
event and hit-tests the down geometrically via reactTagForTouch, which does not
depend on touch delivery. The per-view listener stays as the pre-0.86 /
unobserved-window fallback.
@MatiPl01 MatiPl01 force-pushed the @matipl01/css-hover-svg-pointer branch from e80db20 to 5fe485b Compare July 9, 2026 11:01
@MatiPl01 MatiPl01 changed the title feat: Support :hover and :active on SVG elements feat: Support :hover and :active` on SVG elements Jul 9, 2026
@MatiPl01 MatiPl01 changed the title feat: Support :hover and :active` on SVG elements feat: Support :hover and :active on SVG elements Jul 9, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant