React TS implementation of radial animated focus effect using mask-image technique

I am attempting to replicate the Radial animated focus effect using mask-image: Codepen. While I could simply copy and paste the CSS into a .css file, I want to achieve the same outcome with a styled component. I have defined the CSS within my styled component and applied it accordingly. However, I am unsure why nothing is happening at all and what alternative I should use instead of getElementById as manual DOM manipulation is considered poor practice?

App.tsx

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

const Property = styled.div`
  @property --focal-size {
    syntax: "<length-percentage>";
    initial-value: 100%;
    inherits: false;
  }
`;

const FocusZoom = styled.div`
--mouse-x: center;
  --mouse-y: center;
  --backdrop-color: hsl(200 50% 0% / 50%); /* can't be opaque */
  --backdrop-blur-strength: 10px;
  
  position: fixed;
  touch-action: none;
  inset: 0;
  background-color: var(--backdrop-color);
  backdrop-filter: blur(var(--backdrop-blur-strength));
  
  mask-image: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y), 
    transparent var(--focal-size), 
    black 0%
  );
  
  transition: --focal-size .3s ease;
  
  /*  debug/grok the gradient mask image here   */
/*   background-image: radial-gradient(
    circle, 
    transparent 100px, 
    black 0%
  ); */
}
`;

function App(bool: boolean) {
  const zoom: Element = document.querySelector("focus-zoom");

  const toggleSpotlight = (bool) =>
    zoom.style.setProperty("--focal-size", bool ? "15vmax" : "100%");

  window.addEventListener("pointermove", (e) => {
    zoom.style.setProperty("--mouse-x", e.clientX + "px");
    zoom.style.setProperty("--mouse-y", e.clientY + "px");
  });

  window.addEventListener("keydown", (e) => toggleSpotlight(e.altKey));
  window.addEventListener("keyup", (e) => toggleSpotlight(e.altKey));
  window.addEventListener("touchstart", (e) => toggleSpotlight(true));
  window.addEventListener("touchend", (e) => toggleSpotlight(false));

  return (
    <>
      <h1>
        Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
      </h1>
      <FocusZoom></FocusZoom>
    </>
  );
}

export default App;

Answer №1

Discover a unique solution using styled components Code sandbox

import React, { useEffect } from "react";
import styled, { createGlobalStyle } from "styled-components";

export const GlobalStyle = createGlobalStyle`
  body {
    display: flex;
    align-items: center;
    justify-content: center;
  }

/* custom properties */
  :root {  
    --focal-size: { 
    syntax: "<length-percentage>";
    initial-value: 100%;
    inherits: false;
  }
  --mouse-x: center;
  --mouse-y: center;
  --backdrop-color: hsl(200 50% 0% / 50%);
  --backdrop-blur-strength: 10px;
}
`;

const Wrapper = styled.div`
  height: 400px;
  width: 400px;
  background: conic-gradient(
    from -0.5turn at bottom right,
    deeppink,
    cyan,
    rebeccapurple
  );
`;
const FocusZoom = styled.div`
  position: fixed;
  touch-action: none;
  inset: 0;
  background-color: var(--backdrop-color);
  backdrop-filter: blur(var(--backdrop-blur-strength));

  mask-image: radial-gradient(
    circle at var(--mouse-x) var(--mouse-y),
    transparent var(--focal-size),
    black 0%
  );

  transition: --focal-size 0.3s ease;
`;

function App(bool) {
  useEffect(() => {
    const zoom = document.getElementById("zoomId");

    const toggleSpotlight = (bool) =>
      zoom.style.setProperty("--focal-size", bool ? "15vmax" : "100%");

    window.addEventListener("pointermove", (e) => {
      zoom.style.setProperty("--mouse-x", e.clientX + "px");
      zoom.style.setProperty("--mouse-y", e.clientY + "px");
    });

    window.addEventListener("keydown", (e) => toggleSpotlight(e.altKey));
    window.addEventListener("keyup", (e) => toggleSpotlight(e.altKey));
    window.addEventListener("touchstart", (e) => toggleSpotlight(true));
    window.addEventListener("touchend", (e) => toggleSpotlight(false));
    toggleSpotlight();
  }, []);

  return (
    <Wrapper>
      <h1>
        Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
      </h1>

      <FocusZoom id="zoomId"></FocusZoom>
    </Wrapper>
  );
}

export default App;


Don't forget to import global styles and components in the app file.

import Test, { GlobalStyle } from "./test";

export default function App() {
  return (
    <div className="App">
      <GlobalStyle />
      <Test />
    </div>
  );
}

Answer №2

When working with a React component template, referencing a DOM element can be done easily by utilizing the useRef hook:

function App() {
  // Obtain a direct reference to a DOM element
  const zoomRef = useRef<HTMLDivElement>(null);

  const toggleSpotlight = (bool: boolean) =>
    // Access the DOM element using the .current property of the ref
    zoomRef.current?.style.setProperty(
      "--focal-size",
      bool ? "15vmax" : "100%"
    );

  // More code for event listeners and functionality

  return (
    <>
      <h1>
        Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
      </h1>
      <FocusZoom ref={zoomRef} /> {/* Provide the reference through the special ref prop */}
    </>
  );
}

Demo: https://codesandbox.io/s/exciting-flower-349b48?file=/src/App.tsx


An alternative approach could involve utilizing styled-components props adaptation to replace calls to zoom.style.setProperty(), as demonstrated in Jumping Text in React with styled component

This method aids in substituting the usage of CSS variables, except for --focal-size due to its configured transition.

const FocusZoom = styled.div<{
  focalSize: string; // Define additional styling props for adaptation
  pointerPos: { x: string; y: string };
}>`
  --focal-size: ${(props) => props.focalSize};

  position: fixed;
  touch-action: none;
  inset: 0;
  background-color: hsl(200 50% 0% / 50%);
  backdrop-filter: blur(10px);

  mask-image: radial-gradient(
    circle at ${(props) => props.pointerPos.x + "" + props.pointerPos.y},
    transparent var(--focal-size),
    black 0%
  );

  transition: --focal-size 0.3s ease;
`;

function App() {
  // Store dynamic values in state
  const [focalSize, setFocalSize] = useState("100%");
  const [pointerPosition, setPointerPosition] = useState({
    x: "center",
    y: "center"
  });

  const toggleSpotlight = (bool: boolean) =>
    // Update the state instead of directly manipulating the DOM element
    setFocalSize(bool ? "15vmax" : "100%");

  // More code for event listeners and functionality

  return (
    <>
      <h1>
        Press <kbd>Opt/Alt</kbd> or touch for a spotlight effect
      </h1>
      {/* Pass the states to the styled component */}
      <FocusZoom focalSize={focalSize} pointerPos={pointerPosition} />
    </>
  );
}

Demo: https://codesandbox.io/s/frosty-swirles-jdbcte?file=/src/App.tsx

While this solution may seem excessive for scenarios where values constantly change, it effectively separates logic from style implementation, abstracting the use of CSS variables.


On a side note: when adding event listeners, ensure they are attached only once (often done with useEffect(cb, []) with an empty array), and that they are removed on component unmount. Alternatively, you can utilize useEvent from react-use which handles these tasks directly:

React sensor hook that subscribes a handler to events.

import { useEvent } from "react-use";

function App() {
  // Attaches to window and manages removal on unmount
  useEvent("pointermove", (e: PointerEvent) =>
    setPointerPosition({ x: e.clientX + "px", y: e.clientY + "px" })
  );

  // Additional functionality
}

Answer №3

Try utilizing the useRef hook in place of getElementById

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

Guide to match the size of <td> with an <img>

I am currently attempting to align several images in my HTML using the <table> element. My goal is to eliminate any white space between the images. You can view my progress so far in this JSFiddle example: JSFiddle example. However, I am still encoun ...

How can I transform an HTML element into a textarea using CSS or JavaScript?

While browsing a webpage, I discovered a <div> element with the class .plainMail and I want to find a way to easily select all its text by simply pressing Ctrl+A. Currently using Firefox 22, I am considering converting the div.plainMail into a texta ...

Tips on implementing computed properties in Vue.js while using TypeScript

There is a significant amount of documentation on how to utilize Vue.js with JavaScript, but very little information on using TypeScript. The question arises: how do you create computed properties in a vue component when working with TypeScript? According ...

I encountered an issue while working with Material-UI and Mobx. The error message reads: "Material-UI: capitalize(string) function requires a string argument

enter code hereI encountered this issue when I copied a React component from and the state of this component is managed by Mobx as shown below: @observable snackbarState = { open: false, vertical: null, horizontal: null, }; @action toggle ...

