Skip to main content
Kaleidoscope

ProximityReveal

Reveal content when the user's mouse approaches a target element

Introduction

Use the useProximityReveal hook to detect mouse proximity and the ProximityReveal component to animate content in and out. Attach targetRef to the element you want to detect proximity to.

Editable example
() => {
  const { isRevealed, targetRef } = useProximityReveal({ distance: 64 });

  return (
    <Card>
      <div ref={targetRef} style={{ padding: "var(--space-m)", minHeight: 80 }}>
        <Text>Hover near this card</Text>
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </Card>
  );
}

Edge detection

Restrict proximity detection to specific edges using the edges prop. Only the specified edge will trigger the reveal.

Editable example
() => {
  const { isRevealed, targetRef } = useProximityReveal({ distance: 48, edges: { top: true } });

  return (
    <Card>
      <div ref={targetRef} style={{ padding: "var(--space-m)", minHeight: 100 }}>
        <Text>Only the top edge triggers the reveal</Text>
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </Card>
  );
}

Hide delay

Use hideDelay to keep content visible for a short time after the mouse leaves the proximity zone. This prevents flickering when the cursor briefly moves away.

Editable example
() => {
  const { isRevealed, targetRef } = useProximityReveal({ distance: 48, hideDelay: 500 });

  return (
    <Card>
      <div ref={targetRef} style={{ padding: "var(--space-m)", minHeight: 80 }}>
        <Text>Content stays visible for 500ms after leaving</Text>
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </Card>
  );
}

Force visible

Use forceVisible to override proximity detection and always show content. Useful for states like edit mode where the UI should remain visible regardless of mouse position.

Editable example
() => {
  const [editMode, setEditMode] = React.useState(false);
  const { isRevealed, targetRef } = useProximityReveal({ distance: 48, forceVisible: editMode });

  return (
    <Card>
      <div ref={targetRef} style={{ padding: "var(--space-m)", minHeight: 80 }}>
        <Toggle label="Edit mode" value={editMode} onChange={setEditMode} />
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </Card>
  );
}

Custom proximity

Pass an isNear callback for custom proximity shapes. This example uses a circular zone around the center of the target element.

Editable example
() => {
  const radius = 120;

  const ringWidth = 32;

  const isNear = React.useCallback((mouse, rect) => {
    const cx = rect.left + rect.width / 2;
    const cy = rect.top + rect.height / 2;
    const dx = mouse.x - cx;
    const dy = mouse.y - cy;
    const dist = Math.sqrt(dx * dx + dy * dy);
    return dist <= radius && dist >= radius - ringWidth;
  }, []);

  const { isRevealed, targetRef } = useProximityReveal({ isNear });

  return (
    <div style={{ position: "relative", width: radius * 2, height: radius * 2 }}>
      <div
        style={{
          position: "absolute",
          inset: 0,
          borderRadius: "50%",
          border: "32px solid rgba(59, 130, 246, 0.08)",
          boxSizing: "border-box",
        }}
      />
      <div
        ref={targetRef}
        style={{
          position: "absolute",
          top: "50%",
          left: "50%",
          transform: "translate(-50%, -50%)",
          width: 100,
          height: 100,
          border: "2px dashed rgba(100, 116, 139, 0.4)",
          borderRadius: 4,
          display: "flex",
          alignItems: "center",
          justifyContent: "center",
        }}
      >
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </div>
  );
}

Element tracking

Set trackingMode: "element" to scope mouse tracking to the target element instead of the entire document. Spread onMouseMove and onMouseLeave onto the element. This is useful when you only want to detect proximity while the cursor is already over the element.

Editable example
() => {
  const { isRevealed, targetRef, onMouseMove, onMouseLeave } = useProximityReveal({
    distance: 32,
    edges: { top: true },
    trackingMode: "element",
  });

  return (
    <Card>
      <div
        ref={targetRef}
        onMouseMove={onMouseMove}
        onMouseLeave={onMouseLeave}
        style={{ padding: "var(--space-m)", minHeight: 100 }}
      >
        <Text>Mouse events are scoped to this element</Text>
        <ProximityReveal isRevealed={isRevealed}>
          <Text>Boo! 👻</Text>
        </ProximityReveal>
      </div>
    </Card>
  );
}

Accessibility

ProximityReveal keeps children mounted in the DOM at all times, hiding them visually with opacity: 0 and pointer-events: none. This means screen readers can always reach the content.

Keyboard and screen reader support is baked into the CSS recipe:

  • Focus reveal — when any child receives keyboard focus, the container automatically becomes visible via :has(:focus).
  • Expanded reveal — when a child has aria-expanded="true" (e.g. an open dropdown), the container stays visible via :has([aria-expanded='true']).

No additional work is needed from consumers — accessible reveal behaviour works out of the box.