Skip to content

Instantly share code, notes, and snippets.

@Evavic44
Last active December 25, 2023 15:59
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Evavic44/7d0321a8f631ab91e5b2794697cca063 to your computer and use it in GitHub Desktop.
Save Evavic44/7d0321a8f631ab91e5b2794697cca063 to your computer and use it in GitHub Desktop.
Before and after slider component

Slider Toggle Button

import { FormEvent } from "react";
import styles from "./ToggleButton.module.css";

type ToggleProps = {
  onToggle: (isChecked: boolean) => void;
};

export default function ToggleButton({ onToggle }: ToggleProps) {
  const checkToggle = function (e: FormEvent<HTMLInputElement>) {
    const isChecked = e.currentTarget.checked;
    onToggle(isChecked);
  };

  return (
    <div className={styles.toggle}>
      <input
        onClick={checkToggle}
        className={`${styles.tgl} ${styles.tglSkewed}`}
        id="switch"
        type="checkbox"
      />
      <label
        className={styles.tglBtn}
        data-tg-off="OFF"
        data-tg-on="ON"
        htmlFor="switch"
      ></label>
    </div>
  );
}

Button Styles

.toggle .tgl {
  display: none;
}
.toggle .tgl,
.toggle .tgl:after,
.toggle .tgl:before,
.toggle .tgl *,
.toggle .tgl *:after,
.toggle .tgl *:before,
.toggle .tgl + .tglBtn {
  box-sizing: border-box;
}
.toggle .tgl::-moz-selection,
.toggle .tgl:after::-moz-selection,
.toggle .tgl:before::-moz-selection,
.toggle .tgl *::-moz-selection,
.toggle .tgl *:after::-moz-selection,
.toggle .tgl *:before::-moz-selection,
.toggle .tgl + .tglBtn::-moz-selection,
.toggle .tgl::selection,
.toggle .tgl:after::selection,
.toggle .tgl:before::selection,
.toggle .tgl *::selection,
.toggle .tgl *:after::selection,
.toggle .tgl *:before::selection,
.toggle .tgl + .tglBtn::selection {
  background: none;
}
.toggle .tgl + .tglBtn {
  outline: 0;
  display: block;
  width: 4em;
  height: 2em;
  position: relative;
  cursor: pointer;
  -webkit-user-select: none;
  -moz-user-select: none;
  -ms-user-select: none;
  user-select: none;
}
.toggle .tgl + .tglBtn:after,
.toggle .tgl + .tglBtn:before {
  position: relative;
  display: block;
  content: "";
  width: 50%;
  height: 100%;
}
.toggle .tgl + .tglBtn:after {
  left: 0;
}
.toggle .tgl + .tglBtn:before {
  display: none;
}
.toggle .tgl:checked + .tglBtn:after {
  left: 50%;
}

.toggle .tglSkewed + .tglBtn {
  overflow: hidden;
  transform: skew(-10deg);
  -webkit-backface-visibility: hidden;
  backface-visibility: hidden;
  transition: all 0.2s ease;
  font-family: sans-serif;
  background: #222;
}
.toggle .tglSkewed + .tglBtn:after,
.toggle .tglSkewed + .tglBtn:before {
  transform: skew(10deg);
  display: inline-block;
  transition: all 0.2s ease;
  width: 100%;
  text-align: center;
  position: absolute;
  line-height: 2em;
  font-weight: bold;
  color: #fff;
  text-shadow: 0 1px 0 rgba(0, 0, 0, 0.4);
}
.toggle .tglSkewed + .tglBtn:after {
  left: 100%;
  content: attr(data-tg-on);
}
.toggle .tglSkewed + .tglBtn:before {
  left: 0;
  content: attr(data-tg-off);
}
.toggle .tglSkewed + .tglBtn:active {
  background: #222;
}
.toggle .tglSkewed + .tglBtn:active:before {
  left: -10%;
}
.toggle .tglSkewed:checked + .tglBtn {
  background: #ff5e24;
}
.toggle .tglSkewed:checked + .tglBtn:before {
  left: -100%;
}
.toggle .tglSkewed:checked + .tglBtn:after {
  left: 0;
}
.toggle .tglSkewed:checked + .tglBtn:active:after {
  left: 10%;
}

