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;
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;
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.