React and CSS Pseudo-Classes

What Happens?

I found a pretty component in codepen, implemented by pure CSS and HTML.

I want to use it in my React project.

There are many features implemented by CSS pseudo-classes. I'm trying to get the content of ::before and ::after.

I spent four hours on Google and found nothing works for me.

And I had to rewrite the whole logic in React. I found it is impossible(super hard) to communicate with CSS pseudo-classes in React

Conclusion

React has no elegant way to operate CSS pseudo-classes

Then?

For example:

<HTML>
<header>
	<style>
		span::before {
			content: "before";
			background: #979898;
		}

		span::after {
			content: "after";
			background: #77E371;
		}
	</style>
</header>

<body>
	<span>Content</span>
</body>

</html>

It will render the following elements 1.png 2.png

And we should notice that the elements generated by CSS pseudo-classes can not be accessed by DOM API. That causes many problems in React when we have a feature implemented by CSS pseudo-classes.

Don't use CSS pseudo-classes in React if you want to use the state to control it or get information from it.

Here is the right way to avoid CSS pseudo-classes

const Demo = () => {
  return <>
    <span className="before-span"></span>
    <span>Content</span>
    <span className="after-span"></span>
  </>
}

And write the corresponding CSS style in before-span and after-span.

Then you can control the elements.

It is the historical reason between the support of CSS pseudo-classes from browsers, and virtual dom in React that causes this tricky problem.

The browser developers want to enable CSS to declare some virtual DOMs. And React also handles virtual DOMs as its own logic. There is no relationship between "CSS virtual DOMs" and "React Virtual DOMs".

But once in CSS you declare the pseudo-classes, you can only get it by Window.getComputedStyle() and it's hard to do.

Example

from CSS ::before, ::after to React Component

original codes

My React version

# index.jsx
/**
 * @author Tempest
 * @email tar118@pitt.edu
 * @create date 2022-08-31 13:00:50
 * @modify date 2022-08-31 13:00:50
 * @desc markdown preview component
 */
/* eslint-disable react/no-children-prop */
import React, { useEffect, useState, useRef } from 'react';
import PropTypes from 'prop-types';
import './index.css';

const FlipButton = ({ init = false, openElement, closeElement, size = 'normal', onClick }) => {
  const [open, setOpen] = useState(init);
  const onButtonClick = () => {
    setOpen(!open);
    try {
      onClick();
    } catch (e) {
      console.log('[error] [FlipIconButton] invoke onClick failed', e);
    }
  }
  const buttonCSS = ['flip-switch'];
  if (size === 'small') {
    buttonCSS.push('small-switch');
  } else if (size === 'large') {
    buttonCSS.push('large-switch');
  }
  return (
  <div className={buttonCSS.join(' ')}>
    <input type="checkbox" id="c2" />
    <div className='flip-bg' onClick={onButtonClick}>
      {open && <label className="before">{openElement}</label>}
      {!open && <label className='after'>{closeElement}</label>}
    </div>
  </div>
  )
}

FlipButton.propTypes = {
  init: PropTypes.bool,
  openElement: PropTypes.oneOfType([
    PropTypes.element, PropTypes.string
  ]),
  closeElement: PropTypes.oneOfType([
    PropTypes.element, PropTypes.string
  ]),
  size: PropTypes.oneOf(['small', 'normal', 'large']),
  onClick: PropTypes.func,
};

export default FlipButton;

CSS file

# index.css
.flip-switch {
  font-family: inherit;
  -webkit-font-smoothing: antialiased;
  -webkit-user-select: none;
  -webkit-tap-highlight-color: rgba(0, 0, 0, 0);
  display: block;
  height: 35px;
  width: 62px;
  background: #ffffff;
  padding: 0;
  margin: 0 auto;
  font-size: medium;
  /* perspective: 50px; */
  -webkit-perspective: 50px;
  -moz-perspective: 50px;
  border-radius: 50px;
  -webkit-border-radius: 50px;
  -moz-border-radius: 50px;
}

.small-switch {
  height: 25px;
  width: 45px;
  font-size: small;
}

.large-switch {
  height: 45px;
  width: 75px;
  font-size: large;
}

.flip-switch input {
  opacity: 0;
  position: absolute;
  top: 0;
  right: 100%;
  width: 1px;
  height: 1px;
}

.flip-switch .flip-bg, .flip-switch .before, .flip-switch .after {
  display: flex;
  flex-direction: column;
  justify-content: center;
  align-items: center;
  position: relative;
  height: 100%;
  width: 100%;
  outline: none;
  margin: 0;
  -webkit-appearance: none;
  background: none;
  border: none;
  transform-style: preserve-3d;
  -webkit-transform-style: preserve-3d;
  -moz-transform-style: preserve-3d;
  border-radius: inherit;
  -webkit-border-radius: inherit;
  -moz-border-radius: inherit;
  animation: uncheck 0.6s ease-out;
  -webkit-animation: uncheck 0.6s ease-out;
  -moz-animation: uncheck 0.6s ease-out;
  box-shadow: none;
  -webkit-box-shadow: none;
  -moz-box-shadow: none;
}

.flip-switch .after {
  z-index: 2;
  transform: rotateY(0deg);
  -webkit-transform: rotateY(0deg);
  -moz-transform: rotateY(0deg);
  background: #77E371;
}

.flip-switch .before {
  z-index: 2;
  transform: rotateY(0deg);
  -webkit-transform: rotateY(0deg);
  -moz-transform: rotateY(0deg);
  background: #979898;
}

.flip-switch input:checked + label {
  transform: rotateY(180deg);
  -webkit-transform: rotateY(180deg);
  -moz-transform: rotateY(180deg);
  animation: check 0.6s ease-out;
  -webkit-animation: check 0.6s ease-out;
  -moz-animation: check 0.6s ease-out;
}


@keyframes check {
  0% {
    transform: rotateY(0deg);
  }
  50% {
    transform: rotateY(195deg);
  }
  75% {
    transform: rotateY(165deg);
  }
  100% {
    transform: rotateY(180deg);
  }
}
@-webkit-keyframes check {
  0% {
    -webkit-transform: rotateY(0deg);
  }
  50% {
    -webkit-transform: rotateY(195deg);
  }
  75% {
    -webkit-transform: rotateY(165deg);
  }
  100% {
    -webkit-transform: rotateY(180deg);
  }
}
@-moz-keyframes check {
  0% {
    -moz-transform: rotateY(0deg);
  }
  50% {
    -moz-transform: rotateY(195deg);
  }
  75% {
    -moz-transform: rotateY(165deg);
  }
  100% {
    -moz-transform: rotateY(180deg);
  }
}
@keyframes uncheck {
  0% {
    transform: rotateY(180deg);
  }
  50% {
    transform: rotateY(-15deg);
  }
  75% {
    transform: rotateY(15deg);
  }
  100% {
    transform: rotateY(0deg);
  }
}
@-webkit-keyframes uncheck {
  0% {
    -webkit-transform: rotateY(180deg);
  }
  50% {
    -webkit-transform: rotateY(-15deg);
  }
  75% {
    -webkit-transform: rotateY(15deg);
  }
  100% {
    -webkit-transform: rotateY(0deg);
  }
}
@-moz-keyframes uncheck {
  0% {
    -moz-transform: rotateY(180deg);
  }
  50% {
    -moz-transform: rotateY(-15deg);
  }
  75% {
    -moz-transform: rotateY(15deg);
  }
  100% {
    -moz-transform: rotateY(0deg);
  }
}