Slider Component

import React, { FormEvent, useRef } from "react";
import { BiChevronLeft, BiChevronRight } from "react-icons/bi";
import styles from "./Slider.module.css";
import Image from "next/image";
import ToggleButton from "./ToggleButton";

type props = {
  beforeImage: string;
  afterImage: string;
  beforeTitle: string;
  afterTitle: string;
  plcOne: string;
  plcTwo: string;
};

export default function SliderComponent({
  beforeImage,
  afterImage,
  beforeTitle,
  afterTitle,
  plcOne,
  plcTwo,
}: props) {
  const container = useRef<HTMLDivElement | null>(null);

  const toggleSlider = function (e: FormEvent<HTMLInputElement>) {
    container.current?.style.setProperty(
      "--position",
      `${e.currentTarget.value}%`
    );
  };

  function togglePercentage(value: string) {
    container.current?.style.setProperty("--position", value);
  }

  // Toggles before or after function based on checkbox state
  const handleToggle = (isChecked: boolean) =>
    isChecked ? togglePercentage("0%") : togglePercentage("100%");

  return (
    <div ref={container} className={styles.container}>
      <div className={styles.imageContainer}>
        <Image
          className={`${styles.sliderImage} ${styles.imageBefore}`}
          src={beforeImage}
          width={1000}
          height={320}
          alt={beforeTitle}
          placeholder="blur"
          blurDataURL={plcOne}
          quality={100}
        />
        <Image
          className={`${styles.sliderImage} ${styles.imageAfter}`}
          width={1000}
          height={320}
          src={afterImage}
          alt={afterTitle}
          placeholder="blur"
          blurDataURL={plcTwo}
          quality={100}
        />
      </div>

      <input
        onInput={toggleSlider}
        type="range"
        min="0"
        max="100"
        defaultValue="50"
        className={styles.slider}
        aria-label="Percentage of image showing"
      />
      <div className={styles.sliderLine} aria-hidden="true"></div>
      <button
        className={styles.sliderButton}
        aria-label="Toggle image difference"
      >
        <BiChevronLeft />
        <BiChevronRight />
      </button>
      <div className="absolute bottom-10 left-1/2 -translate-x-1/2 lg:hidden block">
        <ToggleButton onToggle={handleToggle} />
      </div>
    </div>
  );
}

Slider Styles

.container {
  display: grid;
  place-content: center;
  position: relative;
  max-width: 1200px;
  margin: 0 auto;
  width: 1000px;
  --position: 50%;
}

.imageContainer {
  overflow: hidden;
}

.sliderImage {
  height: 100%;
  object-fit: cover;
  object-position: left;
}

.imageBefore {
  position: absolute;
  inset: 0;
  width: var(--position);
}

.imageAfter {
  object-fit: cover;
  object-position: right;
  inset: 0;
}

.slider {
  position: absolute;
  inset: 0;
  cursor: pointer;
  opacity: 0;
  width: 100%;
  height: 100%;
}

.sliderLine {
  position: absolute;
  inset: 0;
  width: 0.2rem;
  height: 100%;
  background-color: #fff;
  left: var(--position);
  transform: translateX(-50%);
  pointer-events: none;
}

.sliderButton {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 50px;
  height: 50px;
  position: absolute;
  background-color: #fff;
  border: none;
  color: black;
  padding: 0.5rem;
  border-radius: 50%;
  font-size: 1.3rem;
  top: 50%;
  left: var(--position);
  transform: translate(-50%, -50%);
  pointer-events: none;
  box-shadow: 1px 1px 1px hsl(0, 50%, 2%, 0.5);
}

@media (max-width: 1024px) {
  .container {
    max-width: 100%;
  }

  .sliderLine,
  .sliderButton {
    display: none;
  }
}

Use Slider

<SliderComponent
  key={slides._id}
  beforeImage={slides.beforeImage}
  afterImage={slides.afterImage}
  beforeTitle={slides.beforeAlt}
  afterTitle={slides.afterAlt}
  plcOne={slides.beforeLqip}
  plcTwo={slides.afterLqip}
/>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment