MUI Gradient Tracked Button

Take a look at this Codepen example I created. It showcases a hover effect where the gradient follows the mouse cursor.

In order to achieve this effect, I have defined two CSS variables - --x and --y, to keep track of the mouse position on the button. The --size CSS variable is responsible for adjusting the dimensions of the gradient.

The gradient is generated using the rule

background: radial-gradient(circle closest-side, pink, transparent)
which ensures it appears correctly on the button.

To make this work, I used functions like Document.querySelector() and EventTarget.addEventListener() to handle the 'mousemove' event. Additionally, Element.getBoundingClientRect() and

CSSStyleDeclaration.setProperty()
are employed to update the values of --x and --y CSS variables dynamically.

Now, my next challenge is to recreate this effect as a styled component in React Typescript utilizing MUI Button. I attempted to declare the necessary CSS styles within my styled Button component, however, it seems they are not being applied properly.

Button.tsx

import React from 'react';
import { styled, Theme } from '@mui/material/styles';
import { Button, SxProps } from '@mui/material';

const HoverButton = styled(Button)(({ theme }) => ({
    borderRadius: 100,
    ".mouse-cursor-gradient-tracking": {
        position: "relative",
        background: "#7983ff",
        padding: "0.5rem 1rem",
        fontSize: "1.2rem",
        border: "none",
        color: theme.palette.secondary.contrastText,
        cursor: "pointer",
        outline: "none",
        overflow: "hidden",
      },
      
      ".mouse-cursor-gradient-tracking span": {
        position: "relative",
      },
      
      ".mouse-cursor-gradient-tracking:before": {
        --size: 0,
        content: '',
        position: "absolute",
        left: "var(--x)",
        top: "var(--y)",
        width: "var(--size)",
        height: "var(--size)",
        background: "radial-gradient(circle closest-side, pink, transparent)",
        transform: "translate(-50%, -50%)",
        transition: "width 0.2s ease, height 0.2s ease",
      },
      
      ".mouse-cursor-gradient-tracking:hover:before": {
        "--size": "200px"
      },
}));

export function SubmitButton(props: { children: React.ReactNode; sx?: SxProps<Theme> }) {
    let button:<Element | null> = document.querySelector('.mouse-cursor-gradient-tracking');
    button.addEventListener('mousemove', e => {
        let rect = e.target.getBoundingClientRect();
        let x = e.clientX - rect.left;
        let y = e.clientY - rect.top;
        button.style.setProperty('--x', x + 'px');
        button.style.setProperty('--y', y + 'px');
    });

    return (
        <HoverButton type="submit" variant="contained" sx={props.sx}>
            {props.children}<span>Hover me</span>
        </HoverButton>
    );
}

Answer №1

Check out this interactive sandbox for a demonstration.

Now, let me highlight some areas where improvements can be made in your current approach.

  1. Your current method involves imperatively selecting the button by class and adding an event handler manually. Here is how you can do it using React:
    let button:<Element | null> = document.querySelector('.mouse-cursor-gradient-tracking');
    button.addEventListener('mousemove', e => {
        let rect = e.target.getBoundingClientRect();
        let x = e.clientX - rect.left;
        let y = e.clientY - rect.top;
        button.style.setProperty('--x', x + 'px');
        button.style.setProperty('--y', y + 'px');
    });

You can simplify this by directly adding an event handler to a button component in React, allowing React to manage the event handling.

const Component = () => {

  const mouseDownHandler = (e) => {
    // add your logic here
  }
  
 return (<button onMouseDown={mouseDownHandler}>Click me</button>);
};
  1. Avoid mixing classes with styled or css-in-js.
const HoverButton = styled(Button)(({ theme }) => ({
    ".mouse-cursor-gradient-tracking": {
        position: "relative",
        background: "#7983ff",
      },
    ".mouse-cursor-gradient-tracking span": {
        position: "relative",
      },
      
      ".mouse-cursor-gradient-tracking:before": {
        --size: 0,
        content: '',
      },
      
      ".mouse-cursor-gradient-tracking:hover:before": {
        "--size": "200px"
      },
}));

Simplify this by using direct CSS styling within your button component:

const HoverButton = styled(Button)`  
  position: relative;
  background: #7983ff;

  & > span {
    ... include your code here
  }

  &:before {
   ... include your code here
  }
  
  &:hover:before {
   ... include your code here
  } 
`;

const SubmitButton = () => {
 return <HoverButton>Text</HoverButton>
}

This way, the styles will be directly applied to the button with the pseudo-classes without the need for a separate custom class unless necessary.

  1. For dynamically updating styles, avoid manual style updates and utilize state management instead:
button.addEventListener('mousemove', e => {
  ...include other codes
  button.style.setProperty('--x', x + 'px');
  button.style.setProperty('--y', y + 'px');
});

To achieve dynamic updates, use state in conjunction with event handlers as demonstrated in point no. 1. This way, you can update the CSS values based on state changes.

Here's the complete revised code for reference.

import { useState } from "react";
import { styled, Theme } from "@mui/material/styles";

// Rest of the code omitted for brevity

export default function App() {
  return (
    <div className="App">
      <h1>Hello CodeSandbox</h1>
      <h2>Start editing to see some magic happen!</h2>
      <SubmitButton />
    </div>
  );
}

