How to Create a Spotlight Card Hover Effect with Tailwind CSS
Tags: tailwindcss, nextjs, vue, html
Published: 2023-06-13 08:38:46

####**[Live Demo](https://cruip-tutorials.vercel.app/spotlight-effect/) / [Download](https://github.com/cruip/cruip-tutorials/blob/main/spotlight-effect/index.html)**

--

Welcome to this tutorial where we’ll take you on a journey to create an eye-catching **spotlight card hover effect** using Tailwind CSS. If you’re new to this trending effect, we recommend checking out the [live demo](https://cruip.com/demos/stellar/) or our [Dark Next.js landing page template](https://cruip.com/stellar/) called **Stellar**.

To begin, we’ll create the spotlight card using pure HTML and vanilla JavaScript. After that, we’ll take it a step further by showing you how to create a reusable component for Next.js and Vue.

Quick navigation

- [HTML Version](#HTML Version)
- [Nextjs Version](#Nextjs Version)
- [Vue Version](#Vue Version)

Let’s get started!

* * *

##Create the spotlight effect with HTML and vanilla JavaScript<a name="HTML Version"></a>

As we usually do in our tutorials, we will start by creating a basic HTML document that will contain the structure of our animation. Then we will write the JavaScript code in an external JS file.

```html
  <!DOCTYPE html>
  <html lang="en">
  
  <head>
      <meta charset="utf-8">
      <title>Spotlight Effect</title>
      <meta name="viewport" content="width=device-width,initial-scale=1">
      <link rel="preconnect" href="https://fonts.googleapis.com">
      <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
      <link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&display=swap" rel="stylesheet">
      <script src="https://cdn.tailwindcss.com"></script>
      <script>
          tailwind.config = {
              theme: {
                  extend: {
                      fontFamily: {
                          inter: ['Inter', 'sans-serif'],
                      },
                  },
              },
          };
      </script>
  </head>
  
  <body class="relative font-inter antialiased">
  
      <main class="relative min-h-screen flex flex-col justify-center bg-slate-900 overflow-hidden">
          <div class="w-full max-w-6xl mx-auto px-4 md:px-6 py-24">
      
              <!-- Cards container -->
              <div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
          
                  <!-- Card 1 -->
                  <!-- Card 2 -->
                  <!-- Card 3 -->
          
              </div>
              <!-- End: Cards container -->
      
          </div>
      </main>
  
      <script src="./spotlight-effect.js"></script>
  </body>
  
  </html>
```

We’ve created a very simple structure, with a container that will hold our cards. Now let’s start with coding one card, and then we’ll duplicate it to create the other two.

```html
  <div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
  
      <!-- 1. Before pseudo element -->
  
      <!-- 2. Card content -->
      <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
          <!-- Radial gradient -->
          <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
              <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
          </div>
          <div class="flex flex-col h-full items-center text-center">
              <!-- Image -->
              <div class="relative inline-flex">
                  <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
                  <img class="inline-flex" src="./card-01.png" width="200" height="200" alt="Card 01" />
              </div>
              <!-- Text -->
              <div class="grow mb-5">
                  <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
                  <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
              </div>
              <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                  <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
                      <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
                  </svg>
                  <span>Connect</span>
              </a>
          </div>
      </div>
  
      <!-- 3. After pseudo element -->
  
  </div>
```

 We’ve added a lot of classes to the card, but don’t worry, we’ll go through them one by one.

### Add a card border

In our first div, we’ve added a `bg-slate-800` to define a dark background color. Then, we’ve added a `p-px` class to add a 1px padding to the card, and the card content has the same background color of the page. Doing that, we’re creating a **fake 1px border** around the card.

The reason why we’re not using [conventional CSS borders](https://www.w3schools.com/css/css_border.asp) is that they won’t allow us to create the effect we want to achieve.

Now, let’s move on with the `before` and `after` pseudo elements. We will use the `before` and `after` prefixes provided by Tailwind CSS to create them, and define their style.

The `before` pseudo element will be used to lighten the borders of our cards on mouse hover, while the `after` pseudo element will be used for the spotligh effect above the card.

### Use the before pseudo element for the card border

Let’s see how it works the before pseudo element. It’s a layer placed below the card content, and it’s absolutely positioned to cover the entire card. It’s visible for 1px on the card sides, and that “lightens” the card borders with a lighter gray color (slate 400) on mouse hover.

The key Tailwind CSS classes of this element are:

*   `before:opacity-0 before:group-hover:opacity-100`: the `before` pseudo element is hidden by default, and it’s visible only when the card container is hovered. Note that we’re using a Tailwind CSS `group-hover` variant, so you want to make sure to add the `group` class to the card parent element.
*   `before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)]`: this class defines the position of the `before` pseudo element. We’re using Tailwind’s [arbitrary properties](https://tailwindcss.com/docs/adding-custom-styles#arbitrary-properties) to set the `translate-x` and `translate-y` CSS properties to the mouse position. This way, the `before` pseudo element will follow the mouse cursor.

### Use the after element for the spotlight

The `after` pseudo element is used to create the spotlight effect. It’s a layer placed above the card content, and it’s absolutely positioned to cover the entire card.

The code looks much like the `before` pseudo element, but there are a few differences:

*   The blurred circle looks a bit bigger (`after:w-96 after:h-96`)
*   The background color is different (`after:bg-indigo-500`) and the target opacity is lower (`after:hover:opacity-10`)
*   We’ll make the spotlight appear when hovering the single card, not the card container as we did for the `before` pseudo element

Cool! Now, when you hover the card, you should see the spotlight effect and the card borders lightening in the top left corner.

As you may guess, we want to animate the spotlight effect, so let’s add some JavaScript.

### Make the spotlight follow the mouse cursor

As you already know, we used `translate-x-[var(--mouse-x)]` and `translate-y-[var(--mouse-y)]` to set the position of both `before` and `after` pseudo element. In other words, we used `--mouse-x` and `--mouse-x` CSS variables to set the position of the elements instead of using a fixed value.

Now we want to update the value of those CSS variables on mouse move, so that the spotlight will follow the mouse cursor. We’ll use a bit of JavaScript to do that.

Let’s create a JavaScript class called `Spotlight` inside our `spotlight-effect.js` file:

```JavaScript
  // Cards spotlight
  class Spotlight {
    constructor(containerElement) {
      this.container = containerElement;
      this.cards = Array.from(this.container.children);
      this.mouse = {
        x: 0,
        y: 0,
      };
      this.containerSize = {
        w: 0,
        h: 0,
      };
      this.initContainer = this.initContainer.bind(this);
      this.onMouseMove = this.onMouseMove.bind(this);
      this.init();
    }
  
    initContainer() {
      this.containerSize.w = this.container.offsetWidth;
      this.containerSize.h = this.container.offsetHeight;
    }
  
    onMouseMove(event) {
      const { clientX, clientY } = event;
      const rect = this.container.getBoundingClientRect();
      const { w, h } = this.containerSize;
      const x = clientX - rect.left;
      const y = clientY - rect.top;
      const inside = x < w && x > 0 && y < h && y > 0;
      if (inside) {
        this.mouse.x = x;
        this.mouse.y = y;
        this.cards.forEach((card) => {
          const cardX = -(card.getBoundingClientRect().left - rect.left) + this.mouse.x;
          const cardY = -(card.getBoundingClientRect().top - rect.top) + this.mouse.y;
          card.style.setProperty('--mouse-x', `${cardX}px`);
          card.style.setProperty('--mouse-y', `${cardY}px`);
        });
      }
    }
  
    init() {
      this.initContainer();
      window.addEventListener('resize', this.initContainer);
      window.addEventListener('mousemove', this.onMouseMove);
    }
  }
  
  // Init Spotlight
  const spotlights = document.querySelectorAll('[data-spotlight]');
  spotlights.forEach((spotlight) => {
    new Spotlight(spotlight);
  });
```

We won’t go into the details of the code, but here’s a quick overview of what it does:

*   It creates a `Spotlight` instance for each element with the `data-spotlight` attribute, which is supposed to be the container holding the cards
*   When hovering the card container, it sets `--mouse-x` and `--mouse-y` CSS variables on each card
*   It updates `--mouse-x` and `--mouse-y` values on mouse move, but only if the mouse cursor is inside the card container

That way, the translate-x and translate-y values of the `before` and `after` pseudo elements will be updated on mouse move, and the spotlight will follow the mouse cursor.

Finally, ensure to add the `data-spotlight` attribute to the card container, and add the cards as first-level children of the container:

```html
  <div class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group" data-spotlight>
  
      <!-- Card 1 -->
      <div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
          <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
              <!-- Radial gradient -->
              <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
                  <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
              </div>
              <div class="flex flex-col h-full items-center text-center">
                  <!-- Image -->
                  <div class="relative inline-flex">
                      <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
                      <img class="inline-flex" src="./card-01.png" width="200" height="200" alt="Card 01" />
                  </div>
                  <!-- Text -->
                  <div class="grow mb-5">
                      <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
                      <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
                  </div>
                  <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                      <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
                          <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
                      </svg>
                      <span>Connect</span>
                  </a>
              </div>
          </div>
      </div>
  
      <!-- Card 2 -->
      <div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
          <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
              <!-- Radial gradient -->
              <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
                  <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
              </div>
              <div class="flex flex-col h-full items-center text-center">
                  <!-- Image -->
                  <div class="relative inline-flex">
                      <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
                      <img class="inline-flex" src="./card-02.png" width="200" height="200" alt="Card 02" />
                  </div>
                  <!-- Text -->
                  <div class="grow mb-5">
                      <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
                      <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
                  </div>
                  <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                      <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
                          <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
                      </svg>
                      <span>Connect</span>
                  </a>
              </div>
          </div>
      </div>
  
      <!-- Card 3 -->
      <div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
          <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
              <!-- Radial gradient -->
              <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
                  <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
              </div>
              <div class="flex flex-col h-full items-center text-center">
                  <!-- Image -->
                  <div class="relative inline-flex">
                      <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
                      <img class="inline-flex" src="./card-03.png" width="200" height="200" alt="Card 03" />
                  </div>
                  <!-- Text -->
                  <div class="grow mb-5">
                      <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
                      <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
                  </div>
                  <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
                      <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
                          <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
                      </svg>
                      <span>Connect</span>
                  </a>
              </div>
          </div>
      </div>
  
  </div>
```

* * *

##Create a reusable Spotlight component for Next.js<a name="Nextjs Version"></a>

Now that we have a working spotlight effect, let’s create a reusable component for Next.js with TypeScript support. The component we are going to build is available in our [GitHub repository](https://github.com/cruip/cruip-tutorials-next/blob/main/components/spotlight.tsx) that includes all Next.js examples from our tutorials.

Let’s create a new file called `spotlight.tsx` inside the `components` folder. Since we have a container and some cards inside it, we are going to create two components for them:

*   `Spotlight` is the main component, and it handles the full logic.
*   `SpotlightCard` is just a wrapper for the card content. It accepts a `className` prop that will be applied to the card container, and a `children` prop that will be the card content.

```tsx
  'use client'
  
  import React, { useRef, useState, useEffect } from 'react'
  import MousePosition from './utils/mouse-position'
  
  type SpotlightProps = {
    children: React.ReactNode
    className?: string
  }
  
  export default function Spotlight({
    children,
    className = '',
  }: SpotlightProps) {
  
    const containerRef = useRef<HTMLDivElement>(null)
    const mousePosition = MousePosition()
    const mouse = useRef<{ x: number; y: number }>({ x: 0, y: 0 })
    const containerSize = useRef<{ w: number; h: number }>({ w: 0, h: 0 })
    const [boxes, setBoxes] = useState<Array<HTMLElement>>([])
  
    useEffect(() => {    
      containerRef.current && setBoxes(Array.from(containerRef.current.children).map((el) => el as HTMLElement))
    }, [])
    
    useEffect(() => {    
      initContainer()
      window.addEventListener('resize', initContainer)
  
      return () => {
        window.removeEventListener('resize', initContainer)
      }
    }, [setBoxes])  
  
    useEffect(() => {
      onMouseMove()
    }, [mousePosition])
  
    const initContainer = () => {
      if(containerRef.current) {
        containerSize.current.w = containerRef.current.offsetWidth
        containerSize.current.h = containerRef.current.offsetHeight
      }
    }  
    
    const onMouseMove = () => {    
      if (containerRef.current) {
        const rect = containerRef.current.getBoundingClientRect()
        const { w, h } = containerSize.current
        const x = mousePosition.x - rect.left
        const y = mousePosition.y - rect.top
        const inside = x < w && x > 0 && y < h && y > 0      
        if (inside) {
          mouse.current.x = x
          mouse.current.y = y
          boxes.forEach((box) => {
            const boxX = -(box.getBoundingClientRect().left - rect.left) + mouse.current.x
            const boxY = -(box.getBoundingClientRect().top - rect.top) + mouse.current.y
            box.style.setProperty('--mouse-x', `${boxX}px`)
            box.style.setProperty('--mouse-y', `${boxY}px`)
          })
        }
      }
    }  
  
    return (
      <div className={className} ref={containerRef}>{children}</div>
    )
  }
  
  type SpotlightCardProps = {
    children: React.ReactNode,
    className?: string
  }
  
  export function SpotlightCard({
    children,
    className = ''
  }: SpotlightCardProps) {
    return <div className={`relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden ${className}`}>{children}</div>
  }
```

As you can see, the code looks much like the one we used in the previous section. We’ve just added some TypeScript annotations, and imported the `mouse-position.tsx` component – that we previously created for [another tutorial](https://cruip.com/how-to-create-a-beautiful-particle-animation-with-html-canvas/#create-a-new-component-for-mouse-position-tracking) – to get the mouse position.

We can now import and use them in a page or in another component, just like this:

```tsx
  <Spotlight className="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
    {/* Card #1 */}
    <SpotlightCard>
      <div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        {/* Radial gradient */}
        <div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div className="flex flex-col h-full items-center text-center">
          {/* Image */}
          <div className="relative inline-flex">
            <div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <Image className="inline-flex" src={Card01} width={200} height={200} alt="Card 01" />
          </div>
          {/* Text */}
          <div className="grow mb-5">
            <h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
    {/* Card #2 */}
    <SpotlightCard>
      <div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        {/* Radial gradient */}
        <div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div className="flex flex-col h-full items-center text-center">
          {/* Image */}
          <div className="relative inline-flex">
            <div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <Image className="inline-flex" src={Card02} width={200} height={200} alt="Card 02" />
          </div>
          {/* Text */}
          <div className="grow mb-5">
            <h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
    {/* Card #3 */}
    <SpotlightCard>
      <div className="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        {/* Radial gradient */}
        <div className="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div className="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div className="flex flex-col h-full items-center text-center">
          {/* Image */}
          <div className="relative inline-flex">
            <div className="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <Image className="inline-flex" src={Card03} width={200} height={200} alt="Card 03" />
          </div>
          {/* Text */}
          <div className="grow mb-5">
            <h2 className="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p className="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a className="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg className="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
  </Spotlight>
```

* * *

##Create a Vue Spotlight component<a name="Vue Version"></a>

As a final step, we are going to create a reusable component for Vue with TypeScript support, that is available in our [GitHub repository](https://github.com/cruip/cruip-tutorials-vue/blob/main/src/components/Spotlight.vue), which includes all the examples from the Cruip tutorials.

Let’s create a new file called `Spotlight.vue` inside the `components` folder.

```html
  <script setup lang="ts">
  import { ref, onMounted, onBeforeUnmount, reactive, watch } from 'vue'
  import useMousePosition from './utils/MousePosition'
  
  const containerRef = ref<HTMLCanvasElement | null>(null)
  const mousePosition = useMousePosition()
  const mouse = reactive<{ x: number; y: number }>({ x: 0, y: 0 })
  const containerSize = reactive<{ w: number; h: number }>({ w: 0, h: 0 })
  const boxes = ref<any[]>([])
  
  onMounted(() => {
    if (containerRef.value) {
      boxes.value = Array.from(containerRef.value.children)
    }
    initContainer()
    window.addEventListener('resize', initContainer)
  })
  
  onBeforeUnmount(() => {
    window.removeEventListener('resize', initContainer)
  })
  
  watch(
    () => mousePosition.value,
    () => {
      onMouseMove()
    }
  )
  
  const initContainer = () => {
    if (containerRef.value) {
      containerSize.w = containerRef.value.offsetWidth
      containerSize.h = containerRef.value.offsetHeight
    }
  }
  
  const onMouseMove = () => {
    if (containerRef.value) {
      const rect = containerRef.value.getBoundingClientRect()
      const { w, h } = containerSize
      const x = mousePosition.value.x - rect.left
      const y = mousePosition.value.y - rect.top
      const inside = x < w && x > 0 && y < h && y > 0
      if (inside) {
        mouse.x = x
        mouse.y = y
        boxes.value.forEach((box) => {
          const boxX = -(box.getBoundingClientRect().left - rect.left) + mouse.x
          const boxY = -(box.getBoundingClientRect().top - rect.top) + mouse.y
          box.style.setProperty('--mouse-x', `${boxX}px`)
          box.style.setProperty('--mouse-y', `${boxY}px`)
        })
      }
    }
  }
  </script>
  
  
  <template>
    <div ref="containerRef">
      <slot></slot>
    </div>
  </template>
```

As you can see, we are using the `MousePosition.ts` component – that we previously built for [the particle animation tutorial](https://cruip.com/how-to-create-a-beautiful-particle-animation-with-html-canvas/#create-a-helper-function-for-mouse-tracking) – which gets the mouse coordinates.

Then, we need a separate component for each box, which we are going to call `SpotlightCard.vue`:

```html
  <template>
    <div class="relative h-full bg-slate-800 rounded-3xl p-px before:absolute before:w-80 before:h-80 before:-left-40 before:-top-40 before:bg-slate-400 before:rounded-full before:opacity-0 before:pointer-events-none before:transition-opacity before:duration-500 before:translate-x-[var(--mouse-x)] before:translate-y-[var(--mouse-y)] before:group-hover:opacity-100 before:z-10 before:blur-[100px] after:absolute after:w-96 after:h-96 after:-left-48 after:-top-48 after:bg-indigo-500 after:rounded-full after:opacity-0 after:pointer-events-none after:transition-opacity after:duration-500 after:translate-x-[var(--mouse-x)] after:translate-y-[var(--mouse-y)] after:hover:opacity-10 after:z-30 after:blur-[100px] overflow-hidden">
      <slot></slot>
    </div>
    </template>
```
Finally, we can use the `Spotlight.vue` and `SpotlightCard.vue` components in our pages:

```html
  <Spotlight class="max-w-sm mx-auto grid gap-6 lg:grid-cols-3 items-start lg:max-w-none group">
    <!-- Card #1 -->
    <SpotlightCard>
      <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        <!-- Radial gradient -->
        <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div class="flex flex-col h-full items-center text-center">
          <!-- Image -->
          <div class="relative inline-flex">
            <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <img class="inline-flex" :src="Card01" width="200" height="200" alt="Card 01" />
          </div>
          <!-- Text -->
          <div class="grow mb-5">
            <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
    <!-- Card #2 -->
    <SpotlightCard>
      <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        <!-- Radial gradient -->
        <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div class="flex flex-col h-full items-center text-center">
          <!-- Image -->
          <div class="relative inline-flex">
            <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <img class="inline-flex" :src="Card02" width="200" height="200" alt="Card 02" />
          </div>
          <!-- Text -->
          <div class="grow mb-5">
            <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
    <!-- Card #3 -->
    <SpotlightCard>
      <div class="relative h-full bg-slate-900 p-6 pb-8 rounded-[inherit] z-20 overflow-hidden">
        <!-- Radial gradient -->
        <div class="absolute bottom-0 translate-y-1/2 left-1/2 -translate-x-1/2 pointer-events-none -z-10 w-1/2 aspect-square" aria-hidden="true">
          <div class="absolute inset-0 translate-z-0 bg-slate-800 rounded-full blur-[80px]"></div>
        </div>
        <div class="flex flex-col h-full items-center text-center">
          <!-- Image -->
          <div class="relative inline-flex">
            <div class="w-[40%] h-[40%] absolute inset-0 m-auto -translate-y-[10%] blur-3xl -z-10 rounded-full bg-indigo-600" aria-hidden="true"></div>
            <img class="inline-flex" :src="Card03" width="200" height="200" alt="Card 03" />
          </div>
          <!-- Text -->
          <div class="grow mb-5">
            <h2 class="text-xl text-slate-200 font-bold mb-1">Amazing Integration</h2>
            <p class="text-sm text-slate-500">Quickly apply filters to refine your issues lists and create custom views.</p>
          </div>
          <a class="inline-flex justify-center items-center whitespace-nowrap rounded-lg bg-slate-800 hover:bg-slate-900 border border-slate-700 px-3 py-1.5 text-sm font-medium text-slate-300 focus-visible:outline-none focus-visible:ring focus-visible:ring-indigo-300 dark:focus-visible:ring-slate-600 transition-colors duration-150" href="#0">
            <svg class="fill-slate-500 mr-2" xmlns="http://www.w3.org/2000/svg" width="16" height="14">
              <path d="M12.82 8.116A.5.5 0 0 0 12 8.5V10h-.185a3 3 0 0 1-2.258-1.025l-.4-.457-1.328 1.519.223.255A5 5 0 0 0 11.815 12H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM12.82.116A.5.5 0 0 0 12 .5V2h-.185a5 5 0 0 0-3.763 1.708L3.443 8.975A3 3 0 0 1 1.185 10H1a1 1 0 1 0 0 2h.185a5 5 0 0 0 3.763-1.708l4.609-5.267A3 3 0 0 1 11.815 4H12v1.5a.5.5 0 0 0 .82.384l3-2.5a.5.5 0 0 0 0-.768l-3-2.5ZM1 4h.185a3 3 0 0 1 2.258 1.025l.4.457 1.328-1.52-.223-.254A5 5 0 0 0 1.185 2H1a1 1 0 0 0 0 2Z" />
            </svg>
            <span>Connect</span>
          </a>
        </div>
      </div>
    </SpotlightCard>
  </Spotlight>
```

What do you think of the final result? Isn’t it beautiful? We personally love it, and we’re not surprised that in 2023 the spotlight card hover effect has become extremely popular, as it manages to make any design look fresher and more modern.

Feel free to apply this effect anywhere you want, and also test it on a white background if you think it may be more consistent with your designs.