react ~4 KB 0 deps v0.1.0 ↗ GitHub ↗

@mshafiqyajid/react-spotlight

Full-screen overlay with an SVG cutout that highlights any element on the page. Supports single-step highlights, multi-step guided tours with built-in prev/next navigation, pulse animation, backdrop blur, eight placement options, focus trap, Escape to close, scroll-into-view, and live tracking via ResizeObserver. Controlled and uncontrolled. Zero dependencies.

Playground #

Feature card
Click "Start tour" to highlight this card with a spotlight overlay.
Props
TSX
import { SpotlightStyled } from "@mshafiqyajid/react-spotlight/styled";
import "@mshafiqyajid/react-spotlight/styles.css";

<SpotlightStyled />

Install #

npm install @mshafiqyajid/react-spotlight

Quick start #

Pass a ref (or a CSS selector string) as target to identify the element the overlay should cut out around. Render your tooltip content as children.

import { useRef, useState } from "react";
import { SpotlightStyled } from "@mshafiqyajid/react-spotlight/styled";
import "@mshafiqyajid/react-spotlight/styles.css";

function App() {
  const [open, setOpen] = useState(false);
  const btnRef = useRef<HTMLButtonElement>(null);

  return (
    <>
      <button ref={btnRef} onClick={() => setOpen(true)}>
        Show spotlight
      </button>

      <SpotlightStyled
        target={btnRef}
        open={open}
        onOpenChange={setOpen}
        placement="bottom"
      >
        <div>
          <strong>Welcome!</strong>
          <p>This button starts the tour.</p>
          <button onClick={() => setOpen(false)}>Got it</button>
        </div>
      </SpotlightStyled>
    </>
  );
}

Multi-step tour #

Pass a steps array to turn the spotlight into a guided tour. Each step specifies its own target and optionally overrides padding, radius, placement, and content. Prev / Next navigation and a step counter are rendered automatically inside the content panel. The Done button appears on the last step and closes the tour.

Acme App
🔍 Search…
Dashboard
Projects
Team
Reports
Revenue
$24k
+12%
Users
1,842
+5%
Orders
384
+8%
Acme Inc © 2026
import { useRef, useState } from "react";
import { SpotlightStyled } from "@mshafiqyajid/react-spotlight/styled";
import "@mshafiqyajid/react-spotlight/styles.css";
import type { SpotlightStep } from "@mshafiqyajid/react-spotlight";

function GuidedTour() {
  const [open, setOpen] = useState(false);
  const navRef    = useRef<HTMLElement>(null);
  const searchRef = useRef<HTMLElement>(null);
  const actionRef = useRef<HTMLElement>(null);

  const steps: SpotlightStep[] = [
    {
      target: navRef,
      placement: "right",
      content: <p>Use the sidebar to navigate between sections.</p>,
    },
    {
      target: searchRef,
      padding: 8,
      content: <p>Search anything from here.</p>,
    },
    {
      target: actionRef,
      placement: "bottom",
      content: <p>Create a new item with this button.</p>,
    },
  ];

  return (
    <>
      {/* ... your UI elements with the refs attached ... */}

      <button onClick={() => setOpen(true)}>Start tour</button>

      {/* SpotlightStyled renders prev/next/done nav automatically */}
      <SpotlightStyled
        target={navRef}
        open={open}
        onOpenChange={setOpen}
        steps={steps}
        pulse
      />
    </>
  );
}

Control the active step yourself with step + onStepChange:

const [currentStep, setCurrentStep] = useState(0);

<SpotlightStyled
  target={ref}
  open={open}
  onOpenChange={setOpen}
  steps={steps}
  step={currentStep}
  onStepChange={setCurrentStep}
/>

Pulse animation #

Add pulse to draw a ring that expands outward from the cutout, directing the user's attention. The animation is suppressed automatically when the user prefers reduced motion.

<SpotlightStyled target={ref} open={open} onOpenChange={setOpen} pulse>
  <p>Look here!</p>
</SpotlightStyled>

Backdrop blur #

Set backdropBlur (px) to apply backdrop-filter: blur() behind the overlay. Reduce overlayColor opacity alongside it for a frosted-glass effect.

Frosted glass target
<SpotlightStyled
  target={ref}
  open={open}
  onOpenChange={setOpen}
  backdropBlur={8}
  overlayColor="rgba(0,0,0,0.4)"
>
  <p>Frosted glass overlay</p>
</SpotlightStyled>

Placement #

Eight positions control where the content panel appears relative to the target. In multi-step mode each step can override this independently.

ValuePanel position
"bottom" (default)Centered below the target
"top"Centered above the target
"left" / "right"Vertically centered beside the target
"bottom-start" / "bottom-end"Below, aligned to start or end edge
"top-start" / "top-end"Above, aligned to start or end edge

CSS selector target #

Pass a CSS selector string when you don't own the target element's ref — useful for highlighting third-party components or dynamically inserted DOM nodes.

<SpotlightStyled
  target="#my-nav-button"
  open={open}
  onOpenChange={setOpen}
>
  <p>This is the navigation button.</p>
</SpotlightStyled>

Trigger slot #

The trigger prop renders a wrapper that toggles the spotlight on click — no state management required for simple use-cases.

import { useRef } from "react";
import { SpotlightStyled } from "@mshafiqyajid/react-spotlight/styled";
import "@mshafiqyajid/react-spotlight/styles.css";

function App() {
  const featureRef = useRef<HTMLDivElement>(null);

  return (
    <>
      <div ref={featureRef}>Feature area</div>

      <SpotlightStyled
        target={featureRef}
        trigger={<button>Highlight</button>}
      >
        <p>Now you see me.</p>
      </SpotlightStyled>
    </>
  );
}

