Search code examples
htmlcssreactjstypescriptstyled-components

Gradient change of border dont work on mouse move


this Codepen uses conic-gradient to create the border. When you move your mouse over the element you see the gradient change. JavaScript is updating a CSS custom property that stores the rotation angle. The border-image-source property sets the source image used to create an element's border. As with other properties that accept an image value, any CSS gradient type is valid too.

Now when I try to recreate this with styled components in React TS I get the gradient border. But it does not change when I move with the mouse, like in the Codepen?

My App.tsx

import React from "react";
import styled from "styled-components";

export interface Props {
  card: Element;
}

const Card = styled.div`
  --startDeg: 0deg;
  inline-size: 50vmin;
  block-size: 50vmin;

  border: 5vmin solid hsl(100 100% 60%);
  border-image-slice: 1;

  border-image-source: conic-gradient(
    from var(--startDeg, 0deg),
    hsl(100 100% 60%),
    hsl(200 100% 60%),
    hsl(100 100% 60%)
  );

  display: grid;
  place-content: center;
  padding: 4ch;
  box-sizing: border-box;
  font-size: 10vmin;
`;

function App() {
  const card: Element = document.querySelector(".card");
  window.addEventListener(
    "mousemove",
    ({ clientX, clientY }) => {
      const { x, y, width, height } = card.getBoundingClientRect();
      const dx = clientX - (x + 0.5 * width);
      const dy = clientY - (y + 0.5 * height);
      const angle = (Math.atan2(dy, dx) * 180) / Math.PI;

      card.style.setProperty("--startDeg", `${angle + 90}deg`);
    },
    false
  );

  return <Card>Conic Gradient Border</Card>;
}

export default App;



Solution

  • In this codesandbox example you can see the working version. (JS version)

    Code:

    import React, { useEffect, useRef, useCallback } from "react";
    import styled from "styled-components";
    
    const Card = styled.div`
      --startDeg: 0deg;
      inline-size: 50vmin;
      block-size: 50vmin;
    
      border: 5vmin solid hsl(100 100% 60%);
      border-image-slice: 1;
    
      border-image-source: conic-gradient(
        from var(--startDeg, 0deg),
        hsl(100 100% 60%),
        hsl(200 100% 60%),
        hsl(100 100% 60%)
      );
    
      display: grid;
      place-content: center;
      padding: 4ch;
      box-sizing: border-box;
      font-size: 10vmin;
    `;
    
    function App() {
      const cardRef = useRef();
    
      const mouseMoveListener = useCallback(({ clientX, clientY }) => {
        const card = cardRef.current;
        const { x, y, width, height } = card.getBoundingClientRect();
        const dx = clientX - (x + 0.5 * width);
        const dy = clientY - (y + 0.5 * height);
        const angle = (Math.atan2(dy, dx) * 180) / Math.PI;
    
        card.style.setProperty("--startDeg", `${angle + 90}deg`);
      }, []);
    
      useEffect(() => {
        window.addEventListener("mousemove", mouseMoveListener, false);
        return () => window.removeEventListener("mousemove", mouseMoveListener);
      }, [mouseMoveListener]);
    
      return <Card ref={cardRef}>Conic Gradient Border</Card>;
    }
    
    export default App;
    

    Explanation:

    Based on React lifecycle, querySelector doesn't work properly due to the fact that React component doesn't finish painting the DOM when the component is mounted. Therefore, using a ref will ensure that the html element will be targeted correctly.

    Also, it is a good practice to register listeners inside the useEffect on the initial mount and remove it with a cleanup function to avoid memory leaks.