Find more resources at: MUI styled(), Styled component props

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

Adjust the image's height to adapt to the size of the browser

I'm having trouble resizing the images to match the height of the browser window. My goal is to limit the images' height to a maximum of 1100px and have them scale down based on the height of the browser. I've experimented with setting the ...

Enhancing Redux in an Electron app using Sqlite3

Currently developing an Electron application where I aim to showcase data sourced from a local sqlite3 database file. My frontend framework of choice is React, supplemented by Redux for table data updates. However, grappling with the most efficient metho ...

A space designated for numerous receivers

Is there a way to create a field that contains other elements, similar to sending messages to multiple users in a social network? https://i.stack.imgur.com/P9e24.png I attempted to understand the code for this, but it's quite complex. If anyone could ...

Dynamic grid arrangement showcasing cells of varying heights

I am dealing with a grid layout where cells are dynamically generated using the following code: <style> .calWrapper{ width:200px; float:left; } </style> <div class="container"> <?php for($i=1; $i<=12; $i++){ echo ...

The CORS policy is preventing the AJAX request from functioning properly on localhost

Recently, I have been working on an Angular application that requires interaction with an API. To test it out, I set up an API on my localhost and made a request using AJAX. However, I encountered the dreaded CORS error. Despite trying various solutions fo ...

A guide on loading modules dynamically using React and Typescript from a server

I am working on a React / Typescript / Webpack / React-Router application that contains some large JS modules. Currently, I have two bundles (common.js and app.js) included on every page, with common.js being a CommonsChunkPlugin bundle. However, there is ...

What is the best way to ensure that the same information is included on every page of

Currently developing a dynamic website where certain components such as the fixed header, menubar, and footer are common across multiple pages. What is the best way to include these fixed components on every webpage? I am utilizing JSP and JavaScript for ...

Is it necessary to only override the monospaced font?

Can the monospace font in Angular Material be customized for just the <code>foo</code> blocks? I want to use a font where the number zero 0 looks distinct from the letter oh O. ...

Create a class for the grandparent element

Is there a way to dynamically add a class to a dropdown menu item when a specific child element is clicked? Here's the HTML structure I have: <ul id="FirstLevel"> <li><a href="#">FirstLevel</a></li> <li>< ...

Optimal methods for handling Ajax requests in the present day

Recently, I revisited some websites I co-built with a friend and was working on getting them functional again. It's been a while since I've done any AJAX work, and I'm realizing that there aren't many resources available to help trouble ...

Items in the Vue.Draggable display can be arranged on both the left and right

Recently delving into Vue.js, I am utilizing Vuedraggable for item dragging in my project. Currently, the items in the draggable div are shown as Text 1 Text 2 Text 3 Text 4 Is there a way to rearrange them to display like this? Text 1 Text 2 Text 3 T ...

Error: Attempted to search for 'height' using the 'in' operator in an undefined variable

I came across the following code snippet: $('.p1').click(function(){ var num = 10; var count = 0; var intv = setInterval(anim,800); function anim(){ count++; num--; ...

I am looking to create buttons that can switch between two different styles of a specific element, like an h1 tag, when clicked. However, instead of toggling

//In this HTML document, I am trying to achieve a functionality where my buttons can toggle the style of an h1 element between the colors yellow and purple when clicked. However, I have encountered an issue where the buttons disappear during a transition ...

Alter the value of a parameter within a script tag with JavaScript/jQuery

Looking to dynamically change a parameter value using JavaScript or jQuery. Here is an example URL: www.exampleurl.com/somepage?foo=test I have a function that can extract the value after the 'foo' parameter: function getParameterByName(name, ...

Moving through content on a single page

import React, { Component, useState } from "react"; import { Content, List, ListItem, Left, Right, Icon, Container, Header, Body, Button, Title, } from "native-base"; //Chapter One expor ...

How can I eliminate the gap above the footer in iframes alignment?

I have a setup with four iframes: header, menuframe, bodyframe, and footer. The menuframe and bodyframe are positioned next to each other with space between the footer and the menuframe/bodyframe. How can I remove this space? Here is the CSS: iframe{ ...

The page could not be generated due to a server error with the syntax. More specifically, an Unexpected token 'export' caused a SyntaxError

Encountering an issue while attempting to retrieve data using getServerSideProps. Seeking assistance, thank you! Server Error SyntaxError: Unexpected token 'export' This error occurred during the page generation. Any console logs will be shown ...

Combining two classes into a single class using ‘this’ in JavaScript

I'm encountering an issue where I am unable to figure out how to extend from the third class. So, I really need guidance on how to call the A class with the parameter 'TYPE', extend it with C, and then be able to call getType() with class C. ...

What is the best way to loop through an API response in Vue.js?

Hey there, I'm new to Vue and struggling with iterating through data in my template. Despite being able to log everything properly, I can't seem to render it on the page. Here's the API URL: https://private-922d75-recruitmenttechnicaltest.a ...

How to Implement Snap-Enabled Dragging in a Container Using jQuery UI

Issue Description (Fiddle): I am using the jQuery UI's .draggable() function to make the red element snap inside the blue container. However, it is snapping to one edge only instead of completely fitting inside (both edges). It requires further dragg ...