Accessibility #

The overlay carries role="dialog", aria-modal="true", and aria-label="Spotlight". Focus is trapped inside while open — Tab / Shift+Tab cycle through focusable children. On close, focus returns to the previously focused element. Escape closes by default (set closeOnEscape={false} to opt out). Multi-step prev/next/done buttons carry descriptive aria-label values and the disabled prev button on step 0 is excluded from the focus cycle.

{/* Escape and overlay click disabled — only explicit action closes */}
<SpotlightStyled
  target={ref}
  open={open}
  onOpenChange={setOpen}
  closeOnEscape={false}
  closeOnOverlayClick={false}
>
  <div>
    <p>You must click the button to continue.</p>
    <button onClick={() => setOpen(false)}>Next</button>
  </div>
</SpotlightStyled>

Headless #

Use useSpotlight directly when you need full control over the overlay markup — for example to use a custom SVG mask or integrate with an existing modal system.

import { useSpotlight } from "@mshafiqyajid/react-spotlight";
import { createPortal } from "react-dom";
import { useRef } from "react";

function MySpotlight() {
  const targetRef = useRef<HTMLButtonElement>(null);
  const { isOpen, open, close, targetRect, overlayProps, spotlightProps, padding, radius } =
    useSpotlight({ target: targetRef, placement: "bottom" });

  return (
    <>
      <button ref={targetRef} onClick={open}>Highlight me</button>

      {isOpen && createPortal(
        <div {...overlayProps}>
          {targetRect && (
            <svg style={{ position: "fixed", inset: 0, width: "100%", height: "100%", pointerEvents: "none" }}>
              <defs>
                <mask id="my-mask">
                  <rect width="100%" height="100%" fill="white" />
                  <rect
                    x={targetRect.x - padding}  y={targetRect.y - padding}
                    width={targetRect.width + padding * 2}
                    height={targetRect.height + padding * 2}
                    rx={radius}
                    fill="black"
                  />
                </mask>
              </defs>
              <rect width="100%" height="100%" fill="rgba(0,0,0,0.6)" mask="url(#my-mask)" />
            </svg>
          )}
          <div {...spotlightProps}>
            <p>Custom content</p>
            <button onClick={close}>Close</button>
          </div>
        </div>,
        document.body
      )}
    </>
  );
}

SpotlightStep type #

interface SpotlightStep {
  /** Ref or CSS selector of the element to highlight */
  target: React.RefObject<Element | null> | string;
  /** Padding around the target in px — overrides root padding */
  padding?: number;
  /** Cutout border-radius in px — overrides root radius */
  radius?: number;
  /** Content panel placement — overrides root placement */
  placement?: SpotlightPlacement;
  /** Content rendered in the panel for this step */
  content?: React.ReactNode;
}

API #

PropTypeDefaultDescription
targetRefObject<Element> | stringElement to highlight. Required.
openbooleanControlled open state.
defaultOpenbooleanfalseUncontrolled initial open state.
onOpenChange(open: boolean) => voidCalled when open state changes.
paddingnumber8Extra space around the target cutout in px.
radiusnumber8Border radius of the cutout in px.
placementSpotlightPlacement"bottom"Content panel placement relative to target.
overlayColorstring"rgba(0,0,0,0.6)"Overlay fill color.
backdropBlurnumber0Backdrop blur in px — applies backdrop-filter: blur().
pulsebooleanfalseShow a pulsing ring around the cutout border.
closeOnOverlayClickbooleantrueClose when clicking the overlay background.
closeOnEscapebooleantrueClose on Escape key.
scrollIntoViewbooleantrueScroll target into view on open and on step change.
childrenReactNodeTooltip content (ignored when steps is set — use step content instead).
triggerReactNodeRenders a toggle wrapper — clicking it opens/closes the spotlight.
stepsSpotlightStep[]Multi-step tour steps with built-in nav.
stepnumberControlled active step index.
defaultStepnumber0Uncontrolled initial step index.
onStepChange(step: number) => voidCalled when the active step changes.
classNamestringClass added to the overlay root element.
styleCSSPropertiesInline style on the overlay root element.

useSpotlight return values #

NameTypeDescription
isOpenbooleanWhether the overlay is open.
open() => voidOpen the spotlight.
close() => voidClose the spotlight.
toggle() => voidToggle open / closed.
targetRectTargetRect | nullLive measured bounding rect of the active target: { x, y, width, height }.
paddingnumberResolved padding for the current step.
radiusnumberResolved cutout radius for the current step.
overlayPropsobjectSpread onto the overlay element — role, aria-modal, aria-label, data-open, onClick, onKeyDown, style, tabIndex.
spotlightPropsobjectSpread onto the content container — computed placement style.
stepnumberActive step index (0 in single-step mode).
totalStepsnumberTotal steps (0 when steps is not set).
nextStep() => voidAdvance to next step — clamped at last step.
prevStep() => voidGo back one step — clamped at first step.
goToStep(n: number) => voidJump to any step index.

CSS variables #

:root {
  --rspot-overlay-color:   rgba(0, 0, 0, 0.6);
  --rspot-backdrop-blur:   0px;
  --rspot-content-bg:      #ffffff;
  --rspot-content-fg:      #18181b;
  --rspot-content-border:  #e4e4e7;
  --rspot-content-shadow:  0 8px 32px rgba(0,0,0,0.18), 0 2px 8px rgba(0,0,0,0.1);
  --rspot-content-radius:  12px;
  --rspot-duration-in:     200ms;
  --rspot-duration-out:    150ms;
  --rspot-ease:            cubic-bezier(0.32, 0.72, 0, 1);
}
Edit this page on GitHub