Creating one-to-one relationships in sequelize-typescript can be achieved by setting up multiple foreign keys

I have a question about adding multiple foreign keys to an object. Specifically, I have a scenario with Complaints that involve 2 Transports. One is used to track goods being sent back, and the other is used for goods being resent to the customer. @Table({ ...

What is the best choice for the navigation menu: using `<ul>` or `<div>`?

Is it considered unprofessional to construct a navigation menu without utilizing unordered list tags? For instance, opting for <div> elements instead. <div class="navbar"> <a href="#"> Home </a> </div> ...

Aligning the H3 and Anchor tags at the bottom of the page

I am currently utilizing Bootstrap 3.0 and hoping to achieve a layout similar to this: Favorites Add I want the "Favorites" text to be aligned on the left and enclosed in an H3 tag, while the "Add" link is aligned on the right within an anc ...

Tips for preventing a React component from re-fetching data when navigating back using the browser button

In my React app using Next.js and the Next Link for routing, I have two pages set up: /product-list?year=2020 and /product-list/details?year=2020&month=4 Within the pages/product-list.js file, I am utilizing React router to grab the query parameter ye ...

Show the alias of a type in Vscode Typescript instead of its definition

Here is some code that I am working with: type Opaque<T,U> = T & {_:U}; type EKey = Opaque<number,'EKey'>; type AKey = Opaque<EKey,'AKey'>; type PKey = Opaque<AKey,'PKey'>; let a = <PKey>1; ...

Thumbnail for Reddit link not displaying due to dynamic OG:image issue

I am currently working on a NextJS app that allows users to share links to Reddit. The issue I am facing is that the link preview in Reddit always shows the same thumbnail image, regardless of the shared link. Interestingly, this problem does not occur whe ...

Convert your Express.js API routes to Next.js API routes for better performance and flexibility

I'm currently working on an Express API route that looks like this: const router = require("express").Router(); router.get("/:jobId/:format", async (req, res) => { try { const { jobId, format } = req.params; if (form ...

Encountered a problem while executing CSS in Shiny R

I am attempting to incorporate my css file into Shiny R. I have been able to run the application without any issues before adding the style.css file. Below are the steps I have taken: library(shiny) library(tidyverse) library(leaflet) library(leaflet.extra ...

Having trouble linking React to Backend in MERN stack due to CORS error

I have successfully created a microservice backend running on Kubernetes through Digital Ocean. Currently, I am facing an issue while attempting to connect my React frontend to the backend. The error message I receive is as follows: Access to XMLHttpReque ...

JQuery along with Font Awesome: Setting the default value for star rating

The Situation: I am currently working on a system where users can rate various products using Laravel. Each product has an average rating which is then used to display stars. These ratings are shown on the "Product Listing" page, which contains multiple p ...

Is it possible to trigger a re-render of a child component from its parent component in React without altering its props?

The issue at hand My parent component (X) is responsible for managing numerous states and child components. Within these children, there is an animated component (Y) - an avatar with various facial expressions that change in sync with its dialogue. Curr ...

What about employing the position:fixed property to create a "sticky" footer?

I've come across various methods for creating a sticky footer, such as Ryan Fait's approach found here, another one here, and also here. But why go through the trouble of using those techniques when simply applying #footer{position:fixed; bottom ...

Is there a way to stop Bootstrap from automatically bolding the font?

When testing this simple example without the bootstrap link, everything seems to be working correctly: Hovering over any word changes its color to red, and when hovering away it returns to black. But as soon as the bootstrap link is included, the text bec ...

Simply input the URL with parameters directly into the search bar in Next.js to access the page

I understand how dynamic routing works within a Next.js app when using Link or Router.push(). However, I am unsure about what happens if I have a URL like this: http://localhost:3000/jhondoe If I paste this URL directly into the browser, how does Next ...

The visual symbol is not being shown on the selection box in Mozilla Firefox

I've encountered an issue with a checkbox I created to display a + and - for expand and collapse functionality. HTML Code: <input type=checkbox class="ins" ng-model=show ng-class='{open:show}'><b>Show</b> CSS code: .ins ...

Changing Image Size in Real Time

Trying to figure out the best way to handle this situation - I need to call a static image from an API server that requires height and width parameters for generating the image size. My website is CSS dynamic, adapting to different screen sizes including ...