cssreactjssassstyled-componentsreflow

CSS Ripple effect with pseudo-element causing reflow


I'm trying to create the material ripple effect with styled-components (which is unable to import the material web-components mixins). I want to stick with using the after element for the foreground effect, to keep the accesibility tree intact.

However, most notably on mobile, the ripple transition is causing reflow in the button's content. It would seem to happen because of the display change (from none to block), but I have tried some alternatives which don't share this artifact, and this side-effect is still present.

Here's my code (I'm using some props to set the ripple, but you can hard-set them if you want to reproduce): [Here was an outdated version of the code]

Thanks for the attention.

Edit: The bug only happens when I add a hover effect to the button, very weird. Below follows the link and a code sample (you will have to set a react repository in order to reproduce it, unfortunately)

https://github.com/Eduardogbg/ripple-hover-reflow-bug

import React, { useRef, useReducer } from 'react';
import ReactDOM from 'react-dom';
import styled from 'styled-components'

const ButtonBase = styled.button`
  cursor: pointer;

  width: 250px;
  height: 6vh;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  outline: none;
  position: relative;
  overflow: hidden;
  border-width: 0;

  background-color: cyan;

  :hover {
    filter: brightness(1.06);
  }

  ::after {
    content: '';
    pointer-events: none;
    
    width: ${({ ripple }) => ripple.size}px;
    height: ${({ ripple }) => ripple.size}px;

    display: none;
    position: absolute;
    left: ${({ ripple }) => ripple.x}px;
    top: ${({ ripple }) => ripple.y}px;

    border-radius: 50%;
    background-color: ${({ ripple }) => ripple.color};

    opacity: 0;
    animation: ripple ${({ ripple }) => ripple.duration}ms;
  }

  :focus:not(:active)::after {
    display: block;
  }
  
  @keyframes ripple {
    from {
      opacity: 0.75;
      transform: scale(0);
    }
    to {
      opacity: 0;
      transform: scale(2);
    }
  }
`

const rippleReducer = ref => (ripple, event) => {
  const { x, y, width, height } = ref.current.getBoundingClientRect()
  const size = Math.max(width, height) 
  
  return {
    ...ripple,
    size, 
    x: event.pageX - x - size / 2,
    y: event.pageY - y - size / 2
  }
}

const DEFAULT_RIPPLE = {
  size: 0,
  x: 0,
  y: 0,
  color: 'white',
  duration: 850
}

const Button = props => {
  const ref = useRef(null)

  const [ripple, dispatch] = useReducer(
    rippleReducer(ref),
    { ...DEFAULT_RIPPLE, ...props.ripple }
  )

  return (
    <ButtonBase
      ref={ref}
      className={props.className}
      ripple={ripple}
      onClick={event => {
        event.persist()
        dispatch(event)
      }}
    >
      {props.children}
    </ButtonBase>
  )
}

ReactDOM.render(
  <div style={{
    backgroundColor: 'red',
    width: '500px', height: '500px',
    display: 'grid',
    placeItems: 'center'
  }}>
    <Button>
      <span style={{ fontSize: '30px' }}>
        abacabadabaca
      </span>
    </Button>
  </div>,
  document.getElementById('root')
);

Solution

  • The problem seems to be related to this chromium bug that was supposedly solved a few years ago: Image moves on hover when changing filter in chrome

    Setting transform: translate3d(0,0,0); looks like a fix, though my eye isn't pixel-perfect.