// Feed

Latest Examples

52 cards
Frosted glassmorphism button featuring mouse-tracking parallax depth, noise texture, and backdrop blur in CSS and JavaScript.
HTML·CSS·JavaScript ·~5.0 kB

Frosted Glass Parallax Button

The architecture splits the button into two physical planes: a floating glass span faceplate and an underlying ::after substrate. JavaScript captures cursor coordinates to feed --_x-motion and --_y-motion variables into CSS, driving an inverse parallax shift on the substrate via the enter keyframe. The realistic frosted glass aesthetic combines dynamic backdrop-filter: blur() with an embedded SVG feTurbulence fractal noise map.

Level
Intermediate
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Parallax Cursor Tracking · Frosted Glassmorphism · SVG Fractal Noise · Private Custom Properties
Best for
Modern Landing Pages · Dark Mode SaaS · Premium Portfolio Showcases
Loading preview
<button class="glassy-button"><span>Get started</span></button>
<button class="glassy-button"><span>Get started</span></button>

<div class="meta">
  <p>Inspired by <a href="https://dribbble.com/shots/22117329-Glass-Buttons" target="_blank" rel="noopener noreferrer">this Dribbble shot</a> by Patrick Hill</p>
</div>
:root {
  --bg: hsl(221, 37%, 10%);
  --button-text: hsl(200, 60%, 12%);
  --button-1-surface: hsl(197, 88%, 58%);
  --button-2-surface: hsl(41, 96%, 56%);
}

html {
  color-scheme: dark;
  font-family: system-ui, -apple-system, sans-serif;
  font-size: 2rem;
  background: var(--bg);
}

body {
  min-height: 80vh;
  margin: 0;
  display: flex;
  gap: 1rem;
  justify-content: center;
  align-items: center;
}

.glassy-button {
  --_padding: 1rem 1.5rem;
  --_transition-speed: 200ms;
  --_hover-opacity: 0.4;
  --_pressed-opacity: 0.15;
  --_hover-blurriness: 5px;
  --_pressed-blurriness: 10px;
  --_frostiness: 0.3;
  --_hover-offset: 0.5rem;
  --_pressed-offset: 0.25rem;
  --_motion-factor: 0.1;

  outline: 0;
  cursor: pointer;
  font: inherit;
  color: var(--button-text);
  font-weight: 500;
  padding: 0;
  border: 0;
  border-radius: 1rem;
  background-color: transparent;
  position: relative;
}

.glassy-button span {
  display: block;
  overflow: hidden;
  padding: var(--_padding);
  border-radius: inherit;
  background-color: hsl(0 0% 100% / 0);
  -webkit-backdrop-filter: blur(0px);
  backdrop-filter: blur(0px);
  transition: background-color var(--_transition-speed),
    backdrop-filter var(--_transition-speed),
    translate var(--_transition-speed),
    -webkit-backdrop-filter var(--_transition-speed);
}

.glassy-button span::before {
  content: "";
  position: absolute;
  inset: 0;
  z-index: -1;
  background-image: url("data:image/svg+xml, %3Csvg viewBox='0 0 171 171' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='3.74' numOctaves='2' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
  opacity: 0;
  transition: opacity var(--_transition-speed);
}

.glassy-button::after {
  content: "";
  position: absolute;
  z-index: -1;
  inset: 0;
  border-radius: inherit;
  background-color: var(--_surface);
  transition: scale var(--_transition-speed),
    translate var(--_transition-speed);
  animation: exit forwards var(--_transition-speed);
}

.glassy-button:hover span,
.glassy-button:focus-visible span {
  outline: 1px solid hsl(0 0% 100% / 0.7);
  background-color: hsl(0 0% 100% / var(--_hover-opacity));
  -webkit-backdrop-filter: blur(var(--_hover-blurriness));
  backdrop-filter: blur(var(--_hover-blurriness));
  translate: 0 calc(var(--_hover-offset) * -1);
}

.glassy-button:hover span::before,
.glassy-button:focus-visible span::before {
  opacity: var(--_frostiness);
}

.glassy-button:hover::after,
.glassy-button:focus-visible::after {
  scale: 0.95;
  translate: 0 0.125rem;
  animation: enter forwards var(--_transition-speed);
}

.glassy-button:active span {
  -webkit-backdrop-filter: blur(var(--_pressed-blurriness));
  backdrop-filter: blur(var(--_pressed-blurriness));
  background-color: hsl(0 0% 100% / var(--_pressed-opacity));
  translate: 0 calc(var(--_pressed-offset) * -1);
}

.glassy-button:active::after {
  scale: 0.875;
  translate: 0 0.25rem;
}

@keyframes enter {
  from {
    transform: translate(0, 0);
  }
  to {
    transform: translate(
      calc(var(--_x-motion) * var(--_motion-factor) * -1),
      calc(var(--_y-motion) * var(--_motion-factor) * -1)
    );
  }
}

@keyframes exit {
  from {
    transform: translate(
      calc(var(--_x-motion) * var(--_motion-factor) * -1),
      calc(var(--_y-motion) * var(--_motion-factor) * -1)
    );
  }
  to {
    transform: translate(0, 0);
  }
}

.glassy-button:nth-child(1) {
  --_surface: var(--button-1-surface);
}

.glassy-button:nth-child(2) {
  --_surface: var(--button-2-surface);
}

.meta {
  font-size: 0.625rem;
  text-align: center;
  position: fixed;
  inset: auto 2rem 2rem;
}

.meta a {
  color: inherit;
}
const glassyButtons = document.querySelectorAll(".glassy-button");

glassyButtons.forEach((button) => {
  button.addEventListener("mousemove", (event) => {
    const centerX = button.offsetWidth / 2;
    const centerY = button.offsetHeight / 2;

    const offsetX = event.offsetX - centerX;
    const offsetY = event.offsetY - centerY;

    button.style.setProperty("--_x-motion", `${offsetX}px`);
    button.style.setProperty("--_y-motion", `${offsetY}px`);
  });
});
  • Palette & Base Surfaces: Assign primary substrate hues via :root tokens --button-1-surface: hsl(197, 88%, 58%) and --button-2-surface: hsl(41, 96%, 56%). Modify label color via --button-text: hsl(200, 60%, 12%).
  • Glass Texture & Frosting: Tune the procedural noise opacity inside .glassy-button via --_frostiness: 0.3. Adjust frosted glass blur intensity via --_hover-blurriness: 5px and click blur via --_pressed-blurriness: 10px.
  • Elevation & Parallax Motion: Scale cursor-driven parallax travel using --_motion-factor: 0.1 (values 0 to 1). Modify physical hover lift distance via --_hover-offset: 0.5rem and active press offset --_pressed-offset: 0.25rem.
  • Geometry & Transition Speed: Alter interior spacing via --_padding: 1rem 1.5rem, capsule curvature via border-radius: 1rem, and interaction pacing using --_transition-speed: 200ms.
Glassmorphism pill buttons in CSS with frosted glass blur, diagonal light sweep, and expandable neon backplate glows.
HTML·CSS ·~3.3 kB

Glowing Neon Glassmorphism Buttons

The frosted glass aesthetic is built using backdrop-filter: blur(15px) paired with a translucent white fill and delicate boundary borders. Dynamic illumination radiates from behind via ::before and ::after neon tabs: upon hover, these backplates expand to 80% width, diffusing multi-tiered box-shadow glows through the glass layer. The interaction is completed by a skewed skewX(45deg) glint sweeping across the pill face.

Level
Beginner
Requires
Google Fonts (Poppins, Roboto)
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Frosted Glass Blur · Neon Backplate Glow · Diagonal Light Sweep · Letter-Spacing Expand
Best for
Creative Portfolios · Futuristic Landing Pages · Dark Mode Dashboards
Loading preview
<div class="container">
  <div class="btn"><a href="#">Read more 1</a></div>
  <div class="btn"><a href="#">Read more 2</a></div>
  <div class="btn"><a href="#">Read more 3</a></div>
</div>
* {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
  font-family: "Roboto", sans-serif;
}

body {
  position: relative;
  display: flex;
  justify-content: center;
  align-items: center;
  min-height: 100vh;
  background: linear-gradient(to bottom, #5d326c, #350048);
}

.container {
  width: 1000px;
  display: flex;
  flex-wrap: wrap;
  justify-content: space-around;
  gap: 20px;
}

.container .btn {
  position: relative;
  top: 0;
  left: 0;
  width: 250px;
  height: 50px;
  margin: 0;
  display: flex;
  justify-content: center;
  align-items: center;
}

.container .btn a {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  background: rgba(255, 255, 255, 0.05);
  box-shadow: 0 15px 15px rgba(0, 0, 0, 0.3);
  border-bottom: 1px solid rgba(255, 255, 255, 0.1);
  border-top: 1px solid rgba(255, 255, 255, 0.1);
  border-radius: 30px;
  padding: 10px;
  letter-spacing: 1px;
  text-decoration: none;
  overflow: hidden;
  color: #fff;
  font-weight: 400;
  z-index: 1;
  transition: 0.5s;
  backdrop-filter: blur(15px);
  -webkit-backdrop-filter: blur(15px);
}

.container .btn:hover a {
  letter-spacing: 3px;
}

.container .btn a::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  width: 50%;
  height: 100%;
  background: linear-gradient(to left, rgba(255, 255, 255, 0.15), transparent);
  transform: skewX(45deg) translate(0);
  transition: 0.5s;
  filter: blur(0px);
}

.container .btn:hover a::before {
  transform: skewX(45deg) translate(200px);
}

.container .btn::before {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  bottom: -5px;
  width: 30px;
  height: 10px;
  background: #f00;
  border-radius: 10px;
  transition: 0.5s;
  transition-delay: 0.2s;
}

.container .btn:hover::before {
  bottom: 0;
  height: 50%;
  width: 80%;
  border-radius: 30px;
}

.container .btn::after {
  content: "";
  position: absolute;
  left: 50%;
  transform: translateX(-50%);
  top: -5px;
  width: 30px;
  height: 10px;
  background: #f00;
  border-radius: 10px;
  transition: 0.5s;
  transition-delay: 0.2s;
}

.container .btn:hover::after {
  top: 0;
  height: 50%;
  width: 80%;
  border-radius: 30px;
}

.container .btn:nth-child(1)::before,
.container .btn:nth-child(1)::after {
  background: #ff1f71;
  box-shadow: 0 0 5px #ff1f71, 0 0 15px #ff1f71, 0 0 30px #ff1f71,
    0 0 60px #ff1f71;
}

.container .btn:nth-child(2)::before,
.container .btn:nth-child(2)::after {
  background: #2db2ff;
  box-shadow: 0 0 5px #2db2ff, 0 0 15px #2db2ff, 0 0 30px #2db2ff,
    0 0 60px #2db2ff;
}

.container .btn:nth-child(3)::before,
.container .btn:nth-child(3)::after {
  background: #1eff45;
  box-shadow: 0 0 5px #1eff45, 0 0 15px #1eff45, 0 0 30px #1eff45,
    0 0 60px #1eff45;
}
  • Neon Glow Palettes: Customize the radiant illumination colors and shadow radii across children using .container .btn:nth-child(n)::before and ::after (child 1: #ff1f71, child 2: #2db2ff, child 3: #1eff45).
  • Glass Frosting & Translucency: Adjust frosted blur density under .container .btn a via backdrop-filter: blur(15px) and base transparency through background: rgba(255, 255, 255, 0.05).
  • Light Sweep Glint: Modify the skewed reflection pass inside .container .btn a::before (background: linear-gradient(to left, rgba(255, 255, 255, 0.15), transparent)) and adjust hover traverse distance via transform: skewX(45deg) translate(200px).
  • Button Footprint & Typography: Set capsule proportions in .container .btn (width: 250px, height: 50px) and pill curvature in .container .btn a (border-radius: 30px). Tweak kinetic text tracking on hover via .container .btn:hover a (letter-spacing: 3px).
Liquid glass lens pill button in CSS with refractive bevel shadows, contrast layer, and circular SVG icon button.
SVG·HTML·CSS ·~4.2 kB

Liquid Glass Lens Button

The liquid glass lens illusion is engineered by wrapping the component in an .fx-layer featuring filter: contrast(3), which clamps soft blurs into sharp optical caustics. Internal volume is modeled using a 6-part box-shadow array with dual opposing specular highlights, while blurred :before and :after pseudo-elements emulate ambient light refraction through thick curved crystal. A floating circular badge anchors the action.

Level
Intermediate
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Refractive Lens Shading · High-Contrast Layer · Multi-Pass Inset Shadows · Blurred Rim Glare
Best for
Minimalist Portfolios · Apple-Style Landing Pages · Modern UI Kits
Loading preview
<div class="container fx-layer">
  <div class="top">
    <div class="box start-btn" style="--w: 260px; --h: 75px; --tr: 15%;">
      <span class="text">Get started</span>
      <div class="btn-icon">
        <svg
          class="svg"
          viewBox="0 0 1024 1024"
          version="1.1"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            d="M779.180132 473.232045 322.354755 16.406668c-21.413706-21.413706-56.121182-21.413706-77.534887 0-21.413706 21.413706-21.413706 56.122205 0 77.534887l418.057421 418.057421L244.819868 930.057421c-21.413706 21.413706-21.413706 56.122205 0 77.534887 10.706853 10.706853 24.759917 16.059767 38.767955 16.059767s28.061103-5.353938 38.767955-16.059767L779.180132 550.767955C800.593837 529.35425 800.593837 494.64575 779.180132 473.232045z"
          ></path>
        </svg>
      </div>
      <div class="circle-overlay"></div>
    </div>
  </div>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background: radial-gradient(circle at center, #f5f5f7, #e2e2e8);
}

.container {
  display: flex;
  flex-direction: column;
  gap: 5rem;
}

.container .top {
  display: flex;
  gap: 5rem;
}

.container .bottom {
  display: flex;
  align-items: center;
  gap: 2rem;
}

.fx-layer {
  filter: contrast(3);
}

/* General button box */
.box {
  --w: 100px;
  --h: 100px;
  --r: 9999px;
  --tr: 25%;
  z-index: 500;
  position: relative;
  width: var(--w);
  height: var(--h);
  display: flex;
  justify-content: center;
  align-items: center;
  border-radius: var(--r);
  border: 1px double rgba(51, 51, 51, 0.08);

  box-shadow:
    inset 2px -2px 1px -1px rgba(255, 255, 255, 0.9),
    inset -2px 2px 1px -1px rgba(255, 255, 255, 0.9),
    inset 6px -6px 1px -6px rgba(255, 255, 255, 0.55),
    inset -6px 6px 1px -6px rgba(255, 255, 255, 0.55),
    inset 0 0 2px rgba(0, 0, 0, 0.8),
    0 4px 8px rgba(0, 0, 0, 0.2);

  background: rgba(0, 0, 0, 0.02);
  backdrop-filter: blur(2px);
  -webkit-backdrop-filter: blur(2px);
  cursor: pointer;
  filter: brightness(0.9);
  user-select: none;
}

.box .svg {
  width: 28px;
  filter: drop-shadow(0 25px 3px rgba(102, 102, 102, 0.2));
}

.box .text {
  filter: drop-shadow(0 25px 3px rgba(102, 102, 102, 0.15));
}

.box:before {
  content: "";
  position: absolute;
  z-index: 1;
  top: 35%;
  left: 50%;
  transform: translateX(-50%);
  width: calc(var(--w) - 16px);
  height: calc(var(--h) - 16px);
  border-radius: var(--r);
  border: 1px solid rgba(0, 0, 0, 0.9);
  filter: blur(8px);
}

.box:after {
  z-index: 501;
  content: "";
  position: absolute;
  width: var(--w);
  height: var(--h);
  border-radius: var(--r);
  filter: blur(7px);
  background: linear-gradient(
    45deg,
    rgba(255, 255, 255, 0.8) 0%,
    transparent var(--tr),
    transparent calc(100% - var(--tr)),
    rgba(255, 255, 255, 0.8) 100%
  );
}

.box .circle-overlay {
  position: absolute;
  width: calc(var(--w) - 9px);
  height: calc(var(--h) - 9px);
  border: 1px solid rgba(255, 255, 255, 0.2);
  border-radius: var(--r);
  filter: blur(1px);
}

.start-btn {
  padding: 0 0.8rem;
  display: flex;
  align-items: center;
  gap: 1rem;
  transition: 0.25s;
}

.start-btn .text {
  font-size: 25px;
  font-family: system-ui, -apple-system, sans-serif;
  color: #3e3e3e;
}

.start-btn .btn-icon {
  width: 40px;
  height: 40px;
  display: flex;
  justify-content: center;
  align-items: center;
  background: #3e3e3e;
  border-radius: 50%;
  box-shadow: 0 0 6px rgba(0, 0, 0, 0.3);
  transition: 0.25s;
}

.start-btn .btn-icon .svg {
  width: 16px;
  fill: #f5f5f5;
}

.start-btn:hover {
  background: rgba(0, 0, 0, 0);
  transform: translateY(-3px);
}

.start-btn:hover .btn-icon {
  transform: scale(1.1);
}

.start-btn:active {
  transform: scale(0.94);
}

.start-btn:active .text {
  color: #000;
}

.start-btn:active .btn-icon {
  transform: scale(0.94);
}
  • Capsule Geometry & Ratios: Set dimensions and corner radii inline or via .box defaults --w: 100px (instance 260px), --h: 100px (instance 75px), --r: 9999px, and glare transparency band threshold --tr: 25% (instance 15%).
  • Lens Refraction & Contrast: Adjust optical punch under .fx-layer (filter: contrast(3)). Tune frosted diffusion on .box via backdrop-filter: blur(2px) and ambient base fill background: rgba(0, 0, 0, 0.02).
  • Specular Bevels & Inset Shadows: Customize the 6-layer illumination in .box box-shadow (inset white highlights rgba(255, 255, 255, 0.9), inner core shade rgba(0, 0, 0, 0.8), and outer drop shadow rgba(0, 0, 0, 0.2)).
  • Glare Pseudo-Elements: Refine the internal dark caustic shadow in .box:before (filter: blur(8px)) and perimeter diagonal sheen in .box:after (filter: blur(7px)).
  • Action Badge & Icon: Modify the trailing circle icon via .start-btn .btn-icon (width: 40px; height: 40px; background: #3e3e3e) and SVG fill .start-btn .btn-icon .svg (fill: #f5f5f5).
Stack of spherical glowing orb buttons in CSS and SVG featuring bespoke vector micro-interactions and sliding unblurred labels.
SVG·HTML·CSS ·~6.5 kB

Luminous Glowing Orb Buttons

The spherical volume of these orbs is sculpted through radial gradients and triple-pass box-shadow halos tied to a custom --glow-color variable. Each button executes bespoke vector kinematics: the plus rotates 180°, the export arrow cycles diagonally via @keyframes arrowFly, and the checkmark draws via stroke-dashoffset. On hover, a skewed glint sweeps across the orb while the label glides outward, transitioning filter: blur(0px).

Level
Intermediate
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Luminous Orb Glow · Bespoke Icon Animations · Shimmer Light Sweep · Unblurring Label Reveal
Best for
Dark Themed Dashboards · Floating Action Menus · Creative Web Portfolios
Loading preview
<div class="button-stack">
  <div class="btn-wrapper">
    <button class="orb-btn btn-teal" aria-label="Add">
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <line x1="12" y1="4" x2="12" y2="20"></line>
        <line x1="4" y1="12" x2="20" y2="12"></line>
      </svg>
    </button>
    <span class="btn-label">Create</span>
  </div>

  <div class="btn-wrapper">
    <button class="orb-btn btn-silver" aria-label="Open or Export">
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M7 17 L17 7 M17 7 L9 7 M17 7 L17 15"></path>
      </svg>
    </button>
    <span class="btn-label">Export</span>
  </div>

  <div class="btn-wrapper">
    <button class="orb-btn btn-mauve" aria-label="Confirm">
      <svg viewBox="0 0 24 24" aria-hidden="true">
        <path d="M5 13 L10 17.5 L19 7"></path>
      </svg>
    </button>
    <span class="btn-label">Confirm</span>
  </div>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #0b0f19;
}

.button-stack,
.button-stack * {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
    "Helvetica Neue", Arial, sans-serif;
}

.button-stack {
  display: flex;
  flex-direction: column;
  gap: 2.5em;
  padding: 2.5em;
  align-items: center;
}

.btn-wrapper {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  z-index: 1;
  opacity: 0;
  animation: floatIn 0.8s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.btn-wrapper:nth-child(1) {
  animation-delay: 0.1s;
}
.btn-wrapper:nth-child(2) {
  animation-delay: 0.25s;
}
.btn-wrapper:nth-child(3) {
  animation-delay: 0.4s;
}

@keyframes floatIn {
  0% {
    opacity: 0;
    transform: translateY(30px) scale(0.9);
  }
  100% {
    opacity: 1;
    transform: translateY(0) scale(1);
  }
}

.btn-wrapper:hover {
  z-index: 10;
}

.orb-btn {
  position: relative;
  width: 5em;
  height: 5em;
  border-radius: 50%;
  display: flex;
  justify-content: center;
  align-items: center;
  cursor: pointer;
  border: none;
  outline: none;
  background: radial-gradient(
    circle at 50% 50%,
    rgba(0, 0, 0, 0.1) 0%,
    rgba(var(--glow-color), 0.08) 50%,
    rgba(var(--glow-color), 0.35) 100%
  );
  box-shadow:
    inset 0 0 25px 2px rgba(var(--glow-color), 0.35),
    inset 0 0 8px 1px rgba(var(--glow-color), 0.6),
    0 0 35px 2px rgba(var(--glow-color), 0.25);
  transition:
    transform 0.4s cubic-bezier(0.2, 0.8, 0.2, 1),
    box-shadow 0.4s cubic-bezier(0.2, 0.8, 0.2, 1);
  -webkit-tap-highlight-color: transparent;
  overflow: hidden;
}

.orb-btn::before {
  content: "";
  position: absolute;
  inset: 0;
  border-radius: 50%;
  background: linear-gradient(
    145deg,
    rgba(255, 255, 255, 0.25) 0%,
    transparent 40%,
    rgba(0, 0, 0, 0.5) 100%
  );
  pointer-events: none;
  z-index: 0;
  box-shadow: inset 1px 2px 4px rgba(255, 255, 255, 0.4);
}

.orb-btn::after {
  content: "";
  position: absolute;
  top: 0;
  left: -150%;
  width: 50%;
  height: 100%;
  background: linear-gradient(
    to right,
    rgba(255, 255, 255, 0) 0%,
    rgba(255, 255, 255, 0.3) 50%,
    rgba(255, 255, 255, 0) 100%
  );
  transform: skewX(-25deg);
  z-index: 1;
  pointer-events: none;
}

.btn-wrapper:hover .orb-btn::after {
  animation: shimmerSweep 0.8s ease-out forwards;
}

@keyframes shimmerSweep {
  0% {
    left: -150%;
  }
  100% {
    left: 200%;
  }
}

.btn-wrapper:hover .orb-btn {
  transform: scale(1.06);
  box-shadow:
    inset 0 0 30px 4px rgba(var(--glow-color), 0.45),
    inset 0 0 12px 2px rgba(var(--glow-color), 0.7),
    0 0 50px 8px rgba(var(--glow-color), 0.35);
}

.btn-wrapper:active .orb-btn {
  transform: scale(0.92);
  box-shadow:
    inset 0 0 35px 5px rgba(var(--glow-color), 0.8),
    inset 0 0 15px 4px rgba(255, 255, 255, 0.7),
    0 0 30px 5px rgba(var(--glow-color), 0.4);
}

.orb-btn svg {
  width: 2.125em;
  height: 2.125em;
  stroke: #ffffff;
  stroke-width: 1;
  fill: none;
  stroke-linecap: round;
  stroke-linejoin: round;
  z-index: 2;
  filter: drop-shadow(0 0 3px rgba(255, 255, 255, 0.3));
  transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

.btn-wrapper:hover .btn-teal svg {
  transform: scale(1.15) rotate(180deg);
}

@keyframes arrowFly {
  0% {
    transform: translate(0, 0);
    opacity: 1;
  }
  40% {
    transform: translate(20px, -20px);
    opacity: 0;
  }
  41% {
    transform: translate(-20px, 20px);
    opacity: 0;
  }
  100% {
    transform: translate(3px, -3px);
    opacity: 1;
  }
}

.btn-wrapper:hover .btn-silver svg {
  animation: arrowFly 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.btn-mauve svg path {
  stroke-dasharray: 40;
  stroke-dashoffset: 0;
  transition: stroke-dashoffset 0.6s cubic-bezier(0.16, 1, 0.3, 1);
}

@keyframes drawCheck {
  0% {
    stroke-dashoffset: 40;
  }
  100% {
    stroke-dashoffset: 0;
  }
}

.btn-wrapper:hover .btn-mauve svg {
  transform: scale(1.25);
}

.btn-wrapper:hover .btn-mauve svg path {
  animation: drawCheck 0.6s cubic-bezier(0.16, 1, 0.3, 1) forwards;
}

.btn-label {
  position: absolute;
  left: 100%;
  margin-left: -0.3125em;
  opacity: 0;
  color: rgba(255, 255, 255, 0.9);
  font-size: 14px;
  font-weight: 500;
  letter-spacing: 3px;
  text-transform: uppercase;
  white-space: nowrap;
  pointer-events: none;
  transition: all 0.6s cubic-bezier(0.16, 1, 0.3, 1);
  transform: translateX(-10px) scale(0.95);
  filter: blur(8px);
  text-shadow: 0 4px 15px rgba(0, 0, 0, 0.6);
}

.btn-wrapper:hover .btn-label {
  margin-left: 2.5em;
  opacity: 1;
  transform: translateX(0) scale(1);
  filter: blur(0px);
  letter-spacing: 5px;
}

.btn-teal {
  --glow-color: 52, 142, 150;
}

.btn-silver {
  --glow-color: 180, 195, 210;
}

.btn-mauve {
  --glow-color: 188, 108, 148;
}
  • RGB Glow Palettes: Customize the radiant chromatic core per orb by setting raw RGB triplets via --glow-color on .btn-teal (52, 142, 150), .btn-silver (180, 195, 210), and .btn-mauve (188, 108, 148).
  • Orb Geometry & Spherical Shading: Scale the button sphere in .orb-btn (width: 5em; height: 5em; border-radius: 50%). Fine-tune ambient light reflection angles inside .orb-btn::before (background: linear-gradient(145deg, rgba(255, 255, 255, 0.25) 0%, transparent 40%, rgba(0, 0, 0, 0.5) 100%)).
  • Shimmer Sweep & Glow Intensity: Modify glint transit velocity via .btn-wrapper:hover .orb-btn::after (animation: shimmerSweep 0.8s ease-out forwards). Adjust resting and hover multi-pass halos inside .orb-btn and .btn-wrapper:hover .orb-btn box-shadow.
  • Label Drift & Unblur: Adjust flyout clearance and blur clearing in .btn-wrapper:hover .btn-label (margin-left: 2.5em; filter: blur(0px); letter-spacing: 5px).
  • SVG Icon Interactions: Adjust the vector scale and rotation timing on teal (transform: scale(1.15) rotate(180deg)), silver arrow exit path (@keyframes arrowFly), and mauve checkmark stroke dash (stroke-dasharray: 40; stroke-dashoffset: 0).
Skeuomorphic steampunk button in CSS with brushed bronze bezel, dark bottle-glass lens, and pulsating amber filament glow.
HTML·CSS ·~5.7 kB

Skeuomorphic Steampunk Push Button

The button constructs a 7-tier skeuomorphic stack: outer chassis, brushed bronze bezel, dark bottle-glass lens, wire mesh, glowing filament, and specular glare. The bronze frame uses a 135° linear gradient with 4-way inset box-shadow passes to carve physical chamfers. An internal .amber-glow powered by mix-blend-mode: color-dodge pulses on hover, while active clicks ignite a white-hot core with mechanical tactile depression.

Level
Intermediate
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Brushed Bronze Bezel · Color-Dodge Filament · Curved Glass Specular · Skeuomorphic Mesh Pattern
Best for
Steampunk Game UIs · Thematic Escape Rooms · Industrial Sci-Fi Interfaces
Loading preview
<button class="steampunk-btn" aria-label="Button 1">
  <span class="btn-frame">
    <span class="btn-glass">
      <span class="glass-pattern"></span>
      <span class="amber-glow"></span>
      <span class="glass-reflection"></span>
      <span class="btn-text">✦</span>
    </span>
  </span>
</button>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #141312;
}

.steampunk-btn {
  --bronze-dark: #2b2927;
  --bronze-mid: #63431f;
  --bronze-light: #856233;
  --bronze-highlight: #fce3a1;
  --glass-green-deep: #281d08;
  --glass-green-bright: #544212;
  --amber-light: #ffc95e;
  --amber-glow: #fa9a6a;

  position: relative;
  width: 110px;
  height: 110px;
  padding: 0;
  border: none;
  background: transparent;
  cursor: pointer;
  outline: none;
  border-radius: 22px;

  box-shadow:
    0 15px 25px rgba(0, 0, 0, 0.8),
    0 5px 10px rgba(0, 0, 0, 0.5),
    inset 0 1px 1px rgba(255, 255, 255, 0.2);

  transition:
    transform 0.15s cubic-bezier(0.4, 0, 0.2, 1),
    box-shadow 0.15s ease;
  -webkit-tap-highlight-color: transparent;
}

/* Bronze frame */
.steampunk-btn .btn-frame {
  display: flex;
  align-items: center;
  justify-content: center;
  width: 100%;
  height: 100%;
  border-radius: 22px;
  padding: 6px;
  box-sizing: border-box;
  background: linear-gradient(
    135deg,
    var(--bronze-highlight) 0%,
    var(--bronze-light) 20%,
    var(--bronze-mid) 45%,
    var(--bronze-dark) 70%,
    var(--bronze-light) 100%
  );
  box-shadow:
    inset 0 2px 3px rgba(255, 255, 255, 0.6),
    inset 0 -3px 5px rgba(0, 0, 0, 0.8),
    inset 2px 2px 4px var(--bronze-highlight),
    inset -2px -2px 4px #000;
  transition: box-shadow 0.1s ease;
}

/* Glass lens */
.steampunk-btn .btn-glass {
  position: relative;
  width: 100%;
  height: 100%;
  border-radius: 14px;
  overflow: hidden;
  display: flex;
  align-items: center;
  justify-content: center;
  background: radial-gradient(
    circle at 50% 50%,
    var(--glass-green-bright) 0%,
    var(--glass-green-deep) 85%
  );
  box-shadow:
    inset 0 0 8px rgba(0, 0, 0, 0.9),
    inset 0 0 15px rgba(0, 0, 0, 0.7),
    0 0 2px rgba(0, 0, 0, 0.5);
  transition:
    background 0.1s ease,
    box-shadow 0.1s ease;
}

/* Mesh pattern */
.steampunk-btn .glass-pattern {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0.25;
  background-image: radial-gradient(
      var(--bronze-light) 0.5px,
      transparent 0.5px
    ),
    linear-gradient(45deg, transparent 49%, rgb(0, 0, 0) 50%, transparent 51%);
  background-size:
    8px 8px,
    6px 6px;
  mix-blend-mode: overlay;
  pointer-events: none;
}

/* Amber light */
.steampunk-btn .amber-glow {
  position: absolute;
  width: 70px;
  height: 70px;
  border-radius: 50%;
  background: radial-gradient(
    circle,
    var(--amber-light) 0%,
    var(--amber-glow) 40%,
    transparent 75%
  );
  opacity: 0.6;
  filter: blur(6px);
  mix-blend-mode: color-dodge;
  transition:
    opacity 0.3s ease,
    transform 0.3s ease,
    filter 0.3s ease;
}

/* Glass reflection */
.steampunk-btn .glass-reflection {
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  border-radius: 14px;
  pointer-events: none;
}

.steampunk-btn .glass-reflection::before {
  content: "";
  position: absolute;
  top: 3px;
  left: 6px;
  right: 6px;
  height: 40%;
  background: linear-gradient(
    180deg,
    rgba(255, 255, 255, 0.4) 0%,
    rgba(255, 255, 255, 0.05) 80%,
    transparent 100%
  );
  border-radius: 10px 10px 80% 80% / 8px 8px 15px 15px;
}

/* Text */
.steampunk-btn .btn-text {
  position: relative;
  z-index: 2;
  font-family: "Georgia", "Times New Roman", serif;
  font-size: 3.1rem;
  font-weight: 500;
  color: var(--bronze-highlight);
  background: linear-gradient(
    180deg,
    #ffffff 0%,
    var(--bronze-highlight) 50%,
    var(--bronze-mid) 100%
  );
  -webkit-background-clip: text;
  -webkit-text-fill-color: transparent;
  text-shadow:
    0 2px 4px rgba(0, 0, 0, 0.9),
    0 0 10px var(--amber-light);
  filter: drop-shadow(0 2px 2px rgba(0, 0, 0, 0.8));
  user-select: none;
  transition:
    text-shadow 0.3s ease,
    filter 0.3s ease;
}

/* Hover */
.steampunk-btn:hover {
  transform: translateY(-2px);
  box-shadow:
    0 20px 30px rgba(0, 0, 0, 0.85),
    0 0 15px rgba(255, 170, 0, 0.3);
}

.steampunk-btn:hover .amber-glow {
  opacity: 0.95;
  transform: scale(1.15);
  filter: blur(4px);
  animation: pulse-amber 1s ease-in-out infinite;
}

.steampunk-btn:hover .btn-text {
  text-shadow:
    0 2px 4px rgba(0, 0, 0, 0.9),
    0 0 16px var(--amber-light);
}

/* Active */
.steampunk-btn:active {
  transform: translateY(1px);
  box-shadow:
    0 5px 10px rgba(0, 0, 0, 0.9),
    inset 0 2px 5px rgba(0, 0, 0, 0.8),
    0 0 35px rgba(252, 153, 15, 0.473),
    0 0 85px rgba(252, 153, 15, 0.29),
    0 0 165px rgba(255, 198, 85, 0.564);
}

.steampunk-btn:active .amber-glow {
  animation: none;
  opacity: 0.9;
  transform: scale(1.3);
  filter: blur(15px);
  background: radial-gradient(
    circle,
    #ffffff 0%,
    var(--amber-light) 30%,
    var(--amber-glow) 60%,
    transparent 80%
  );
}

.steampunk-btn:active .btn-glass {
  background: radial-gradient(
    circle at 50% 50%,
    var(--glass-green-bright) 0%,
    #4a3b10 60%,
    var(--glass-green-deep) 100%
  );
  box-shadow:
    inset 0 0 10px rgba(0, 0, 0, 1),
    inset 0 0 20px rgba(250, 154, 106, 0.2),
    0 0 3px rgba(0, 0, 0, 0.7);
}

@keyframes pulse-amber {
  0%,
  100% {
    opacity: 0.5;
    transform: scale(1);
    filter: blur(6px);
  }
  50% {
    opacity: 0.9;
    transform: scale(1.12);
    filter: blur(4px);
  }
}
  • Metallic Bronze Framing: Customize bezel metallics via custom properties in .steampunk-btn: --bronze-dark: #2b2927, --bronze-mid: #63431f, --bronze-light: #856233, and highlight peak --bronze-highlight: #fce3a1.
  • Glass Lens & Filament Luminescence: Modify substrate glass tones via --glass-green-deep: #281d08 and --glass-green-bright: #544212. Re-tone internal filament radiation using --amber-light: #ffc95e and --amber-glow: #fa9a6a.
  • Chassis Footprint & Bezel Stroke: Scale button dimensions in .steampunk-btn (width: 110px, height: 110px, border-radius: 22px). Adjust bronze rim thickness via .steampunk-btn .btn-frame (padding: 6px).
  • Mesh Grid Density & Specular Curve: Adjust internal screen wire scale in .glass-pattern (background-size: 8px 8px, 6px 6px). Tweak curvature ratios of the top lens highlight in .glass-reflection::before (border-radius: 10px 10px 80% 80% / 8px 8px 15px 15px).
  • Filament Pulse Rate: Control filament breathing frequency in .steampunk-btn:hover .amber-glow (animation: pulse-amber 1s ease-in-out infinite).
Vector design tool bounding box button in CSS with corner resize handles, iris collapse animation, and text reveal.
HTML·CSS ·~5.0 kB

Vector Bounding Box Button

The component mimics vector design software: a bounding box framed by four corner anchor nodes .point pinned by negative offsets calc(var(--point-size) * -0.5). Hovering triggers an iris squeeze via @keyframes frame collapsing horizontal edges to inset: 0 50% while smoothly crossfading the name into an email address. Clicking or focusing engages :focus::before to float an “Address copied” toast upward purely using CSS.

Level
Intermediate
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Vector Handle Aesthetics · Iris Squeeze Keyframe · Text Swap Animation · Pseudo Floating Toast
Best for
Design Portfolios · Developer Resumes · Creative Agency Contact CTAs
Loading preview
<div class="btn-wrapper">
  <button class="btn">
    <span class="frame">
      <span class="point top left"></span>
      <span class="point top right"></span>
      <span class="point bottom left"></span>
      <span class="point bottom right"></span>
    </span>
    <span class="txt-box">
      <span class="txt">John Doe</span>
      <span class="txt">[email protected]</span>
    </span>
  </button>
  <div class="txt-secondary" id="hint1">Hover to reveal address</div>
  <div class="txt-secondary" id="hint2">Click to copy</div>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #f0f4f8;
}

.btn-wrapper {
  --color: #b5faff31;
  --txt-color: #283a3b;
  --txt-color-2: #283a3b;
  --point-size: 8px;
  --point-color: #ffffff;
  --line-color: #00000015;
  --line-style: solid;
  --line-weight: 1px;
  --anim-speed: 1s;

  position: relative;
  display: grid;
  place-items: center;
  padding: 1.5rem 5rem;
  min-width: 160px;
  min-height: 48px;

  user-select: none;
}

.txt-secondary {
  position: absolute;
  bottom: -2rem;
  font: 400 0.75em "Inter", sans-serif;
  color: #0006;
  font-style: italic;
  will-change: opacity;
  transition: opacity calc(var(--anim-speed, 1s) * 0.5) ease;
  opacity: 1;
}

#hint2 {
  opacity: 0;
}

.btn {
  filter: drop-shadow(0 6px 2px #00000055) drop-shadow(0 14px 4px #00000055)
    drop-shadow(0 32px 8px #00000055) drop-shadow(0 64px 16px #00000055);
  position: absolute;
  display: flex;
  align-items: center;
  justify-content: center;
  cursor: pointer;
  border: none;
  background: none;
  width: 100%;
  height: 100%;
}

.txt-box {
  position: absolute;
  display: grid;
  place-items: center;
  text-wrap: nowrap;
  inset: 0 0%;
  overflow: clip;
  will-change: inset, filter;
  transition: filter 0.25s ease;
  animation: frame-half calc(var(--anim-speed, 1s) * 0.5) forwards;
}

.txt-box::after {
  content: "";
  position: absolute;
  inset: var(--point-size, 8px);
  background: repeating-linear-gradient(45deg, #3f87a6, #ebf8e1 15%, #fff 20%);
  mix-blend-mode: hard-light;
  background-size: 440%;
  transition: background-size 0.4s ease-in;
  filter: blur(1px);
  z-index: 3;
  opacity: 0.1;
}

.txt {
  position: absolute;
  padding: 1rem 2rem;
  z-index: 2;
  font: 500 1.3em "Inter", sans-serif;
  color: var(--txt-color, #15104c);
  will-change: opacity, display, text-shadow;
  text-shadow:
    0 -1px 1px #ffffff60,
    0 2px 1px #00000015,
    0 4px 2px #00000015,
    0 8px 4px #00000015,
    0 16px 8px #00000015;
}

.txt:last-child {
  color: var(--txt-color-2, #15104c);
  opacity: 0;
  animation: none;
}

.frame {
  position: absolute;
  inset: 0 0%;
  z-index: 1;
  border: var(--line-style, solid) var(--line-weight, 1px) var(--line-color, #000000);
  background-color: var(--color, #f9d323);
  transition-delay: calc(var(--anim-speed, 1s) * 0.5);
  box-shadow: inset 0 1px 4px 1px #fff5;
  animation: frame-half calc(var(--anim-speed, 1s) * 0.5) forwards;
}

.point {
  position: absolute;
  box-sizing: border-box;
  width: var(--point-size, 8px);
  aspect-ratio: 1;
  border-radius: 25%;
  border: solid var(--line-weight, 1px) var(--line-color, #000000);
  background-color: var(--point-color, #fff);
  background-image: radial-gradient(circle at 50% 120%, #0005, #ffff);
}

.point.top {
  top: calc(var(--point-size, 8px) * -0.5);
}

.point.bottom {
  bottom: calc(var(--point-size, 8px) * -0.5);
}

.point.left {
  left: calc(var(--point-size, 8px) * -0.5);
}

.point.right {
  right: calc(var(--point-size, 8px) * -0.5);
}

.btn:hover .txt {
  animation: txt-out calc(var(--anim-speed, 1s) * 0.5) forwards;
}

.btn:hover .txt:last-child {
  animation: txt-in calc(var(--anim-speed, 1s) * 0.5) forwards;
}

.btn:hover .txt-box {
  animation: frame var(--anim-speed, 1s) ease;
}

.btn:hover .txt-box::after {
  background-size: 700%;
}

.btn:hover .frame {
  animation: frame var(--anim-speed, 1s) ease;
}

.btn:hover ~ #hint1 {
  opacity: 0;
}

.btn:hover ~ #hint2 {
  opacity: 1;
}

@keyframes txt-in {
  90% {
    opacity: 0;
  }
  100% {
    opacity: 1;
  }
}

@keyframes txt-out {
  50% {
    opacity: 1;
  }
  100% {
    opacity: 0;
  }
}

@keyframes frame-half {
  0% {
    inset: 0 50%;
  }
  100% {
    inset: 0 0%;
  }
}

@keyframes frame {
  50% {
    inset: 0 50%;
  }
}

.btn::before {
  content: "Address copied";
  position: absolute;
  inset: 0;
  font: 400 1em "Inter", sans-serif;
  letter-spacing: 0.03em;
  color: #000a;
  z-index: -1;
  filter: blur(16px);
  opacity: 0;
}

.btn:active .txt-box {
  filter: contrast(1.4) brightness(1.4);
}

.btn:focus::before {
  animation: appear calc(var(--anim-speed, 1s) * 1.5)
    cubic-bezier(0.175, 0.885, 0.32, 1.275) forwards;
}

@keyframes appear {
  70% {
    opacity: 0.75;
    filter: blur(0px);
  }
  100% {
    transform: translateY(-24px);
  }
}
  • Frame Colors & Outline: Modify base tint in .btn-wrapper via --color: #b5faff31, primary typography via --txt-color: #283a3b, and hover secondary text via --txt-color-2: #283a3b. Tune border weight and color using --line-weight: 1px and --line-color: #00000015.
  • Vector Anchor Handles: Scale the corner square handles via --point-size: 8px and adjust their surface fill via --point-color: #ffffff. Modify point curvature via .point (border-radius: 25%).
  • Iris Compression Motion: Control the horizontal squeeze speed via --anim-speed: 1s. Adjust the collapse pinch threshold inside @keyframes frame at 50% (inset: 0 50%).
  • Sheen Texture & Blend: Customize the diagonal striped gloss on .txt-box::after by editing background: repeating-linear-gradient(45deg, #3f87a6, #ebf8e1 15%, #fff 20%) and restyle the blend behavior via mix-blend-mode: hard-light.
  • Focus Toast Notification: Adjust the simulated copy confirmation label by editing content: "Address copied" on .btn::before and tweak its lift height in @keyframes appear at 100% (transform: translateY(-24px)).
Pure CSS animated heart like button toggled via checkbox with pop scale and radial burst particles.
SVG·HTML·CSS ·~3.3 kB

Bursting Heart Like Checkbox

The interactive like button couples an invisible overlay checkbox with layered vector SVGs without requiring JavaScript. Toggling :checked engages sibling selectors to trigger concurrent animations for the filled heart and celebration burst. The filled shape scales with an elastic overshoot of scale(1.2) and a brightness(1.5) flash, while polygon spark lines scatter radially to scale(1.4) and smoothly fade to opacity: 0.

Level
Beginner
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Checkbox State Toggle · Elastic Pop Scale · Burst Particle Shock · Pure CSS Trigger
Best for
Social Media Feeds · E-commerce Wishlists · Interactive Blog Posts
Loading preview
<div class="heart-container" title="Like">
  <input type="checkbox" class="checkbox" id="Give-It-An-Id">
  <div class="svg-container">
    <svg viewBox="0 0 24 24" class="svg-outline" xmlns="http://www.w3.org/2000/svg">
      <path d="M17.5,1.917a6.4,6.4,0,0,0-5.5,3.3,6.4,6.4,0,0,0-5.5-3.3A6.8,6.8,0,0,0,0,8.967c0,4.547,4.786,9.513,8.8,12.88a4.974,4.974,0,0,0,6.4,0C19.214,18.48,24,13.514,24,8.967A6.8,6.8,0,0,0,17.5,1.917Zm-3.585,18.4a2.973,2.973,0,0,1-3.83,0C4.947,16.006,2,11.87,2,8.967a4.8,4.8,0,0,1,4.5-5.05A4.8,4.8,0,0,1,11,8.967a1,1,0,0,0,2,0,4.8,4.8,0,0,1,4.5-5.05A4.8,4.8,0,0,1,22,8.967C22,11.87,19.053,16.006,13.915,20.313Z">
      </path>
    </svg>
    <svg viewBox="0 0 24 24" class="svg-filled" xmlns="http://www.w3.org/2000/svg">
      <path d="M17.5,1.917a6.4,6.4,0,0,0-5.5,3.3,6.4,6.4,0,0,0-5.5-3.3A6.8,6.8,0,0,0,0,8.967c0,4.547,4.786,9.513,8.8,12.88a4.974,4.974,0,0,0,6.4,0C19.214,18.48,24,13.514,24,8.967A6.8,6.8,0,0,0,17.5,1.917Z">
      </path>
    </svg>
    <svg class="svg-celebrate" width="100" height="100" xmlns="http://www.w3.org/2000/svg">
      <polygon points="10,10 20,20"></polygon>
      <polygon points="10,50 20,50"></polygon>
      <polygon points="20,80 30,70"></polygon>
      <polygon points="90,10 80,20"></polygon>
      <polygon points="90,50 80,50"></polygon>
      <polygon points="80,80 70,70"></polygon>
    </svg>
  </div>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #f8fafc;
}

/* From Uiverse.io by catraco */ 
.heart-container {
  --heart-color: rgb(255, 91, 137);
  position: relative;
  width: 50px;
  height: 50px;
  transition: 0.3s;
}

.heart-container .checkbox {
  position: absolute;
  width: 100%;
  height: 100%;
  opacity: 0;
  z-index: 20;
  cursor: pointer;
}

.heart-container .svg-container {
  width: 100%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

.heart-container .svg-outline,
.heart-container .svg-filled {
  fill: var(--heart-color);
  position: absolute;
}

.heart-container .svg-filled {
  animation: keyframes-svg-filled 1s;
  display: none;
}

.heart-container .svg-celebrate {
  position: absolute;
  animation: keyframes-svg-celebrate 0.5s;
  animation-fill-mode: forwards;
  display: none;
  stroke: var(--heart-color);
  fill: var(--heart-color);
  stroke-width: 2px;
}

.heart-container .checkbox:checked ~ .svg-container .svg-filled {
  display: block;
}

.heart-container .checkbox:checked ~ .svg-container .svg-celebrate {
  display: block;
}

@keyframes keyframes-svg-filled {
  0% {
    transform: scale(0);
  }
  25% {
    transform: scale(1.2);
  }
  50% {
    transform: scale(1);
    filter: brightness(1.5);
  }
}

@keyframes keyframes-svg-celebrate {
  0% {
    transform: scale(0);
  }
  50% {
    opacity: 1;
    filter: brightness(1.5);
  }
  100% {
    transform: scale(1.4);
    opacity: 0;
    display: none;
  }
}
  • Heart Color Scheme: Change the icon fill and particle stroke via --heart-color: rgb(255, 91, 137) on .heart-container.
  • Container Footprint: Adjust overall dimensions on .heart-container (width: 50px, height: 50px) and motion smoothing via transition: .3s.
  • Pop Animation & Brightness: Alter the heart inflation keyframe timing on .heart-container .svg-filled (animation: keyframes-svg-filled 1s). Modify the overshoot scale factor (transform: scale(1.2) at 25%) and illumination flash (filter: brightness(1.5) at 50%).
  • Celebration Burst Sparks: Scale and retime the particle explosion on .heart-container .svg-celebrate (animation: keyframes-svg-celebrate .5s; stroke-width: 2px). Adjust spark dispersion radius in @keyframes keyframes-svg-celebrate at 100% (transform: scale(1.4)).
Dark theme like button in CSS with a dual-stage rolling vertical counter and animated popping SVG heart.
SVG·HTML·CSS ·~2.9 kB

Rolling Counter Like Button

The button features a dark skeuomorphic aesthetic with a 4-layer box-shadow combining inner bevel highlights and ambient drop shadows. The counter operates like a mechanical odometer within an overflow: hidden partition: toggling the hidden checkbox shifts .like-count.one to translateY(-40px) while .two rolls into view from below. Simultaneously, the SVG heart changes fill to #fc4e4e and executes an @keyframes enlarge pop.

Level
Beginner
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Rolling Counter Transition · Checkbox State Machine · Skeuomorphic Shadows · Vector Heart Pop
Best for
Dark Themed Dashboards · Blog Post Reactions · Social Media Feeds
Loading preview
<div class="like-button">
  <input class="on" id="heart" type="checkbox" />
  <label class="like" for="heart">
    <svg
      class="like-icon"
      fill-rule="nonzero"
      viewBox="0 0 24 24"
      xmlns="http://www.w3.org/2000/svg"
    >
      <path
        d="m11.645 20.91-.007-.003-.022-.012a15.247 15.247 0 0 1-.383-.218 25.18 25.18 0 0 1-4.244-3.17C4.688 15.36 2.25 12.174 2.25 8.25 2.25 5.322 4.714 3 7.688 3A5.5 5.5 0 0 1 12 5.052 5.5 5.5 0 0 1 16.313 3c2.973 0 5.437 2.322 5.437 5.25 0 3.925-2.438 7.111-4.739 9.256a25.175 25.175 0 0 1-4.244 3.17 15.247 15.247 0 0 1-.383.219l-.022.012-.007.004-.003.001a.752.752 0 0 1-.704 0l-.003-.001Z"
      ></path>
    </svg>
    <span class="like-text">Likes</span>
  </label>
  <span class="like-count one">68</span>
  <span class="like-count two">69</span>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #121212;
}

#heart {
  display: none;
}

.like-button {
  position: relative;
  cursor: pointer;
  display: flex;
  height: 48px;
  width: 136px;
  border-radius: 16px;
  border: none;
  background-color: #1d1d1d;
  overflow: hidden;
  box-shadow:
    inset -2px -2px 5px rgba(255, 255, 255, 0.2),
    inset 2px 2px 5px rgba(0, 0, 0, 0.1),
    4px 4px 10px rgba(0, 0, 0, 0.4),
    -2px -2px 8px rgba(255, 255, 255, 0.1);
  user-select: none;
}

.like {
  width: 70%;
  height: 100%;
  display: flex;
  cursor: pointer;
  align-items: center;
  justify-content: space-evenly;
}

.like-icon {
  fill: #505050;
  height: 28px;
  width: 28px;
  transition: all 0.2s ease-out;
}

.like-text {
  color: #fcfcfc;
  font-size: 16px;
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
}

.like-count {
  position: absolute;
  right: 0;
  width: 30%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
  color: #717070;
  font-size: 16px;
  font-family: "Segoe UI", Tahoma, Geneva, Verdana, sans-serif;
  border-left: 2px solid #4e4e4e;
  transition: all 0.5s ease-out;
}

.like-count.two {
  transform: translateY(40px);
}

.on:checked ~ .like .like-icon {
  fill: #fc4e4e;
  animation: enlarge 0.2s ease-out 1;
}

.on:checked ~ .like-count.two {
  transform: translateY(0);
  color: #fcfcfc;
}

.on:checked ~ .like-count.one {
  transform: translateY(-40px);
}

@keyframes enlarge {
  0% {
    transform: scale(0.5);
  }
  100% {
    transform: scale(1.2);
  }
}
  • Chassis & Bevel Shadows: Adjust capsule footprint in .like-button (width: 136px, height: 48px, border-radius: 16px) and background fill (background-color: #1d1d1d). Fine-tune inner bevel and outer elevation highlights inside .like-button box-shadow.
  • Icon Palette & Pop Scale: Change the inactive heart fill on .like-icon (fill: #505050) and active fill under .on:checked ~ .like .like-icon (fill: #fc4e4e). Adjust pulse expansion in @keyframes enlarge at 100% (transform: scale(1.2)).
  • Rolling Counter Geometry: Adjust the vertical roll displacement on .like-count.two (transform: translateY(40px)) and .on:checked ~ .like-count.one (transform: translateY(-40px)). Restyle divider line color via .like-count (border-left: 2px solid #4e4e4e).
  • Motion Easing & Timing: Modify rolling transition duration under .like-count (transition: all 0.5s ease-out) and heart pop timing under .on:checked ~ .like .like-icon (animation: enlarge 0.2s ease-out 1).
Split counter like button in CSS with inverted color hover states and vertical number sliding animations.
SVG·HTML·CSS ·~3.3 kB

Split Counter Like Button

The button is split into two proportional zones: a 30% numeric column on the left and a 70% action area housing the SVG icon and label. Hovering inverts the color palette between cyan #00d5ff and white, while sliding the text off-screen via translateX(200%) so the icon scales up to occupy the center. Toggling the hidden checkbox coordinates vertical translateY shifts on the digit stack, smoothly transitioning the counter.

Level
Beginner
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Split Partition Layout · Hover Palette Inversion · Sliding Counter Transform · Icon Drift Translation
Best for
Social Media Cards · Blog Post Footers · Modern Landing Pages
Loading preview
<div class="button-container">
  <input hidden="" id="checknumber" type="checkbox" />
  <label for="checknumber" class="button">
    <div id="leftpart">
      <p id="currentnumber">24</p>
      <p id="movenumber">25</p>
    </div>

    <div id="rightpart">
      <svg
        id="likeimg"
        stroke-linejoin="round"
        stroke-linecap="round"
        stroke-width="3"
        stroke="#00d5ff"
        fill="none"
        viewBox="0 0 24 24"
        height="24"
        width="24"
        xmlns="http://www.w3.org/2000/svg"
      >
        <path
          d="M14 9V5a3 3 0 0 0-3-3l-4 9v11h11.28a2 2 0 0 0 2-1.7l1.38-9a2 2 0 0 0-2-2.3zM7 22H4a2 2 0 0 1-2-2v-7a2 2 0 0 1 2-2h3"
        ></path>
      </svg>
      <div id="fontlikebutton">Like</div>
    </div>
  </label>
</div>
body {
  margin: 0;
  min-height: 100vh;
  display: grid;
  place-items: center;
  background-color: #f0f4f8;
}

.button-container {
  position: relative;
  display: inline-block;
  user-select: none;
}

.button {
  cursor: pointer;
  width: 15em;
  height: 5em;
  display: flex;
  align-items: center;
  background-color: white;
  border: none;
  box-shadow: 0px 5px 0px rgba(45, 45, 45, 0.2);
  overflow: hidden;
  border-radius: 0.5em;
  transition: all 0.2s ease;
}

#fontlikebutton {
  font-family: "Trebuchet MS", sans-serif;
  font-weight: 600;
  font-size: 2em;
  color: #00d5ff;
  margin-left: 0.2em;
  transition: transform 0.2s ease;
}

.button:hover {
  background-color: #00d5ff;
}

.button:hover svg {
  filter: brightness(0) invert(1);
  transform: scale(1.5) translateX(100%);
}

.button:hover #fontlikebutton {
  transform: translateX(200%);
}

.button:active {
  transform: scale(0.95) translateY(10%);
  box-shadow: 0px 2px 0px 0px rgb(0, 213, 255, 0.5);
}

.button:active svg {
  margin-bottom: 0.5em;
  transform: scale(1.5) translateX(85%) rotate(-30deg);
}

.button:active #fontlikebutton {
  transform: translateX(200%);
}

#likeimg {
  transition: all 0.2s ease;
}

#rightpart {
  width: 70%;
  height: 100%;
  display: flex;
  justify-content: center;
  align-items: center;
}

#leftpart {
  color: white;
  display: flex;
  justify-content: center;
  align-items: center;
  flex-direction: column;
  font-family: "Trebuchet MS", sans-serif;
  font-weight: 600;
  font-size: 2em;
  background-color: #00d5ff;
  width: 30%;
  height: 100%;
  transition: all 0.2s ease;
}

.button:hover #leftpart {
  color: #00d5ff;
  background: white;
}

#currentnumber {
  margin: 0;
  transform: translateY(50%);
  transition: all 0.2s ease;
}

#movenumber {
  margin: 0;
  transform: translateY(-200%);
  transition: all 0.2s ease;
}

#checknumber:checked ~ .button #currentnumber {
  transform: translateY(200%);
}

#checknumber:checked ~ .button #movenumber {
  transform: translateY(-50%);
}
  • Cyan Theme & Palette: Replace the accent color (#00d5ff) across active states: typography in #fontlikebutton, SVG stroke in #likeimg, background in #leftpart, hover fill on .button:hover, and click glow on .button:active.
  • Container Dimensions & Partitions: Adjust overall button dimensions on .button (width: 15em, height: 5em, border-radius: 0.5em). Modify the split column ratios by adjusting width: 30% on #leftpart and width: 70% on #rightpart.
  • Hover Icon Drift & Text Ejection: Tune the kinetic sliding distance on hover by modifying #likeimg displacement inside .button:hover svg (transform: scale(1.5) translateX(100%)) and label exit distance via .button:hover #fontlikebutton (transform: translateX(200%)).
  • Sliding Digit Offsets: Adjust vertical number positions for idle state in #currentnumber (translateY(50%)) and #movenumber (translateY(-200%)). Adjust checked targets via #checknumber:checked ~ .button #currentnumber (translateY(200%)) and #movenumber (translateY(-50%)).
Wild west themed duel button with leather gradient, spinning star icon, and light shimmer effect over a desert grid background.
HTML·CSS·SVG ·3.5 kB

Wild West Shimmer Button

A semantic <button> centered within a layered backdrop featuring grid lines, a radial vignette, and focus glow. The shimmer effect animates background-position across an ::after pseudo-element, while the SVG icon rotates via transform. However, animating background-position triggers continuous CPU repaints, and transition: all forces layout and shadow recalculations on hover.

Level
Beginner
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Shimmer Animation · Icon Rotation · Layered Vignette · Elevation Hover
Best for
Gaming UIs · Western-Themed Websites · Call-to-Action Sections
Loading preview
<div class="wild-west-scene">
  <div class="wild-west-scene__grid"></div>
  <div class="wild-west-scene__overlay"></div>
  <div class="wild-west-scene__vignette"></div>
  <div class="wild-west-scene__center-focus"></div>
  <button class="wild-west-button">
    <span class="wild-west-button__content">
      <svg
        class="wild-west-button__icon"
        xmlns="http://www.w3.org/2000/svg"
        viewBox="0 0 24 24"
        fill="currentColor"
      >
        <path
          d="M12 2L9.09 5H4v4.09L1 12l3 2.91V19h5.09L12 22l2.91-3H20v-5.09L23 12l-3-2.91V5h-5.09L12 2zm0 4.5a5.5 5.5 0 110 11 5.5 5.5 0 010-11zm0 2a3.5 3.5 0 100 7 3.5 3.5 0 000-7z"
        ></path>
      </svg>
      <span class="wild-west-button__text">Duel</span>
    </span>
  </button>
</div>
body {
  margin: 0;
  padding: 0;
  height: 100vh;
  overflow: hidden;
  display: flex;
  justify-content: center;
  align-items: center;
  background-color: #d2b48c;
}

/* From Uiverse.io by LeonKohli */ 
.wild-west-scene {
  position: relative;
  width: 100%;
  height: 100%;
}

.wild-west-scene__grid {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-color: #d2b48c;
  background-image: linear-gradient(rgba(139, 69, 19, 0.1) 1px, transparent 1px),
    linear-gradient(90deg, rgba(139, 69, 19, 0.1) 1px, transparent 1px),
    linear-gradient(rgba(139, 69, 19, 0.2) 2px, transparent 2px),
    linear-gradient(90deg, rgba(139, 69, 19, 0.2) 2px, transparent 2px);
  background-size:
    20px 20px,
    20px 20px,
    100px 100px,
    100px 100px;
}

.wild-west-scene__overlay {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background-repeat: repeat;
  background-size: 100px 100px;
}

.wild-west-scene__vignette {
  position: absolute;
  top: 0;
  left: 0;
  width: 100%;
  height: 100%;
  background: radial-gradient(
    circle at center,
    transparent 0%,
    transparent 25%,
    rgba(0, 0, 0, 0.2) 50%,
    rgba(0, 0, 0, 0.5) 75%,
    rgba(0, 0, 0, 0.7) 100%
  );
}

.wild-west-scene__center-focus {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  width: 300px;
  height: 300px;
  background: radial-gradient(
    circle at center,
    rgba(255, 248, 220, 0.3) 0%,
    rgba(255, 248, 220, 0.1) 50%,
    transparent 70%
  );
  border-radius: 50%;
  box-shadow: 0 0 100px 50px rgba(255, 248, 220, 0.1);
}

.wild-west-button {
  position: absolute;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 15px 30px;
  font-size: 24px;
  color: #f1dabb;
  background: linear-gradient(45deg, #8b4513, #a0522d);
  border: none;
  border-radius: 10px;
  cursor: pointer;
  text-transform: uppercase;
  letter-spacing: 2px;
  font-weight: bold;
  overflow: hidden;
  transition: all 0.3s ease;
  box-shadow:
    0 10px 20px rgba(0, 0, 0, 0.3),
    inset 0 -5px 10px rgba(0, 0, 0, 0.2);
}

.wild-west-button::before {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  opacity: 0.3;
}

.wild-west-button:hover {
  transform: translate(-50%, -55%) scale(1.05);
  box-shadow:
    0 15px 30px rgba(0, 0, 0, 0.4),
    inset 0 -5px 10px rgba(0, 0, 0, 0.2);
}

.wild-west-button:active {
  transform: translate(-50%, -48%) scale(0.95);
  box-shadow:
    0 5px 10px rgba(0, 0, 0, 0.3),
    inset 0 -3px 5px rgba(0, 0, 0, 0.2);
}

.wild-west-button__content {
  position: relative;
  z-index: 1;
  display: flex;
  align-items: center;
  justify-content: center;
}

.wild-west-button__icon {
  width: 30px;
  height: 30px;
  margin-right: 10px;
  fill: #f1dabb;
  transition: transform 0.3s ease;
}

.wild-west-button:hover .wild-west-button__icon {
  transform: rotate(360deg) scale(1.2);
}

.wild-west-button::after {
  content: "";
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  bottom: 0;
  background: linear-gradient(
    90deg,
    transparent,
    rgba(255, 255, 255, 0.2),
    transparent
  );
  background-size: 200% 100%;
  animation: wild-west-button-shine 3s linear infinite;
}

@keyframes wild-west-button-shine {
  0% {
    background-position: 200% 50%;
  }
  100% {
    background-position: -200% 50%;
  }
}
  • Colors & Themes: Change the leather button gradient in .wild-west-button (linear-gradient(45deg, #8b4513, #a0522d)). Update button text and icon color via color: #f1dabb and .wild-west-button__icon (fill: #f1dabb). Adjust terrain base tone in .wild-west-scene__grid (background-color: #d2b48c).
  • Gradients & Lighting: Modify ambient darkness in .wild-west-scene__vignette (rgba(0, 0, 0, 0.7) stop). Tweak spotlight intensity in .wild-west-scene__center-focus (rgba(255, 248, 220, 0.3)). Alter glint highlight opacity in .wild-west-button::after (rgba(255, 255, 255, 0.2)).
  • Sizing & Motion: Adjust dimensions in .wild-west-button (padding: 15px 30px, font-size: 24px, border-radius: 10px). Scale the SVG graphic via .wild-west-button__icon (width: 30px, height: 30px). Change glint cycle speed via .wild-west-button::after (animation: wild-west-button-shine 3s linear infinite) and hover scale ratio (scale(1.05)).
Retro arcade cabinet push button revealing an animated Pac-Man and ghost sequence using pure SCSS and checkbox toggling.
HTML·CSS ·~15 kB

Interactive Pac-Man Arcade Button

A JS-free arcade interaction driven entirely by the checkbox hack on an input:checked sibling selector. Maze walls are procedurally generated via SCSS functions combining multiple radial and linear gradients into a single background. Characters morph along keyframes with animated clip-path polygons and CSS @property interpolations. Continuous polygon repaints cause heavy paint invalidations, lacking reduced-motion overrides.

Level
Advanced
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Checkbox Toggle · Procedural Gradients · Polygon Morphing · Property Animation
Best for
Retro Gaming Portfolios · Gamified Interfaces · Interactive Landing Pages
Loading preview
<arcade-machine>
  <arcade-machine-title></arcade-machine-title>
  <label>
    <arcade-button style="--button-label: 'Start'">
      <input type="checkbox" id="input">
      <arcade-button-text></arcade-button-text>
      <ghost class="clyde"></ghost>
      <pacman></pacman>
      <dots></dots>
      <dots-v></dots-v>
      <walls></walls>
    </arcade-button>
  </label>
</arcade-machine>
<div class="ghosts-bar">
  <div class="ghosts">
    <ghost class="blinky"></ghost>
    <ghost class="clyde"></ghost>
    <ghost class="inky"></ghost>
    <ghost class="pinky"></ghost>
    <ghost class="dizzied"></ghost>
  </div>
</div>
:root {
  --surface: #271C22;
  --primary: #FFD600;
  --secondary: #99CFD4;
  --secondary-variant: #4A46BA;
  --tertiary: #D52B5D;
  --tertiary-variant: #4A2832;

  --dot: #D0ABB8;
  --ghost-1: #D0ABB8;
  --ghost-2: #DDA445;
  --ghost-3: #99CFD4;
  --ghost-4: #E0161A;
  --ghost-variant: #4A46BA;
}

* {
  box-sizing: border-box;
}

body {
  width: 100vw;
  height: 100vh;
  display: grid;
  margin: 0;
  place-items: center;
  font-family: 'Road Rage', 'Helvetica Neue', system-ui;
  background-color: var(--surface);
  color: var(--primary);
  overflow: hidden;
}

body::after {
  content: "";
  position: absolute;
  background: url(https://assets.codepen.io/907471/noise.svg);
  mix-blend-mode: overlay;
  inset: 0;
  pointer-events: none;
  filter: invert(1) brightness(2);
  z-index: 10;
}

arcade-machine {
  display: grid;
  place-items: center;
  border-bottom-left-radius: 10rem;
  border-bottom-right-radius: 10rem;
  width: 100vw;
  height: 100vh;
  background: linear-gradient(to bottom, #AB1740, #CB2057);
  position: relative;
}

arcade-machine::before {
  content: '';
  background: linear-gradient(to bottom, #DC3362, #CB2057);
  box-shadow: -0.0825rem 0.1rem 1rem 0.0625rem rgba(70, 8, 17, 0.4), -0.0625rem 0.1rem 0 rgba(131, 30, 61, 0.34), 0.0625rem 0.1rem 0 rgba(131, 30, 61, 0.34);
  position: absolute;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
  top: 0;
  left: 2rem;
  right: 2rem;
  bottom: 1.5rem;
}

arcade-machine::after {
  content: '';
  background: linear-gradient(to bottom, #DC3362, #CB2057);
  box-shadow: -0.0625rem 1rem 0.4rem #CB2057, 0.0625rem 1rem 0.4rem #CB2057;
  position: absolute;
  border-bottom-left-radius: inherit;
  border-bottom-right-radius: inherit;
  top: 50%;
  left: 2rem;
  right: 2rem;
  bottom: 1.5rem;
}

arcade-machine-title {
  display: flex;
  aspect-ratio: 4.8 / 20;
  height: min(20rem, 35vh);
  border-bottom-left-radius: 10rem;
  border-bottom-right-radius: 10rem;
  position: absolute;
  left: 4rem;
  top: 0;
  background: url(https://assets.codepen.io/907471/arcade-button-title.svg) center center no-repeat, linear-gradient(to bottom, #CD1E4F, #C0134B 20%);
  background-size: contain;
  padding: 0.5rem;
  background-origin: content-box;
  box-shadow: inset -0.0625rem 0 0 rgba(131, 30, 61, 0.34), inset 0.0625rem 0 0 rgba(131, 30, 61, 0.34), inset 0 -0.2rem 0.8rem rgba(0, 0, 0, 0.15);
}

arcade-button {
  z-index: 1111111;
  display: flex;
  align-items: center;
  color: var(--primary);
  border-radius: 10rem;
  width: 20rem;
  height: 7rem;
  position: relative;
  overflow: hidden;
  animation: progress 2s linear infinite;
  transition: border 0.3s linear;
  --border-color: var(--secondary);
  border: 0.375rem solid var(--border-color);
  text-transform: uppercase;
  background: var(--surface);
  box-shadow: -0.0625rem -0.0625rem 0.125rem rgba(255, 255, 255, 0.25),
    -0.125rem -0.125rem 0.8125rem rgba(231, 135, 164, 0.15),
    -0.25rem -0.25rem 1.1875rem 0.3125rem #DD6589,
    0.25rem 0.25rem 0.1875rem #7E233E,
    0.25rem 0.25rem 1.5rem 0.4375rem #951D41;
}

@media (hover) {
  arcade-button:hover {
    border-color: var(--tertiary);
    border-width: 0.75rem;
    cursor: pointer;
  }
  arcade-button:hover::after,
  arcade-button:hover walls,
  arcade-button:hover dots,
  arcade-button:hover dots-v {
    margin: -0.375rem;
  }
}

arcade-button:active {
  border-color: var(--tertiary-variant);
  border-width: 0.5rem;
  cursor: pointer;
}

arcade-button:active::after {
  margin: -0.125rem;
}

arcade-button::before {
  content: '';
  position: absolute;
  inset: 0;
  animation: content-placeholder-animation var(--content-placeholder-animation-duration, 1.5s) ease 2 forwards;
  background: transparent linear-gradient(
    to right,
    transparent 0,
    transparent calc(10% - 6.25rem),
    color-mix(in srgb, var(--tertiary-variant), transparent 15%) 50%,
    transparent calc(50% + 6.25rem),
    transparent 100%
  ) center left;
  background-size: 100% 100%;
}

arcade-button::after {
  content: '';
  position: absolute;
  inset: 0;
  background: radial-gradient(transparent 40%, var(--surface)) repeat;
  z-index: 120;
  background-size: 0.25rem 0.25rem;
  transition: margin 0.3s linear;
}

arcade-button input {
  display: none;
  z-index: 111;
}

arcade-button input:not(:checked) ~ *:not(arcade-button-text):not(.audio) {
  display: none;
  transition: all 0.3s ease;
  opacity: 1;
}

arcade-button input:not(:checked) ~ arcade-button-text {
  animation: none;
}

arcade-button input:not(:checked) ~ arcade-button-text::after {
  content: var(--button-label);
}

arcade-button input:not(:checked) ~ walls {
  opacity: 0;
}

arcade-button input:checked ~ .audio {
  display: block !important;
}

arcade-button-text {
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  font-size: 2rem;
  letter-spacing: 0.5rem;
  animation: label 5s linear infinite;
}

arcade-button-text::after {
  content: 'Loading';
}

@keyframes content-placeholder-animation {
  from {
    opacity: 0;
    transform: translateX(-100%);
  }
  50% {
    opacity: 1;
  }
  to {
    opacity: 0;
    transform: translateX(100%);
  }
}

@keyframes label {
  from, 66% {
    opacity: 0;
    transform: translateY(0.26rem);
  }
  69% {
    transform: translateY(0);
    opacity: 0.1;
  }
  71%, 76%, 82%, 90% {
    opacity: 1;
    transform: translateY(0);
  }
  74%, 78% {
    opacity: 0.2;
  }
  93%, to {
    opacity: 0;
    transform: translateY(0.26rem);
  }
}

@property --ghost-eye-size {
  syntax: "<number>";
  inherits: true;
  initial-value: 0;
}

ghost {
  --ghost-eye-size: 1;
  --ghost-chomp-color: transparent;
  display: grid;
  background: 
    radial-gradient(calc(0.45rem * var(--ghost-eye-size)) calc(0.45rem * var(--ghost-eye-size)) at 0.85rem 1rem, var(--eye-color, var(--ghost-variant)) 50%, transparent 50%),
    radial-gradient(calc(0.45rem * var(--ghost-eye-size)) calc(0.45rem * var(--ghost-eye-size)) at 1.75rem 1rem, var(--eye-color, var(--ghost-variant)) 50%, transparent 50%),
    radial-gradient(calc(0.6rem * var(--ghost-eye-size)) calc(0.75rem * var(--ghost-eye-size)) at 0.75rem 1rem, white 50%, transparent 50%),
    radial-gradient(calc(0.6rem * var(--ghost-eye-size)) calc(0.75rem * var(--ghost-eye-size)) at 1.6rem 1rem, white 50%, transparent 50%),
    var(--ghost-color);
  clip-path: polygon(0% 100%, 0% 0%, 100% 0%, 99% 100%, 77% 86%, 58% 100%, 42% 84%, 27% 100%, 15% 84%);
  animation: ghost-body 0.3s ease infinite, ghost-move 5s linear infinite;
  border-top-left-radius: 50%;
  border-top-right-radius: 50%;
  width: 2rem;
  height: 2.4rem;
  margin-top: 0.35rem;
  left: 1rem;
  z-index: 8;
  place-items: center;
}

ghost::after {
  content: '';
  background: var(--ghost-chomp-color, white);
  height: 0.4rem;
  width: 1.25rem;
  transform: translate(0.25rem, 0.5rem);
  clip-path: polygon(0% 77%, 24% 7%, 45% 47%, 63% 0%, 77% 45%, 94% 2%, 80% 76%, 63% 30%, 46% 90%, 25% 39%);
  animation: ghost-chomp 0.3s ease infinite;
}

ghost.pinky {
  --ghost-color: var(--ghost-color-variant, var(--ghost-1));
}

ghost.clyde {
  --ghost-color: var(--ghost-color-variant, var(--ghost-2));
}

ghost.inky {
  --ghost-color: var(--ghost-color-variant, var(--ghost-3));
}

ghost.blinky {
  --ghost-color: var(--ghost-color-variant, var(--ghost-4));
}

ghost.dizzied {
  --ghost-color: var(--ghost-variant);
  --ghost-color-variant: var(--ghost-variant);
  --ghost-eye-size: 0.5;
  --ghost-eye-color: transparent;
  --ghost-chomp-color: white;
}

@keyframes ghost-body {
  0%, 100% {
    clip-path: polygon(0% 100%, 0% 0%, 100% 0%, 100% 100%, 90.56% 90.56%, 82.44% 73.62%, 75.44% 90.56%, 65.13% 100%, 56.49% 90.56%, 47.44% 73.62%, 42.25% 91.5%, 31.22% 100%, 21.24% 92.09%, 15% 73.62%, 8.5% 91.5%);
  }
  50% {
    clip-path: polygon(0% 75.25%, 0% 0%, 100% 0%, 100% 75.25%, 90.56% 90.56%, 83.3% 100%, 75.44% 90.56%, 65.96% 73.58%, 56.49% 90.56%, 49.96% 100%, 42.25% 91.5%, 31.23% 73.58%, 21.24% 92.09%, 14.98% 100%, 8.5% 91.5%);
  }
}

@keyframes ghost-chomp {
  0%, 100% {
    clip-path: polygon(0% 77%, 24% 7%, 45% 47%, 63% 0%, 77% 45%, 94% 2%, 80% 76%, 63% 30%, 46% 90%, 25% 39%);
  }
  50% {
    clip-path: polygon(0 5%, 22% 64%, 44% 5%, 61% 70%, 77% 6%, 99% 76%, 79% 33%, 61% 91%, 44% 37%, 21% 95%);
  }
}

@keyframes ghost-move {
  0% {
    transform: translateX(-15rem);
    --ghost-eye-size: 1;
    --ghost-chomp-color: transparent;
  }
  49.99% {
    transform: translateX(5rem);
    --ghost-eye-size: 1;
    --ghost-chomp-color: transparent;
    --ghost-color-variant: unset;
    --ghost-eye-color: unset;
  }
  50% {
    transform: translateX(7rem) scaleX(-1);
    --ghost-color-variant: var(--ghost-variant);
    --ghost-eye-size: 0.5;
    --ghost-eye-color: transparent;
    --ghost-chomp-color: white;
  }
  60% {
    transform: translateX(2rem) scaleX(-1);
  }
  100% {
    transform: translateX(-15rem) scaleX(-1);
  }
}

.ghosts-bar {
  position: absolute;
  left: 0;
  bottom: 6rem;
  right: 0;
  display: grid;
  place-items: center;
}

.ghosts {
  background: var(--surface);
  bottom: 4rem;
  display: flex;
  gap: 1rem;
  border-radius: 2rem;
  padding: 1rem;
  opacity: 0;
  animation: fadeIn 0.35s 4s ease-in forwards;
  transform: scale(0.5);
}

.ghosts ghost {
  position: relative;
  left: unset;
  top: unset;
  animation: ghost-body 0.3s ease infinite;
}

@keyframes fadeIn {
  from {
    opacity: 0;
    transform: translateY(1rem) scale(0.5);
  }
  to {
    opacity: 1;
    transform: translateY(0) scale(0.5);
  }
}

@property --pacman-chomp-deg {
  syntax: "<angle>";
  inherits: true;
  initial-value: 0deg;
}

pacman {
  --pacman-chomp-deg: 42deg;
  display: block;
  width: 2.5rem;
  margin-top: 0.2rem;
  aspect-ratio: 1;
  border-radius: 50%;
  position: relative;
  animation: pacman-chomp 0.6s linear infinite, pacman-move 5s linear infinite;
  z-index: 10;
}

pacman::before {
  content: '';
  background-color: var(--primary);
  position: absolute;
  top: 0;
  left: 0;
  right: 0;
  height: 1.25rem;
  border-top-left-radius: 10rem;
  border-top-right-radius: 10rem;
  transform: rotate(calc(-1 * var(--pacman-chomp-deg)));
  transform-origin: bottom;
}

pacman::after {
  content: '';
  background-color: var(--primary);
  position: absolute;
  top: 1.25rem;
  left: 0;
  right: 0;
  height: 1.25rem;
  border-bottom-left-radius: 10rem;
  border-bottom-right-radius: 10rem;
  transform: rotate(var(--pacman-chomp-deg));
  transform-origin: top;
}

@keyframes pacman-chomp {
  0%, 100% {
    --pacman-chomp-deg: 0deg;
  }
  50% {
    --pacman-chomp-deg: 70deg;
  }
}

@keyframes pacman-move {
  0% {
    transform: translateX(-10rem);
  }
  49.99% {
    transform: translateX(10rem);
  }
  50% {
    transform: translateX(10rem) scaleX(-1);
  }
  60% {
    transform: translateX(4rem) scaleX(-1);
  }
  100% {
    transform: translateX(-10rem) scaleX(-1);
  }
}

dots {
  --target-x-position: 70%;
  display: block;
  position: absolute;
  height: 1.5rem;
  left: 0;
  min-width: 100%;
  background: 
    radial-gradient(0.35rem 0.35rem at 2.42rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 4.84rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 7.26rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 9.68rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(1.5rem 1.5rem at 12.1rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 14.52rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 16.94rem 50%, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 19.36rem 50%, var(--dot) 50%, transparent 50%) no-repeat;
  animation: dots 5s linear infinite;
  transition: margin 0.3s linear;
}

dots-v {
  display: block;
  position: absolute;
  height: 5rem;
  width: 100%;
  left: 0;
  background: 
    radial-gradient(0.35rem 0.35rem at 12.1rem 0.7rem, var(--dot) 50%, transparent 50%) no-repeat,
    radial-gradient(0.35rem 0.35rem at 12.1rem 4.4rem, var(--dot) 50%, transparent 50%) no-repeat;
  transition: margin 0.3s linear;
}

@keyframes dots {
  0%, 20% {
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  }
  50%, 95% {
    clip-path: polygon(var(--target-x-position) 0, 100% 0, 100% 100%, var(--target-x-position) 100%);
  }
  98% {
    opacity: 1;
  }
  99% {
    opacity: 0;
  }
  100% {
    opacity: 1;
    clip-path: polygon(0 0, 100% 0, 100% 100%, 0 100%);
  }
}

walls {
  display: flex;
  position: absolute;
  background:
    radial-gradient(1rem at -9.25rem 0.5rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    radial-gradient(1rem at 9.25rem 0.5rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--surface), var(--surface)) no-repeat -9.25rem 0rem / 18.5rem 1rem,
    radial-gradient(1.5rem at -9.5rem 0.5rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    radial-gradient(1.5rem at 9.5rem 0.5rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--secondary-variant), var(--secondary-variant)) no-repeat -9.5rem -0.25rem / 19rem 1.5rem,

    radial-gradient(1rem at 14.75rem 0.5rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    radial-gradient(1rem at 33.25rem 0.5rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--surface), var(--surface)) no-repeat 14.75rem 0rem / 18.5rem 1rem,
    radial-gradient(1.5rem at 14.5rem 0.5rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    radial-gradient(1.5rem at 33.5rem 0.5rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--secondary-variant), var(--secondary-variant)) no-repeat 14.5rem -0.25rem / 19rem 1.5rem,

    radial-gradient(1rem at -9.25rem 5.7rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    radial-gradient(1rem at 9.25rem 5.7rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--surface), var(--surface)) no-repeat -9.25rem 5.2rem / 18.5rem 1rem,
    radial-gradient(1.5rem at -9.5rem 5.7rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    radial-gradient(1.5rem at 9.5rem 5.7rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--secondary-variant), var(--secondary-variant)) no-repeat -9.5rem 4.95rem / 19rem 1.5rem,

    radial-gradient(1rem at 14.75rem 5.7rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    radial-gradient(1rem at 33.25rem 5.7rem, var(--surface) 0.5rem, transparent 0.5rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--surface), var(--surface)) no-repeat 14.75rem 5.2rem / 18.5rem 1rem,
    radial-gradient(1.5rem at 14.5rem 5.7rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    radial-gradient(1.5rem at 33.5rem 5.7rem, var(--secondary-variant) 0.75rem, transparent 0.75rem) no-repeat center center / 100% 100%,
    linear-gradient(45deg, var(--secondary-variant), var(--secondary-variant)) no-repeat 14.5rem 4.95rem / 19rem 1.5rem;
  width: 20rem;
  height: 6rem;
  transition: margin 0.3s linear, opacity 0.3s linear;
  opacity: 1;
}
  • Colors & Themes: Modify primary tokens in :root via --surface (#271C22), --primary (#FFD600), --secondary (#99CFD4), and --tertiary (#D52B5D). Override ghost sprite palettes by redefining --ghost-1 (#D0ABB8) through --ghost-4 (#E0161A) or vulnerability color via --ghost-variant (#4A46BA).
  • Gradients & Visual Effects: Update cabinet enclosure ramps in arcade-machine and pseudo-elements via linear-gradient(to bottom, #AB1740, #CB2057). Adjust scanline raster density in arcade-button:after using background-size: 0.25rem 0.25rem and neon depth via arcade-button box-shadow.
  • Sizing & Scales: Adjust component footprint in arcade-button (width: 20rem, height: 7rem, border-radius: 10rem). Set idle label text via inline custom property --button-label: 'Start' and customize typography under arcade-button-text (font-size: 2rem, letter-spacing: .5rem). Scale character actors via pacman (width: 2.5rem) and ghost (width: 2rem, height: 2.4rem).
Pure CSS diagonal optical illusion with parallel lines distorted on a yellow background, revealed on hover.
CSS ·1 kB

Diagonal Illusion Pattern in Pure CSS

A pure CSS diagonal optical illusion that distorts parallel lines without JavaScript. Combines repeating-linear-gradient grid stripes with rotated conic-gradient elements on body::before and body::after pseudo-elements. Hovering transitions the @property --color variable to transparent, removing the visual interference to demonstrate that all diagonal lines are strictly parallel.

Level
Intermediate
Browsers
Chrome · Edge · Safari · Firefox
Highlights
Optical Illusion · Pure CSS · Animated Property · Hover Reveal
Best for
Interactive Demos · Hero Backgrounds · Creative Canvas
Loading preview
/*
optical effect
The diagonal black lines are parallel, but they look like they are not. Mouse over to reveal
*/
@property --color {
  syntax: '<color>';
  initial-value: #f00;
  inherits: true;
}

body {
  --color: #000;
  margin: 0;
  font-size: 1vmax;
  height: 100vh;
  overflow: hidden;
  transition: --color 1s;
  background: #fd5;
}

body:hover {
  --color: #0000;
}

body::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 300em;
  height: 300em;
  background:
    repeating-linear-gradient(#0000 0 18em, #000 0 20em),
    conic-gradient(at 1em 8em, #0000 75%, var(--color) 0) 0 -5em / 2em 20em;
  transform: skewY(-45deg);
}

body::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 300em;
  height: 300em;
  background:
    repeating-linear-gradient(#0000 0 18em, #000 0 20em),
    conic-gradient(at 1em 8em, #0000 75%, var(--color) 0) 0 -5em / 2em 20em;
  transform: rotate(90deg) skewY(45deg) translate(0.25em, 12em);
}
  • Colors / Themes: Change canvas color via background: #fd5 on body. Adjust illusion tile color --color: #000 and initial value initial-value: #f00 inside @property --color. Tweak hover reveal state via --color: #0000.
  • Gradients & Visual Effects: Modify grid line spacing by editing repeating-linear-gradient(#0000 0 18em, #000 0 20em) stop offsets. Alter distortion block positioning through conic-gradient(at 1em 8em, #0000 75%, var(--color) 0) 0 -5em / 2em 20em inside pseudo-elements.
  • Sizing, Layout & Scales: Change viewport scale density via font-size: 1vmax on body. Adjust tilt severity by modifying transform: skewY(-45deg) on body::before and transform: rotate(90deg) skewY(45deg) on body::after.
Cyberpunk theme switcher using CSS corner-shape notches, light-dark colors, and pure CSS state handling.
HTML·CSS ·4 kB

Futuristic CSS Theme Switcher with Corner Shaping

A pure CSS theme switcher showcasing bleeding-edge spec features. Uses :has() on :root to toggle color schemes without JavaScript. Leverages light-dark(), color-mix(), and native inline if() logic for dynamic styling. Corner shapes like notch and superellipse() provide a pixel-art sci-fi aesthetic. Built on an accessible <fieldset> radio control structure with full keyboard focus support.

Level
Advanced
Browsers
Chrome · Edge
Highlights
Pure CSS State · Corner Shaping · Color Mix Engine · Inline CSS if()
Best for
Dark Themes · Futuristic Dashboards · Gaming UI
Loading preview
<fieldset id="color-scheme">
  <legend>Theme</legend>
  <label for="system" tabindex="0">
    <input type="radio" id="system" name="theme" value="system" tabindex="-1"/><span>Sys</span>
  </label>
  <label for="light" tabindex="0">
    <input type="radio" id="light" name="theme" value="light" tabindex="-1" checked="checked"/><span>Light</span>
  </label>
  <label for="dark" tabindex="0">
    <input type="radio" id="dark" name="theme" value="dark" tabindex="-1"/><span>Dark</span>
  </label>
</fieldset>
fieldset {
	border: 2px solid 
		if(
			style(--color-scheme: light):
				color-mix(
					in oklab, 
					var(--theme-accent), 
					transparent 80%
				)
			;
			
			/* uses NOT over ELSE to respect `prefers-color-scheme` */
			not style(--color-scheme: light): 
				color-mix(
					in oklab, 
					var(--theme-accent), 
					transparent 70%
				)
			;
		)
	;	
	border-radius: 5px;
	corner-shape: notch;
	color: var(--theme-text);
	outline: 
		7px ridge 		
		if(
			style(--color-scheme: light):
				color-mix(
					in oklab, 
					var(--theme-accent), 
					transparent 25%
				)
			;
			
			/* uses NOT over ELSE to respect `prefers-color-scheme` */
			not style(--color-scheme: light): 
				color-mix(
					in oklab, 
					var(--theme-accent), 
					transparent 25%
				)
			;
		)
	;	
	outline-offset: 2px;
	
	filter: 
		drop-shadow(
			0 0 1dvw 
			color-mix(
				in oklab, 
				light-dark(	transparent, var(--theme-accent) ), 
				transparent 5%
			)
		)
		drop-shadow(
			0 0 5dvw 
			color-mix(
				in oklab, 
				light-dark(	transparent, var(--theme-accent) ), 
				transparent 25%
			)
		)
	;
	
	font-family: "Silkscreen", monospace;	
	font-size: 1.75rex;
	letter-spacing: -.075ch;
	text-rendering: geometricprecision;
	
	display: grid;
	gap: 0;
	
	margin: 0;
	padding: 0;
	
	max-width: 30px;
	padding: 6px 12px;
	
	position: relative;
	z-index: 3;
	
	legend {
		color: color-mix(in oklab, var(--theme-accent), transparent 25%);
		text-align: center;
		text-box: trim-both cap alphabetic;
		top: -.5ex;
		position: relative;
		padding-top: .84ex;
	}
}

input {
	--d: 10px;
	--rotation: 225deg;
	
	appearance: none;
	
	background: currentColor;
	color: inherit;
	
	border-radius: 6px;
	corner-shape: notch;
	rotate: var(--rotation);
	
	margin-top: -1px;
	
	outline: 2px solid color-mix(in oklab, var(--theme-text), transparent 50%);
	outline-offset: 2px;
	
	height: var(--d);
	width: var(--d);

	label:hover &:not(:checked) {
		--rotation: 90deg;
		corner-shape: superellipse(-.25);
		outline-color: color-mix(in oklab, var(--theme-text), transparent 75%);
	}
	
	&:checked {		
		--rotation: unset;
		color: inherit;
		outline-color: color-mix(in oklab, var(--theme-accent), transparent 25%);
		outline-offset: 3px;
		corner-shape: superellipse(-.5);
		
		label:hover & {
			outline-color: color-mix(in oklab, var(--theme-accent), transparent 0%);
		}
	}
}

label {
	border-radius: 6px;
	corner-shape: superellipse(-1);
	
	display: flex;
	align-items: center;
	gap: 8px;	
	
	user-select: none;
	padding: 19px 24px 18px;
	
	span {
		text-box: trim-both cap alphabetic;
		display: inline-block;
	}
	
	&:has(:checked) {		
		background: var(--theme-text);		
		color: var(--theme-canvas);
		
		input { color: var(--theme-accent);	}
	}
	
	&:is(:focus-visible),
	&:has(:focus-within) {
		--rotation: 225deg;
		outline-width: 2px;
		outline-style: solid;
		outline-offset: -4px;
	}
	
	&:is(:focus-visible) {
		outline-color: color-mix(in oklab, var(--theme-accent), transparent 50%);
		&:not(:has(:checked)):hover { background: color-mix(in oklab, var(--theme-accent), transparent 84%); }
	}
	
	&:has(:focus-within) {
		outline-color: color-mix(in oklab, var(--theme-accent), transparent 50%);
	}
	
	&:has(:focus) {
		outline-color: color-mix(in oklab, var(--theme-accent), transparent 50%);
	}
}

@media (prefers-reduced-motion: no-preference) {		
	html,
	body,
	fieldset, 
	label, 
	input { 
		transition-property: background, border-color, box-shadow, color, filter, rotate;
		transition-duration: .2s;
		transition-timing-function: ease-out;
	}
	
	label,
	input {
		&:focus-within,
		&:focus,
		&:hover,
		&:focus-visible {
			transition-property: background, border-color, rotate;
			transition-duration: .2s;
			transition-timing-function: ease-in;
		}
	}
}

:root {
	--white: 0 0 90%;
	--black: 0 0 10%;
	--gray: 0 0 45%;
	
	--gold: 36deg 75% 33%;
	--turq: 150deg 55% 35%;
	
	--theme-canvas: light-dark(
		hsl(var(--white)),
		hsl(var(--black))
	);
	
	--theme-text: light-dark(
		hsl(var(--black)),
		hsl(var(--white))
	);
	
	--theme-text-2: ;
	
	--theme-accent: light-dark(
		hsl(var(--gold)),
		hsl(var(--turq))
	);
	
	--color-scheme: light dark;
	color-scheme: var(--color-scheme);
	&:has(#color-scheme input[value="light"]:checked) {	--color-scheme: light; }
	&:has(#color-scheme input[value="dark"]:checked) { --color-scheme: dark; }
	
	container: html / inline-size;
}

@font-face {
	src: url("https://assets.codepen.io/2392/Silkscreen-Regular.woff2");
	font-family: "Silkscreen";
}
	
body {
	accent-color: var(--theme-accent);
	background: var(--theme-canvas);	
	color: var(--theme-text);
	
	box-sizing: border-box;
	display: grid;
	place-content: center;	
	
	height: 100cqh;	
	margin: 0;
	padding: 3rem 3cqw;	
}
  • Colors & Palette: Redefine --gold (36deg 75% 33%), --turq (150deg 55% 35%), or override --theme-accent and --theme-canvas inside :root using custom hsl() coordinates.
  • Corner Geometry: Replace corner-shape: notch or corner-shape: superellipse(-.5) on fieldset and input selectors to alter edge geometry.
  • Switch Sizing: Adjust --d: 10px on input to rescale input indicators, or tweak padding: 19px 24px 18px on label for custom panel height.
  • Typography: Swap font-family: "Silkscreen", monospace and font-size: 1.75rex on fieldset to change retro text rendering.
  • Glow & Shadow Effects: Modify drop-shadow() parameters and color-mix() opacity percentages on fieldset to control light-dark background halos.
  • Rotation Angles: Alter --rotation: 225deg on input to change indicator hover transformation direction.
Responsive portfolio project grid featuring holographic background overlays, container query cards, and stacked image previews.
HTML·CSS ·9 kB

Holographic CSS Project Cards Grid

An advanced portfolio project card layout leveraging CSS Container Queries and modern color functions. Features dynamic light/dark/holo theme switching using :has() selectors and @property animated radial gradients. Staggered card preview stacks expand on hover with pure CSS if() media logic and mix-blend-mode effects. Semantic layout powered by CSS Grid.

Level
Advanced
Browsers
Chrome · Edge
Highlights
Holographic Overlay · Container Queries · Pure CSS Theme · Stacked Previews
Best for
Portfolio Web · Showcases · Agency Landing Pages
Loading preview
<header class="settings">
  <fieldset id="color-scheme">
    <label for="system" tabindex="0">
      <input id="system" type="radio" name="theme" value="system" checked="checked"/>				<span>Sys</span>
    </label>
    <label for="light" tabindex="0">
      <input id="light" type="radio" name="theme" value="light"/>				<span>Light</span>
    </label>
    <label for="dark" tabindex="0">
      <input id="dark" type="radio" name="theme" value="dark"/>				<span>Dark</span>
    </label>
    <label for="holo" tabindex="0">
      <input id="holo" type="checkbox" name="theme" value="holo" checked="checked"/>				<span>Holo</span>
    </label>
  </fieldset>
</header>
<div class="project-container">
  <div class="project project--3 full">
    <div class="preview-container">
      <picture tabindex="0">
        <div class="bg red" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg blue" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg green" alt="fakeCap"></div>
      </picture>
    </div>
    <div class="content-container">
      <hgroup>
        <h1>Project / Client Name</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
      </hgroup>
      <ul class="tag-container">
        <li class="tag design">ui/ux</li>
        <li class="tag design">component library</li>
        <li class="tag design">product design</li>
        <li class="tag dev">interactive prototypes</li>
        <li class="tag dev">html</li>
        <li class="tag dev">css</li>
        <li class="tag dev">documentation</li>
      </ul>
    </div>
  </div>
  <div class="project project--1">
    <div class="preview-container">
      <picture tabindex="0">
        <div class="bg green" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg red" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg blue" alt="fakeCap"></div>
      </picture>
    </div>
    <div class="content-container">
      <hgroup>
        <h1>Project / Client Name</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
      </hgroup>
      <ul class="tag-container">
        <li class="tag design">ui/ux</li>
        <li class="tag design">component library</li>
        <li class="tag design">a11y auditing</li>
        <li class="tag design">wcag conformance</li>
        <li class="tag dev">interactive prototyping</li>
        <li class="tag dev">html</li>
        <li class="tag dev">css</li>
        <li class="tag dev">documentation</li>
      </ul>
    </div>
  </div>
  <div class="project project--2">
    <div class="preview-container">
      <picture tabindex="0">
        <div class="bg blue" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg green" alt="fakeCap"></div>
      </picture>
      <picture tabindex="0">
        <div class="bg red" alt="fakeCap"></div>
      </picture>
    </div>
    <div class="content-container">
      <hgroup>
        <h1>Project / Client Name</h1>
        <p>Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat.</p>
      </hgroup>
      <ul class="tag-container">
        <li class="tag design">ui/ux</li>
        <li class="tag design">component library</li>
        <li class="tag design">wcag conformance</li>
        <li class="tag dev">interactive prototyping</li>
        <li class="tag dev">html</li>
        <li class="tag dev">css</li>
        <li class="tag dev">documentation</li>
      </ul>
    </div>
  </div>
</div>
@property --1-holo-pos {
  syntax: "<percentage>+";
  inherits: true;
  initial-value: 100% 0%;
}

@property --2-holo-pos {
  syntax: "<percentage>+";
  inherits: true;
  initial-value: 0% 100%;
}

:root {
	--scheme-light: light;
	--scheme-dark: dark;
	--color-scheme: var(--scheme-light) var(--scheme-dark);	
	color-scheme: var(--color-scheme);
	
	--primary--h: 25;
	--primary--s: 30%;
	--primary--l: 50%;
	
	--primary-000: var(--primary--h) var(--primary--s) 2.5%;	
	--primary-005: var(--primary--h) var(--primary--s) 6%;
	--primary-010: var(--primary--h) var(--primary--s) 12%;
	--primary-020: var(--primary--h) var(--primary--s) 20%;
	--primary-030: var(--primary--h) var(--primary--s) 30%;
	--primary-040: var(--primary--h) var(--primary--s) 40%;
	--primary-050: var(--primary--h) var(--primary--s) 50%;
	--primary-060: var(--primary--h) var(--primary--s) 60%;
	--primary-080: var(--primary--h) var(--primary--s) 80%;
	--primary-070: var(--primary--h) var(--primary--s) 70%;
	--primary-090: var(--primary--h) var(--primary--s) 90%;
	
	--secondary--h: 206;
	--secondary--s: 12%;
	--secondary--l: 50%;
	
	--secondary-000: var(--secondary--h) var(--secondary--s) 2.5%;	
	--secondary-005: var(--secondary--h) var(--secondary--s) 6%;
	--secondary-010: var(--secondary--h) var(--secondary--s) 12%;
	--secondary-020: var(--secondary--h) var(--secondary--s) 20%;
	--secondary-030: var(--secondary--h) var(--secondary--s) 30%;
	--secondary-040: var(--secondary--h) var(--secondary--s) 40%;
	--secondary-050: var(--secondary--h) var(--secondary--s) 50%;
	--secondary-060: var(--secondary--h) var(--secondary--s) 60%;
	--secondary-080: var(--secondary--h) var(--secondary--s) 80%;
	--secondary-070: var(--secondary--h) var(--secondary--s) 70%;
	--secondary-090: var(--secondary--h) var(--secondary--s) 90%;
	
	--bg: light-dark( hsl(var(--primary-080)), hsl(var(--secondary-010)) );
	
	--spacer-x: 1.75dvw;
	--spacer-y: 18px;
	
	--spacer: min(var(--spacer-x), var(--spacer-y));
	
	--radius-min: 3cqi;
	--radius-001: min(var(--radius-min), 18px);
	--radius-002: calc( var(--radius-001) / 2 );	
}

.project-container {
	--col-w--min: calc(max(390px, 50cqw) - var(--spacer-x));
	
	container: project-container / inline-size;	
	display: grid;
	grid-template-columns: 
		repeat(
			auto-fit, 
			minmax( min( var(--col-w--min), 100% ), 1fr )
		)
	;
	gap: calc(var(--spacer-y) * 2) var(--spacer-x);
	margin: 0 auto;
}

.project {	
	--preview-x: var(--x-init);
	
	background: 
		linear-gradient(
			light-dark( hsl(var(--primary-090) / .5), hsl(var(--secondary-020) / 1) ),
			light-dark( hsl(var(--primary-090) / 1), hsl(var(--secondary-040) / 1) )
		)
	;
	border-radius: var(--radius-001);
	
	container: project / inline-size;	
	
	display: grid;
	grid-template-rows: auto 1fr;
	align-content: start;
	min-height: 50cqh;
	
	&:not(.full) {
		--x-init: 6cqw;
		--x-hover: -1cqw;
		
		.preview-container {			
			picture {
				aspect-ratio: 9/16;
				height: 100cqh;
				
				&:nth-of-type(2) {
					height: 110cqh;
				}
			}
		}
	}
	
	&.full { 
		--x-init: 2cqw;
		--x-hover: -4cqw;
		grid-column: 1 / -1;
		
		.preview-container {			
			picture {
				aspect-ratio: 16/9;
				height: 100cqh;
				
				&:nth-of-type(2) {
					height: 110cqh;
					width: 75%;
					position: absolute;
				}
			}
		}
	}	
}

.preview-container {	
	border-radius: var(--radius-002);
	
	container: preview-container / size;	
	
	display: flex;
	align-items: center;
	justify-content: center;
	
	margin: var(--spacer) var(--spacer) 0 var(--spacer);
	padding: calc(var(--spacer) / 3);	
	
	min-height: 150px;
	height: 60cqw;
	max-height: 60dvh;
	
	transition-property: height;
	transition-duration: .3s;
	transition-timing-function: ease-out;

	will-change: height;
	
	position: relative;

	&:before {		
		background: linear-gradient(
			light-dark( hsl(var(--primary-070)), hsl(var(--secondary-010) / .75) ),
			light-dark( hsl(var(--primary-080) / .5), hsl(var(--secondary-020) / .35) )
		);
		
		border-bottom: 
			1px 
			solid 
			light-dark( 
				hsl(var(--primary--h) var(--primary--s) 95% / 1),  
				hsl(var(--secondary-060) / .45) 
			)
		;	
		border-radius: inherit;		
		opacity: 1;
		
		content: "";
		inset: 0;
		position: absolute;
	}
	
	picture {
		--height-init: 100cqh;
		--height: var(--height-init);
		--shadow-blur: 4cqw;
		--shadow-spread: 0;
		--shadow-y: .25cqh;
		--translateY: 1.25cqh;
		
		border-radius: var(--radius-002);			
		box-shadow: 
			0 
			var(--shadow-y)
			var(--shadow-blur) 
			var(--shadow-spread)
			
			if(
				style(--color-scheme: var(--scheme-light)): hsl( var(--primary-020) / var(--shadow-opacity, 60%) );
				not style(--color-scheme: var(--scheme-light)): hsl( var(--secondary-010) / var(--shadow-opacity, 75%) );
			)
		;
		
		container: picture / size;
		display: grid;
		place-content: start;	
		min-height: 0;
		
		overflow: hidden;
		
		translate: var(--translateX, 0) var(--translateY);
		
		transition-property: box-shadow, filter, opacity, rotate, translate, width;
		transition-duration: .3s;
		transition-timing-function: ease-in-out;
		
		position: relative;		
		
		/* stack <picture> contents */
		* { grid-area: 1 / 1;	}
		
		img {			
			border-radius: inherit;
			pointer-events: none;
			user-select: none;
			width: 100cqw;
			
			&.fill {
				all: initial;
				object-fit: cover;
			}
		}
		
		.project:has(:hover, :focus) & {
			--preview-x: var(--x-hover);
			
			--shadow-blur: 25cqh;
			--shadow-spread: 3cqh;
			--shadow-opacity: if(
					style(--color-scheme: var(--scheme-light)): 70%;
					not style(--color-scheme: var(--scheme-light)): 100%;
				)
			;
			filter: unset;
			mix-blend-mode: unset;
		}
		
		/* inactive state */
		.preview-container:has(:hover) &:not(:hover),
		&:first-of-type,
		&:last-of-type {
			mix-blend-mode: luminosity;
			filter: 
				grayscale(100%)
				if(
					style(--color-scheme: var(--scheme-light)): opacity(50%);
					not style(--color-scheme: var(--scheme-light)): brightness(60%) opacity(80%);
				)
			;
			
			&:focus,
			&:hover {
				filter: unset;
				mix-blend-mode: unset;
				opacity: 1;
				rotate: 0deg;
				z-index: 600;
			}
		}
		
		&:nth-of-type(1) {
			--translateX: var(--preview-x);
			rotate: if(
				media(width > 750px): -3deg; 
				media(width > 480px): -2deg; 
				else: 0deg
			);			
		}
		
		&:nth-of-type(2) {
			--translateY: -1.25cqh;
			scale: 1;
			z-index: 500;
			
			&:hover {
				--translateY: -2cqh;
			}
		}
		
		&:nth-of-type(3) {
			--translateX: calc(var(--preview-x) * -1);
			rotate: if(
				media(width > 750px): 3deg; 
				media(width > 480px): 2deg; 
				else: 0deg
			);
		}
		
		&:focus { 
			outline: 6px solid #ffc32c; 
			outline-offset: 3px; 
		}
		
		&.scrollable {
			cursor: ns-resize;			
			overflow-y: auto;
			scrollbar-width: none;
		}
		
		&:focus,
		&:hover {			
			--shadow-blur: 15cqh;
			--shadow-spread: 1cqh;
			--shadow-opacity: 50%;
			--shadow-y: 3cqh;
			--translateY: 0;
			mix-blend-mode: initial;
		}
	}
}

@media (orientation: portrait) {
	.preview-container {
		max-height: 360px;
	}
}

@media (max-width: 880px) {
	.preview-container {
		max-height: 720px;
	}
}

.content-container {
	display: grid;
	gap: 1lh;
	color: light-dark( hsl(var(--primary-030)), hsl(var(--secondary-080) / 1) );
	padding: 3ex min(6%, calc(var(--spacer) * 3));
	
	hgroup {
		display: grid;
		gap: .5ex;
		margin-block: .5ex;
		
		h1 {
			color: light-dark( hsl(var(--primary-010)), hsl(var(--secondary--h) var(--secondary--s) 95% / 1) );
			font-size: clamp(1.1rem, 5cqw, 1.5rem);
			font-weight: 550;
		}
		
		p { 
			font-size: clamp(0.85rem, 5cqw, 1rem);
			font-weight: 460;
			line-height: 2.4ex;
		}
	}
}

.tag-container {
	display: flex;
	flex-wrap: wrap;
	gap: .5ch;
	list-style: none;
	
	li.tag {
		border-radius: 60px;
		background: linear-gradient(
			light-dark( hsl(var(--primary-030) / .2), hsl(var(--secondary-020) / .81) ),
			light-dark( hsl(var(--primary-040) / .05), hsl(var(--secondary-020) / .36) )
		);
		
		border-bottom: 
			1px 
			solid 
			light-dark( 
				hsl(var(--primary--h) var(--primary--s) 95% / 1),  
				hsl(var(--secondary-060) / .65) 
			)
		;	
		
		display: flex;
		align-items: center;
		text-box: trim-both ex alphabetic;
		font-size: 84%;
		font-weight: 500;
		height: 29px;
		padding-inline: 1.45ch;
		padding-bottom: .18ex;
		user-select: none;
		white-space: nowrap;
		overflow-x: hidden;
		text-overflow: ellipsis;
	}
}

/* Demo Resets */
* { font-family: inherit; margin: 0; padding: 0; }

html { 
	background: var(--bg); 
	height: 100%;
	transition: all .45s ease-out;
	
	&:has(#color-scheme input[value="dark"]:checked) {
		--color-scheme: var(--scheme-dark);
	}

	&:has(#color-scheme input[value="light"]:checked) {
		--color-scheme: var(--scheme-light);
	}
	
	&:has(#color-scheme input[value="holo"]:checked) {
		body {
			&:before {
				background: 
					radial-gradient(
						circle at var(--1-holo-pos) in hsl longer hue,
						oklch(100% 0 80 / .75),
						oklch(90% 0.1 300 / .75)
					),
					radial-gradient(
						circle at var(--2-holo-pos) in hsl longer hue,
						oklch(90% 0.1 70 / .75),
						oklch(90% 0.1 370 / .75)
					),
					radial-gradient(
						rgba(168, 169, 174), 
						rgba(165, 174, 216)
					)		
				;
				background-blend-mode: plus-lighter, plus-darker, overlay;
				animation-name: holo-anim;
				animation-duration: 3s;
				animation-iteration-count: infinite;
				animation-timing-function: ease-in-out;
				animation-direction: alternate;
				mix-blend-mode: color;
				opacity: .5;
				user-select: none;
				pointer-events: none;
				position: absolute;
				inset: 0;
				content: "";
				z-index: 10;
			}
		}
	}
}

body {
	container: body / inline-size;
	font-family: "Libre Franklin";
	padding: 0 calc(var(--spacer-x) * 4) calc(var(--spacer-y) * 4);
	overflow-x: hidden;
	position: relative;
}

@keyframes holo-anim {
	to { 
		--1-holo-pos: -50% 30%;
		--2-holo-pos: 60% 150%;
	}
}

header.settings {
	--header-border--w: 1px;
	border-radius: var(--radius-002);
	corner-shape: square square round round;
	overflow: clip;
	display: grid;
	justify-content: end;
	margin-bottom: calc(var(--spacer-y) * 3);	
	background: light-dark(
		hsl(var(--primary-070) / .5),
		hsl(var(--secondary-005) / .5)
	);
	color: light-dark(
		hsl(var(--primary-090)), 
		hsl(var(--secondary-010))
	);
	
	fieldset {
		border: 0;
		margin: 0;
		padding: 0;
	}

	label {
		cursor: pointer;
		display: flex;
		align-items: center;
		font-weight: 720;
		user-select: none;
	}
}

fieldset[id="color-scheme"] {
	display: flex;
	flex-wrap: wrap;

	label {
		--gap: 1.25ch;
		color: light-dark(
			hsl(var(--primary-030) / 0.75),
			hsl(var(--secondary-060) / 0.75)
		);			
		flex: 1;
		font-size: 1.25ex;
		margin: 0;
		padding: 3ex 1.5rem;
		text-transform: uppercase;

		input {
			color: inherit;
			appearance: none;
			border-radius: 2px;
			outline: 2px solid currentColor;		
			outline-offset: 2px;
			height: 0.5rem;
			width: 0.5rem;
			transition-property: outline, background;
			transition-duration: .15s;
			transition-timing-function: ease-out;

			&:checked {
				background: light-dark(
					hsl(var(--primary-060)),
					hsl(var(--secondary-010))
				);
				outline-color: light-dark(
					hsl(var(--primary-060) / .5),
					hsl(var(--secondary-010) / .5)
				);
			}
		}
		
		span {
			margin-left: 0.75rem;
		}

		&:hover {
			background: light-dark(
				hsl(var(--primary-090)),
				hsl(var(--secondary-010))
			);
			color: light-dark(
				hsl(var(--primary-040)),
				hsl(var(--secondary-090))
			);

			input {
				outline-color: currentColor;
				&:checked {
					background: currentColor;
				}
			}
		}

		&:has(input:checked) {
			background: light-dark(
				hsl(var(--primary-090)),
				hsl(var(--secondary-050))
			);
			color: light-dark(
				hsl(var(--primary-030)),
				hsl(var(--secondary-010))
			);
		}
		
		&[for="holo"] {
			--icon-unchecked: "✧";
			--icon-checked: "✦";
			--icon-opacity: .5;
			position: relative;
			
			input {				
				outline: 0;
				border: 0;
				width: 0;
			}

			span {
				margin-left: 1.15rem;
				&:before { 
					content: var(--icon, var(--icon-unchecked)); 
					font-size: 1.2rem;
					opacity: var(--icon-opacity);
					position: absolute;
					left: 1.5ch;
					top: .75ex;
				}					
			}

			&:has(:checked) {
				--icon: var(--icon-checked);
				--icon-opacity: 100%;
				input { background: none; }
			}
		}
	}
}

.bg {
	height: 100cqh;
	width: 100cqw;
}

.red { background: oklch(60% 50% 10deg); }
.blue { background: oklch(50% 60% 220deg); }
.green { background: oklch(70% 70% 110deg); }

@font-face {
	font-family: "Libre Franklin";
	src: url("https://assets.codepen.io/2392/LibreFranklin.ttf");
	font-style: normal;
	font-weight: 100 900;
}
  • Colors / Themes: Tweak base HSL color channels --primary--h: 25 and --secondary--h: 206 on :root. Customize preview card background blocks using .red, .blue, and .green selectors with custom oklch() values.
  • Gradients & Visual Effects: Edit the animated holographic foil overlay inside html:has(#color-scheme input[value="holo"]:checked) by altering radial-gradient stops and @keyframes holo-anim targets --1-holo-pos: -50% 30%. Adjust image card stack blending via mix-blend-mode: luminosity.
  • Sizing, Layout & Scales: Modify responsive grid column width --col-w--min: calc(max(390px, 50cqw) - var(--spacer-x)) on .project-container. Customize image stack fan-out distance by changing --x-init: 6cqw and --x-hover: -1cqw on .project.
Glassmorphism capsule card featuring realistic liquid glass refraction and chromatic aberration SVG filters.
SVG·HTML·CSS ·11 kB

Liquid Glass Distortion Card in CSS and SVG

A ultra-realistic liquid glass frosted capsule element combining CSS backdrop-filter with SVG displacement shaders. Employs embedded feImage displacement mapping and split-channel feDisplacementMap RGB filters to reproduce physical optical refraction and chromatic aberration over scrolling background content. Styled with layered inset lab() shadows.

Level
Advanced
Browsers
Chrome · Edge · Safari
Highlights
Liquid Glass Effect · Chromatic Aberration · SVG Backdrop Filter · Glassmorphism Capsule
Best for
Overlays · Navigation Bars · Hero Visuals · Modern UI Cards
Loading preview
<div class="liquidglass">
  <svg class="glass-surface__filter" xmlns="http://www.w3.org/2000/svg">
    <defs>
      <filter id="glass-filter-_r_b_" color-interpolation-filters="sRGB" x="0%" y="0%" width="100%" height="100%">
       <feImage
  x="0"
  y="0"
  width="100%"
  height="100%"
  preserveAspectRatio="none"
  result="map"
  href="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAApQAAAIdCAYAAACDcO0sAAAQAElEQVR4Aey9iZrcOo+k7bdnX3u23u7/Qj0OyUhCEEhRSmVVVhXOc2ACEQGQDLsy+ft8/c8//Pr167cC+G3xD//wD78t/t2/+3e/ffz7f//vf1v8h//wH35b/Mf/+B9/W/yn//Sfflv85//8n3/7+C//5b/8tviv//W//rb4b//tv/22+O///b//9vE//sf/+G3xP//n//zt4x//8R9//6OL//W//tdvi//9v//3bx//5//8n98+/u///b+/Y/y///f/fsf4p3/6p98x/vmf//l3Fv/yL//yuxf/+q//+nsm/u3f/u33Z8TM2aTp3c/wzBdh0UPV0WvV8fdEtf99U+5/Xy2333db/Z8L5f7PjXL7M2Wr/zOn3P482mp/Vv1qf5Zt9X/WldvPgV/t58Sv9nPkV/s5i6v/eYy5/dz2VvsZP7Pq86Fi/ZwsH8qH+jNQfwbqz0D+Z0APyj/e/Mx/f//WW/r97v7R59J+ihknjnQ9PsOvYjN9UfOK+uxM+Zv1RCzTGZZpZ7ler/otpIlhXK2f7UDtXw6UA+XA+zrwox+UH/3boi/q2T3PaGdnRp32UES8V4+04hRZb4Y/g8U94qyPrnWemT2PNDYn00XMazPO88p7oV4fPV3h5UA58PUd+PNfJn5V8DIPvv6fkOdu8JYPSn3BPXettTvOifWqar+eyXqzeviZ2abVLIXVd62aqZidJ62ipz/LZfqrWOz7jPrsnvIx64lYphOmyLSGi1OozkKcRcYXVg6UA6934DMedq+/1c/eYeb39Ds79JYPyu9suL7Iz97vSk+2h+YoMq6HHelHfMbdicVZ71DPnOFIo98LaRTKfQhTeEy5MIXyLMRZZHxh5cAXc+DtjjvzmPCat7tAHehDHPB/Biz/kI0/YJMv96DUl+IrfXn1fJ39yh7qsdCMmTC9rTM9XqM+X8d8xGfcnVic9Y710ZnkZ9SMsJ42w22OOIXqinKgHHjeAXsExPX5yd9zQvTpO9bP/s5FT56d91n9X+5B+RlGnf1CPqs/eyfNn4mzc02/zD74P1iSxvRxzbgMi32qM13Evlqd3Ut3UIizUK2wWqtqhXIfwhQes1y4wupay4Fy4JoD8Yte9bVJ79Gl8390vMfNX3uKWU9nTxHnzfZ9tq4elE/8Djzzpf1M7xNHPmydOddIk3EZpoNEPNYzmtjzjvXRmbJ7jrA4T1qFcIXyinKgHLjugH2hX5/w2k47n62z62tPVdOPHOj9Pp3pO9J+Jl8Pyhe5P/PFPqN50fF2Y3UWxY4IwEiTcRmmkRGP9Ywm61GfReQ/u9a5sjNcxTRPoX6F8opyoBy45oD/sr824Z4uf45efs9OX2tKz4vPwO92Lt5hNN9rR7rP4H78g/Kzv4g/e3/9oZs5gzQK6WMIV2R4xFRnWuE+oibWXmv5SBO5c/WvX3foj2boHlHTwwzP9OIqyoFyYN4BfUnPq+9Ras8s7pl+75TsnB+N3Xuj56ZdufuZHf38UZ/pRpqP5A4flPWFtf529Hzo4eoaceItZnWmv2vVvoqjeSNNjzuDR+1RrfMeaTzv87O9r9JnZ5rB7DxRK7yiHCgHzjugL+XzXec7tI+P8xOe7/D7n8mf3/kDJ7zpVj2/j47r+3raGU2v90788EH57Gav/uJ7xfxXzDzy8SP31F6KozOJH+l63Bk8amOtM8SImqPa9x9pP5rX2eKeIyzTSl9RDpQD5x3QF/H5rrkOzfYx13Vd5ffq5denV+erHIi/V6N9vLanM02PfyX+8gfl7OE/6ovyo/axe5/Z74zW5p9ZNV8x2zPS9rgzeNTGWueM2DP1M73xLJqlEK5QrlCuUK5QbpHVVzGb+QlrbVkOfAsH9MV790U00+Lu2TbP5sfV+M9a43mqbv8v8pz5PYm+9Xq9LtMYn3Gvwt7mQTm6YPzSPaM90zuaK643q4er52xoluJs30iveYqRxnPSKjzm8x53Bo/aWGu/iD1TP9vr+30+e86ZnqjJZgurKAfKgecc0JftcxO23Zqn2KLPV5oZ4/mpxxPinjP18dSfq+j5N+OI7+3pTbPnf/0acb9u/id9UGZfbDfv+1bjXn3fK/PVo7hqlHotzsxQz0jf48/gURtr7R+xZ+rP6u3d4+g81hd1wivKgXLgfRy4+8va5tn6ipva7NH6in1r5t6B7Pdgr2qI1ze0ZcY3pGUjrqmey9IH5XMj9/8Xsc/O+4z+s1/mR/ojvndH9V2J3rwebnv0eOHSaI1xBo/aWGt2xJ6tNdPizKwzWs0/q+/1xDnSVbynA3Wqr+eAvlifPbVmKO6ao1mKZ+dZv2b1wjQfvfbO853xKx57P0b9I90MN5p9lXs8KD/jS+wz9rxqlPU9c+Znem3/V6xH5xKvyPY+g0dtrDU/YnfWZ2ad0V4999EemltRDpQD7+WAvqyfOZH6LZ6ZY702y6/GvWL1+5zJX3GWd5858mfm7LG/12O6jO9xwjP9M9jjQfnMEPXGL0dhz0c+4dm9nu3PTzX3N7Ov2rt3phGusyiOND2+15vhGRbnRs2d9ZlZZ7S6wx36OENzK8qBcuB9HNAXsOLqidSruNpvfZrhw/A7Vj+3l9+xT8349fjfNnqffx38c6Q1PhuTcRmW9c5itz0oZzec1Z35gj3SHvE6U6bJsJ5WuKLXI85iRmPaV6zaX3E0e6TpcRk+g0XNnfWZWWe1Xq9cYb4qV1it9aiWpqIc+JIOfOND64v36vXUq3i2XzMUV+f4Ps3Jwms+Ms/O8tWxK/7FO49mmDbTHHGxZ6SP2lH91IMyfjmONjLuSo/1zq4fscfsWXo6nVHR41+Baz/F0WxpFD1dxglTxJ4ZLGrurM/MepVWnmi2QrlFrA2vtRwoB76+A898SVuv1med0IwYz87M+uMeZ+ps3lfHRvefvZuf0esZaYyLvWfx2N+rn3pQ9oa+Ar/7y3d2Xk/Xw3X3ESfexwmtbzuVaw/FTNNIJ04R52SYNBkesVfWZ2aPtOIUupPC51frOENzKsqBcuD9HNCX79lTXenRHupTKL8S6o1xZU7siTOzOvZU3Xfgin++pzfZNJG/gscZM/XpB+WVL8LZnlndzMWuaO7c/8wsaRVXztzr0TyLnsbjR1rxXm/5LC6dwvq0vrI+M3ukHXF33EEzKsqBcuA9HdAX8ZmTSa/49etM16/H/57u14V/tJ/FhfZNi82J60b0CUU8zzvXV+2JdxrN8dpMZ3zkzuA9bZzp66kHZfxS9QM+Kz97prN6f69ebw+33iPedLZK78PwmdX3KZ/pMc2RvsdnuDCFzdYa6wyLmmfqM70j7YiLd5BWIVyhXKFcoVyhvKIcKAe+nwP6Aj57K/UorvZd6bW91BvDuDvXuMeV+s7zvHrWzP1mzhDn9HpMl/E9boTHOdJGrFdPPSh7zSN89stzpBtxce+ojXXUq840GSZtL470R3xvrnD1zob0Z8Nm9/pGvLjYdxWLfTO139vrfS7NqL7KxbmjOVGruqIcKAfe34EzX6RntLq59ArlsyG9xWyP6azPr8Y9s/p5vfyZ+d+1N/Pq6K6+J9OOeONiX4bPYnGW6u6DMn5JSvyT4xk/nul9hec6j2I0e8Rn3FUs9j1Tn+kdaY84z/tcfh7V0lSUA+XA93FAX8BnbnNFf7ZH51GPheqrYTOydXLmh8iy830UdscF41lHM7020xkfuTN4ps0wv8fmQRm/DL1wJp/tn9XFPa/22ZysfxazGdmazYg6aRQR/8ha+ytGe4pXZBrhishdxWLfM/WZ3pH2KidPRr3iK8qBcuDnOnD0ZRydOatXv/VoVX021Bfj7IxZfdzn2Xp231foZs9+Zm8/c9RnukzT40Z4nCPtDCbN5kEp4GzEL9Gz/VF/Zl7UHtVxr7N1nO/7R1zUzWp939Vce1kczZCup+lxGT6DRc0z9ZnekfYZzvcqV/S8vB2vgeVAOXCrA9mXaLbB3TrtoZkK5TMhrcWM3musz1bPXc1t1tF6df5X7ut5cnQn39fTmibje1yGP4NdelBe+bIc9VxlMuOOsGyvWUyzM61wxYgT70NahcfuzDVbMTNTOkVP2+MyfAaLmmfqM70jreeUK8wPnwvztc8jp7qiHCgHvqcD+uKdudkZ3axW+0qrUD4b0vuY7Ys6PyPmUVv11oGsOuOh1x7Nirz1zuCZtofZvOkHZfzitAGj9UrPaJ64szPP6rXHUYxmilMczTBeWgvDrqw2w9aZGUfaHj/C477SeuzO+swsr1WusHP1cvGRi7U0Fp4zrNZyoBz4uQ7oC3jm9rM6zZJWoXwmpLWY0UeN9fo1au6q/R7fLT/jUbx7r9frMo3xkRvhmfYIs3nDB+WVL8jZnpFuxMWLRW2so151ppnF1K/I9MItjnjT+VU9V8PPOcptj5FOmozPcGEKr1etiNhd9ZnZXutzncXXPj/DRa3qis9yoPYtB8YOxJ/zsTpn9QWaMw2d0Uh9Rjertbln9OpRqMeHsGfDzxvlz+7zzv29e8+c2ff29KbJ+B6X4c9guwfllR+22Z5ZXTTkap/NeaZ/1DvitLd4hfJ3CJ1FMTqLeEWmyfCrWOw7U4+04hR2/l4u/i7Oz9HcinKgHCgHZhzQl/eRThrFkU68dBaqZ8L0ts70ZBrrz9ZMX9jqwFm/vH6dsP11xBu37fiV/v9YP9P2sF9//9k9KP/i3cWIu79Ez8yL2qPazuzX2CMuw0b4ESde0Zsr7iNC+ytGe4lXZBrhishdxWLfmXqk/Wwu+lN1OVAO/EwH9MV7dPO7NNpHsxTKZ0Jaixl91FivX6Pm7trv9ZXyKz74+436j3TGxxkjPNMeYTbv1IMyfmHHTbJ61HOVy/Y5wkZ7+d6eroerd8SJV0hjofrVYXtpPdprpOlxGR4x1Qq//zP1qPfVnOYr7C4+N6zWcuDNHKjjfIID+nI92vZOzcwsO4+0CqtnV/X4mO0b6fy8mXw065250d1mzh37ez2my/gel+HPYIcPyitfnFd6MhM8Fmeerf0sy+OMq7j6erPExZDWInJXa5tn68yckbbHjXC/p3S+Vh6xM/VIewenGQqdU9HLI6e6ohwoB76WA/7n++zJ9YV7tsfrj/rFK3xPls9o1CedheqZML2tMz2ZxvqzNdP/NOyKL74n88v4M1zWcwazvdIH5ZUfttmekW7E2YGfWbP5GTbaY6MPQnGKAA9L6WMMG/6QUa/6Dzz1r7QWvQbxGZfhwhReH2txETtTj7R3cFdn6F4V5UA5UA7MOqAv6ZH2iFevNArlo5BGMdJ4TloLj8/k1hfXmd67NHHvz66v3iueezTHtJnGOK2RF6bI8CuYZinSB2Uc2KvjF/EV3WhG5J6tdb444wwmrSKbIdxCvMLqs6t6R3F2nvQ2T3kvepoRHmdJ6zHVioj1amkVxvtcmK99PuKkU0ij6OUjTj0KaSrKgXLg6znwESfWl+rVfWZ6ZzUzOp1TOgvVs2E9ts72jQV1sMzb/X4kEbhsaty+Sp6ftIoMl2FR+2ozvpaes/53Hqz0+uqPOvrYZVXhff8erz37WkXvzawNrA/kGYO6xB3P6fOp9KHz2/e775J/v/HxQfv5V1w0/fQMzD4AZbfa5R/pHNN67p0///S5eS97X/8P9F8K0Nf7f9X7OnpE+W1es6n7XvBnv9uIUbv5unvV6tHof66Y9Gz+i+Z8X7e9Snt7X4vWglFAclAPlQDlQDpQDJxzIPuhGv4Z+/Z7F9Nn8EXuM6OnV4qM+GfX6vP9S9PnV/bN3+L3+p6fG3xH27shZf9U3q6vOeh9yX9gZ9g/36kEpMlLK9+l8PVDgOVAOfFYHcujD+RpeX0f39XfMvGeWf4vXj8T8rM8M8zPhXn/UqE+8RuzV0rQ4K9+KnfGmv8s3sD6erx6UPWcKLwfKgXKgHCgHfsEBe7D1/kZ8b89or9F+Prc+/0zv3R9/t79/y0u8YuxL1GfWb+mP6v38WnzE+bHOfbXfV96yD9j+reT1oPyWb0BtWQ6UA+VAOVAPnOfAnzxcPHN7vPfO9PZ6u/uT9p7uK0f/r97nOf38p/uY4fHReS2vXhNreYn/JeXz9+Y8KOfcrFmHDoD2GPCgdW1gbeDnbuArH6y9Dzo3GjOayEevWToL1Ufz9zKszuh9RuxrRms+Z+b3+BlMf6SReZ+NuVdLezXywt7XU+WteT0orzhYPeVAOVAOlAPlwK84YI+Xz8+X6OnzE76F/R2YmUf7qFec7U3zI/r0MGPWv7Onm9FmHsh7g+0fI9vVvE4PrI7v0X3y3Z56UFaX039KB4DNZ8Eez6bA7p7A42cAew2Mf9K0WfA+mNNZ6P7Ies3Y38O2D8YwjX9Fhvf0MGL66FmY93oRP/N7es5jI06YIuPX+vT3qAclUPlf08C3bODZhzvE16v9r/yB/WfG6rE76vE9D6v/AunO8D59Gf3p/jP9M/NRPfLpYQonPeIpnWJEex7GvT9zXvFh5yN8b8z79Xf+7O9gLff3U/WgvOJi9ZQD5UA5UA6UA5/kgD1eOj0i/iAn/C0unR6vMzzb90Q+es+Xj849Yp8T/4u9H0S7bZ6F+2dY9K6ofjOfpXUWe49e3vIqXJzpz2rU2uYpfXUez9XbeYdYv63MfyoLXRv4eRswenSdwcIUR73W9+rpRzyPzWjpWlzG3f3Z+97pGekjToxqTsujWv6pD8ofv38VvzbwpQ0AD//S89n/Kef+q3bA9p9vV04n3nLY6mGPe2M8Y7I0Pka9bY491ss94R6vUfN4f0XEz+jP3Bv2pT69Z5v5Ue/+rR6U3uGqf8YGoP9ogcYDKrcBLL9pAMtz8InRymcY9X91K670+OjxGvXe6SreKzO9vGf56H3O0GfeAcaPsK3VM9rsc8X6YfWgvOpi6csB4PFoAmP8KIFV+8D0Y0Z8R7W+P7Mv4Z6re3tGzXnN9On94/F+uD83M3+H5mZzzT3D488C9v4r3+2pB+XgVnXlQDlQDpQD5cC/cgPY9wZg/bbyb93An7Yg+H9vA1c8f+R8G/PThB/+H/fVf0m8v+C/xH6rV6/VnZ/+L9R5rZ26t7eU+Zf0mWe1ZqbeW0X+XgP9rN9O97X8vD3++7hUD0r9E+XlQDlQDpQD5cC9DhRP7T/p9Gf7L7EvV8vbeE+/j6iN++Xv6m9xrZfH3pXby+LkfdbL9Fev7mU+37PzF1gUf9bLfbZ76/P/mHclV6vP1s98Zq/nfbZnvZ0N+F072D0or9pefeVAOVAOfFcHeoxE9H9O9P/R1f+TPhv1R67K8YyO9mP9bO9oZp7ofZ+969X8M72z82fna8/V971T/V4vT4X/pZz/qI66K4t6/v4D+v3XyEdfV/ovOf/+H/8bC/+r6nOfE79H/T+K99rC+hWw2B/5T2FffgKofYDN94bN8F4/Z8byR/K1ZzUrf/O/+rXf67/+D/wQ6pA3/uL8jC3O7/tX/f7f/gX9vI/pZ/sD087sT8U+s/oYf/+ZtWfrb8D/3D09u7N+6pWfP6XzX/F6zL6m/6D0p8vKgXKgHCgHyoFy4L9gAxH6XfO5b4b9bK/fPZ//r/Y39f/bAetgO99f5X9D+XUerz7D3f7L/D0925m/+L/gP60OfvH9V/v9F73O/L+iM/+7+q+xL74v/k/R/L0BWB/vWf/K++L/g/+E+n/+93/4H/D/b/+E+X+g+R9Y/h8AAAAASUVORK5CYII="
/>

        <feDisplacementMap in="SourceGraphic" in2="map" id="redchannel" result="dispRed" scale="-20" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
        <feColorMatrix in="dispRed" type="matrix" values="1 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 1 0" result="red"></feColorMatrix>
        <feDisplacementMap in="SourceGraphic" in2="map" id="greenchannel" result="dispGreen" scale="-24" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
        <feColorMatrix in="dispGreen" type="matrix" values="0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 0 0 1 0" result="green"></feColorMatrix>
        <feDisplacementMap in="SourceGraphic" in2="map" id="bluechannel" result="dispBlue" scale="-28" xChannelSelector="R" yChannelSelector="G"></feDisplacementMap>
        <feColorMatrix in="dispBlue" type="matrix" values="0 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 1 0" result="blue"></feColorMatrix>
        <feBlend in="red" in2="green" mode="screen" result="rg"></feBlend>
        <feBlend in="rg" in2="blue" mode="screen" result="output"></feBlend>
        <feGaussianBlur in="output" stdDeviation="3"></feGaussianBlur>
      </filter>
    </defs>
  </svg>
</div>
<h1>This is a HTML text</h1>
:root,:host{ 
    --glass-frost: 0.1;
    --glass-saturation: 1.2;
    --filter-id: url(#glass-filter-_r_b_);
}

body {
    margin: 0;
    padding: 0;
    min-height: 200vh;
    display: flex;
    flex-direction: column;
    align-items: center;
    justify-content: center;
    font-family: sans-serif;
    background: url(https://images.unsplash.com/photo-1551384963-cccb0b7ed94b?q=80&w=3247&auto=format&fit=crop&ixlib=rb-4.1.0&ixid=M3wxMjA3fDB8MHxwaG90by1wYWdlfHx8fGVufDB8fHx8fA%3D%3D) center / cover;
  overflow:auto;
  color:white;
}

.liquidglass{
    position: fixed;
    left:50%;
    top:50%;
    transform:translate(-50%);
    z-index: 1;
    width: 340px;
    background: hsl(0 0% 0% / var(--glass-frost, 0));
    -webkit-backdrop-filter: var(--filter-id,url(#glass-filter))saturate(var(--glass-saturation,1));
    backdrop-filter: var(--filter-id, url(#glass-filter)) saturate(var(--glass-saturation, 1));
    box-shadow: inset 0 0 2px 1px #ffffff59, inset 0 0 10px 4px #ffffff26, inset 0 4px 16px #11111a0d, inset 0 8px 24px #11111a0d, inset 0 6px 56px #11111a0d;
    box-shadow: inset 0 0 2px 1px lab(100% 0 0 / .35), inset 0 0 10px 4px lab(100% 0 0 / .15), inset 0 4px 16px lab(5.32203% 1.61424 -5.88284 / .0509804), inset 0 8px 24px lab(5.32203% 1.61424 -5.88284 / .0509804), inset 0 6px 56px lab(5.32203% 1.61424 -5.88284 / .0509804);
    border-radius: 9999px;
    height: 140px;
    pointer-events: none;
}
.glass-surface__filter{
    pointer-events: none;
    opacity: 0;
    z-index: -1;
    width: 100%;
    height: 100%;
    position: absolute;
    inset: 0;
}
  • Colors / Themes: Tweak glass opacity via --glass-frost: 0.1 and saturation via --glass-saturation: 1.2 in :root. Adjust capsule inner reflections on .liquidglass via box-shadow inset lab() color stops.
  • Gradients & Visual Effects: Modify chromatic dispersion scales in SVG by changing scale="-20" (red), scale="-24" (green), and scale="-28" (blue) on feDisplacementMap filters. Adjust edge softening using stdDeviation="3" on feGaussianBlur.
  • Sizing, Layout & Scales: Resize glass capsule container via width: 340px and height: 140px on .liquidglass. Alter corner curvature using border-radius: 9999px to convert from pill capsule to rounded box.
Pure CSS Cafe Wall optical illusion with parallel lines distorted by conic gradients, revealed on hover.
CSS ·1 kB

Optical Illusion Pattern in Pure CSS

An optical illusion rendered entirely in CSS using zero HTML elements. Employs pseudo-elements with skewed repeating-linear-gradient and conic-gradient layers to create the perception of non-parallel lines. Hovering over the viewport transitions the @property --color variable to transparent, instantly disabling the visual distortion and revealing parallel horizontal lines.

Level
Intermediate
Browsers
Chrome · Edge · Safari · Firefox
Highlights
Optical Illusion · Pure CSS · Animated Property · Hover Reveal
Best for
Interactive Demos · Hero Backgrounds · Creative Canvas
Loading preview
/*
optical effect
The black lines are parallel, but they look like they are not. Mouse over to reveal
*/
@property --color {
  syntax: '<color>';
  initial-value: #f00;
  inherits: true;
}

body {
  --color: #f55;
  margin: 0;
  font-size: 1vmax;
  height: 100vh;
  overflow: hidden;
  transition: --color 1s;
}

body:hover {
  --color: #0000;
}

body::before {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 300em;
  height: 300em;
  background:
    repeating-linear-gradient(#0000 0 18em, #000 0 20em),
    conic-gradient(at 1.25em 10em, #0000 75%, var(--color) 0) 0 -6em / 2.5em 20em;
  transform: skewY(-45deg);
}

body::after {
  content: "";
  position: absolute;
  top: 50%;
  left: 50%;
  translate: -50% -50%;
  width: 300em;
  height: 300em;
  background:
    repeating-linear-gradient(#0000 0 18em, #000 0 20em),
    conic-gradient(at 1.25em 10em, #0000 75%, var(--color) 0) 0 -6em / 2.5em 20em;
  transform: rotate(90deg) skewY(45deg) translate(0.25em, 12em);
}
  • Colors / Themes: Change distortion tile colors by modifying --color: #f55 on body and initial-value #f00 inside @property --color. Update hover transparency target --color: #0000 to alter reveal contrast.
  • Gradients & Visual Effects: Adjust line frequency by tweaking repeating-linear-gradient(#0000 0 18em, #000 0 20em) offsets inside body::before and body::after. Modify pattern angles via conic-gradient(at 1.25em 10em, ...) tile dimensions / 2.5em 20em.
  • Sizing, Layout & Scales: Rescale pattern density by altering font-size: 1vmax on body. Tweak illusion distortion strength by adjusting skew angles transform: skewY(-45deg) and skewY(45deg).
Scroll-driven text assembly animation using CSS sibling-index, math functions, and scroll-timeline.
HTML·CSS ·3 kB

Scroll Driven Text Unscramble in CSS

A scroll-driven text assembly effect built without JavaScript. Uses CSS animation-timeline: scroll() paired with @property --page-offset to transition letter positions on scroll. Random dispersion is computed natively using sibling-index(), sin(), and mod(). Fully accessible structure with aria-label readable words and aria-hidden individual letter spans.

Level
Advanced
Browsers
Chrome · Edge · Safari
Highlights
Scroll Driven · Pure CSS Math · 3D Text Assembly · Accessibility Ready
Best for
Hero Sections · Landing Pages · Storytelling Web
Loading preview
<section class="scramble">
  <p aria-label="Meaning">
    <span aria-hidden="true">M</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">a</span>
    <span aria-hidden="true">n</span>
    <span aria-hidden="true">i</span>
    <span aria-hidden="true">n</span>
    <span aria-hidden="true">g</span>
  </p>

  <p aria-label="emerges,">
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">m</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">r</span>
    <span aria-hidden="true">g</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">s</span>
    <span aria-hidden="true">,</span>
  </p>

  <p aria-label="one">
    <span aria-hidden="true">o</span>
    <span aria-hidden="true">n</span>
    <span aria-hidden="true">o</span>
  </p>

  <p aria-label="letter">
    <span aria-hidden="true">l</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">t</span>
    <span aria-hidden="true">t</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">r</span>
  </p>

  <p aria-label="at">
    <span aria-hidden="true">a</span>
    <span aria-hidden="true">t</span>
  </p>

  <p aria-label="a">
    <span aria-hidden="true">a</span>
  </p>

  <p aria-label="time.">
    <span aria-hidden="true">t</span>
    <span aria-hidden="true">i</span>
    <span aria-hidden="true">m</span>
    <span aria-hidden="true">e</span>
    <span aria-hidden="true">.</span>
  </p>
</section>

<div class="icon">
		<svg xmlns="http://www.w3.org/2000/svg" width="40" height="40" viewBox="0 0 24 40" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mouse">
			<path stroke="none" d="M0 0h24v24H0z" fill="none" />
			<path d="M6 3m0 4a4 4 0 0 1 4 -4h4a4 4 0 0 1 4 4v10a4 4 0 0 1 -4 4h-4a4 4 0 0 1 -4 -4z" />
			<path d="M12 7l0 4" />
			<path d="M8 26l4 4l4 -4">
				<animateTransform attributeType="XML" attributeName="transform" type="translate" values="0 0; 0 4; 0 0" dur="1s" repeatCount="indefinite" />
			</path>
		</svg>
	</div>

<footer>
  <a href="https://codepen.io/cbolson/pen/NPrrpzJ" target="_blank">See Circular version</a>
</footer>
@import url(https://fonts.bunny.net/css?family=noto-sans-mono:400);
@layer base, mouse, demo;

@layer demo{
  @property --page-offset {
    syntax: "<number>";
    inherits: true;
    initial-value: 75;
  }
  :root {
    --cta: rgb(94, 165, 0);
    --mouse-color: var(--cta);
		--first-word-color: var(--cta);
    --letter-spacing: 1ch;
    --f-size: clamp(1rem, 2.5vw + .045rem,  1.5rem);
  }

  body {
    height: 800svh;
    font-family: 'Noto Sans Mono', monospace;    
  }
  
  .scramble {
    --count: sibling-count();
    position: fixed;
    left: 50%;
    top: 50%;
    translate: -50% -50%;
    display: flex;
    align-items: center;
    flex-wrap: wrap;
    gap: 1ch;
		
		animation: --page-scroll 1s linear;
    animation-timeline: scroll(root block);
    animation-timing-function: steps(100);
  }
  
  .scramble > * {
    perspective: 300px;
    transform-style: preserve-3d;
    margin: 0;
    display: flex;
    align-items: center;
    gap: 0;
  }

  .scramble > *:nth-child(2){
    color: var(--first-word-color);
  }

  .scramble > * > span {
    --i: sibling-index();
    --dir: calc(1 - 2 * mod(var(--i), 2));
    --rand: calc(mod((var(--i) * 7 + 20), 4) - 4);
    --wave: sin(calc(var(--i) * 17deg));
    
    /* y position */
    --start-y: calc(
      (var(--rand) + var(--wave) )
      * var(--dir)
      * var(--page-offset)
      * 1px
    );
    
    /* depth */
    --z: calc(abs(var(--rand) * 2 + var(--wave)));
    --depth-factor: calc(var(--page-offset) * 0.02);
    --start-z: calc(var(--z) * var(--depth-factor) * 13px);
  
    transform:
    translate3d(
      0,
      var(--start-y),
      var(--start-z)
    );
    
    display: inline-block;
    position: relative;
    font-size: var(--f-size);
  }
	
	@keyframes --page-scroll {
    to {
      --page-offset: 0;
    }
  }
}

/* the mouse icon is just to indicate scroll, it is not relevant to the demo */
@layer mouse{
  .mouse {
    pointer-events: none;
		position: fixed;
		bottom:10px;
		left: 50%;
    translate: -50% 0;
		display: block;
		width: 50px;
		height: 50px;
		opacity: 1;
    color:var(--mouse-color);
    display: none;
		animation-name: mouse;
		animation-duration: 1s;
		animation-timing-function: linear;
		animation-fill-mode: forwards;
		animation-timeline: scroll(root block);
	}
  @supports  (animation-timeline: scroll()) {
    .mouse {
      display: block;
    }
  }
	@keyframes mouse {
		95% {
			opacity: 1;
		}
		100% {
			opacity: 0;
		}
	}
}

 /* general styling not relevant for this demo */
@layer base {
	* {
		box-sizing: border-box;
	}
	:root {
		color-scheme: light dark;
		--bg-dark: rgb(12, 10, 9);
		--bg-light: rgb(248, 244, 238);
		--txt-light: rgb(10, 10, 10);
		--txt-dark: rgb(245, 245, 245);
		--line-light: rgba(0 0 0 / .25);
		--line-dark: rgba(255 255 255 / .25);
    
    --clr-bg: light-dark(var(--bg-light), var(--bg-dark));
    --clr-txt: light-dark(var(--txt-light), var(--txt-dark));
    --clr-lines: light-dark(var(--line-light), var(--line-dark));
	}
 
	body {
		background-color: var(--clr-bg);
		color: var(--clr-txt);
		min-height: 100svh;
		margin: 0;
		padding: 1rem;
		font-family: system-ui, sans-serif;
		font-size: 1rem;
		line-height: 1.5;
    display: grid;
    place-items: center;
    grid-template-rows: 1fr auto;
	}
	@supports not (animation-timeline: scroll()) {
	 body::before {
			content:"Sorry, your browser doesn't support animation-timeline";
			position: fixed;
			top: 2rem;
			left: 50%;
			translate: -50% 0;
			font-size: 0.8rem;
		}
	}
  footer a {
    font-size: .8rem;
    color: var(--cta);
    z-index: 2;
    text-decoration: none;
    display: block;
    transition: transform 150ms ease-in-out;
  }
  footer a:hover {
    transform: scale(1.12);
  }
}
  • Colors / Themes: Modify key accent variable --cta: rgb(94, 165, 0) in :root. Tweak target element highlight on .scramble > *:nth-child(2) using --first-word-color: var(--cta).
  • Gradients & Visual Effects: Adjust 3D depth strength inside .scramble > * > span by changing --start-z: calc(var(--z) * var(--depth-factor) * 13px). Modify scattering amplitude using initial-value: 75 on @property --page-offset.
  • Sizing, Layout & Scales: Change typography responsiveness via --f-size: clamp(1rem, 2.5vw + .045rem, 1.5rem). Adjust scroll viewport height on body via height: 800svh to slow down or speed up scroll duration.
Advanced interactive segment control with CSS Anchor-based sliding pill highlights and SVG knockout mask filters.
SVG·HTML·CSS·JavaScript

Advanced Sliding Radio Pill Selector with CSS Anchor

This highly advanced segment controller showcases state-of-the-art interactive transitions. It demonstrates two distinct animations: a directional elastic hop and an SVG-masked CSS Anchor Positioning slide. When anchoring is active, a custom feColorMatrix filter punches out the text, leaving an inverted, color-shifting cutout inside the sliding pill without duplicating HTML nodes.

Level
Advanced
Requires
gsap.js · gsap-draggable.js · tweakpane.js
Browsers
Chrome · Edge · Firefox · Safari
Highlights
CSS Anchor Positioning · Sliding Pill Animations · SVG Knockout Masking · Tweakpane Control
Loading preview
<svg class="sr-only">
      <filter id="knockout-black" colorInterpolationFilters="sRGB">
        <feColorMatrix
          type="matrix"
          values="1 0 0 0 0
                  0 1 0 0 0
                  0 0 1 0 0
                  -1 -1 -1 0 1"
        />
        <feComposite in="SourceGraphic" operator="out" />
      </filter>
      <filter id="knockout-white" colorInterpolationFilters="sRGB">
        <feColorMatrix
          type="matrix"
          values="1 0 0 0 0
                  0 1 0 0 0
                  0 0 1 0 0
                  1 1 1 0 0"
        />
        <feComposite in="SourceGraphic" operator="out" />
      </filter>
    </svg>
    <div class="resize-container">
    </div>
    <svg class="svg-mask" xmlns="http://www.w3.org/2000/svg"></svg>
    <a
      aria-label="Follow Jhey"
      class="bear-link"
      href="https://twitter.com/intent/follow?screen_name=jh3yy"
      target="_blank"
      rel="noreferrer noopener"
    >
      <svg
        class="w-9"
        viewBox="0 0 969 955"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <circle
          cx="161.191"
          cy="320.191"
          r="133.191"
          stroke="currentColor"
          stroke-width="20"
        ></circle>
        <circle
          cx="806.809"
          cy="320.191"
          r="133.191"
          stroke="currentColor"
          stroke-width="20"
        ></circle>
        <circle
          cx="695.019"
          cy="587.733"
          r="31.4016"
          fill="currentColor"
        ></circle>
        <circle
          cx="272.981"
          cy="587.733"
          r="31.4016"
          fill="currentColor"
        ></circle>
        <path
          d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
          fill="currentColor"
        ></path>
        <rect
          x="310.42"
          y="448.31"
          width="343.468"
          height="51.4986"
          fill="#FF1E1E"
        ></rect>
        <path
          fill-rule="evenodd"
          clip-rule="evenodd"
          d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
          fill="currentColor"
        ></path>
      </svg>
    </a>
@import url('https://unpkg.com/normalize.css') layer(normalize);
@import url('https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&display=swap');

@layer normalize, base, prototype, anchor, hop, knockout;

@layer knockout {
  .svg-mask {
    outline-offset: -2px;
    position: absolute;
    inset: 0;
    height: 100%;
    width: 100%;
    pointer-events: none;
    z-index: 2;
    translate: 0 100%;
  }

  [data-knockout='mask'][data-style='anchor'] fieldset:first-of-type > div {
    -webkit-mask: var(--svg-mask, initial);
            mask: var(--svg-mask, initial);
  }

  [data-knockout='none'] .mask-layer,
  [data-knockout='mask'] .mask-layer {
    display: none;
  }
  [data-knockout='filter'][data-style='anchor'] {
    .mask-layer {
      filter: url(#knockout-black);
    }
    @media (prefers-color-scheme: dark) {
      .mask-layer {
        filter: url(#knockout-white);
      }
    }
  }
}

@layer hop {
  [data-match='true'][data-style='hop'] {
    label::before {
      inset: unset;
      top: 0;
      left: 0;
      bottom: 0;
      width: max(var(--pill-width-current, 100%), var(--pill-width-previous, 100%));
    }
    label:has(+ :not(:checked) ~ :checked)::before,
    label:has(+ [data-current-checked='true']):has(~ [data-previous-checked='true'])::before {
      right: 0;
      left: unset;
    }
    label:has(+ [data-previous-checked='true'])
      ~ label:has(+ [data-current-checked='true'])::before,
    label:has(+ [data-current-checked='true']) ~ label::before {
      left: 0;
      right: unset;
    }
  }
  [data-style='hop'] {
    label {
      position: relative;
      background: #0000;
      overflow: hidden;
    }
    label::before {
      content: '';
      position: absolute;
      inset: 0;
      border-radius: 100px;
      z-index: -1;
      width: 100%;
    }

    label:hover {
      background: light-dark(
        color-mix(in srgb, canvasText, #0000 95%),
        color-mix(in srgb, canvasText, #fff0 75%)
      );
    }

    /* the labels that precede a checked option */
    label:has(+ :not(:checked) ~ :checked)::before {
      transform: translateX(100%);
    }

    /* the last checked that comes after the current checked */
    label:has(+ [data-current-checked='true']) ~ label::before {
      transform: translateX(-100%);
    }

    
    label:has(+ [data-previous-checked='true'])::before,
    label:has(+ [data-current-checked='true'])::before {
      background: light-dark(#000, #fff);
      transition: transform calc(var(--duration, 0.2) * 1s) ease-out;
    }
  }
  [data-style='hop'][data-wrap='true'] {
    fieldset:first-of-type:has(:nth-of-type(3)) {
      label:has(+ [data-current-checked='true']):first-of-type ~ label:last-of-type::before {
        transform: translateX(100%);
      }
      label:not(:has(+ [data-current-checked='true'])):first-of-type:has(
          ~ input:last-of-type:checked
        )::before {
        transform: translateX(-100%);
      }
    }
  }
}

@layer anchor {
  @supports (not (anchor-name: --active)) {
    .fields {
      position: relative;
    }
    .pill {
      top: calc(var(--top, 0) * 1px);
      left: calc(var(--left, 0) * 1px);
      height: calc(var(--height, 0) * 1px);
      width: calc(var(--width, 0) * 1px);
    }
    .fields[data-initiated='true'] .pill {
      transition-property: left, top, width;
      transition-duration: calc(var(--duration, 0.2) * 1s);
      transition-timing-function: ease-out;
    }
  }
  
  @supports (anchor-name: --active) {
    .pill {
      top: anchor(top);
      left: anchor(left);
      width: anchor-size(width);
      height: anchor-size(height);
      position-anchor: --active;
      transition-property: left, top, width;
      transition-duration: calc(var(--duration, 0.2) * 1s);
      transition-timing-function: ease-out;
    }
    label:has(+ :checked) {
      anchor-name: --active;
    }
  }
  [data-style='anchor'] {
    .pill {
      display: inline-block;
      background: light-dark(#000, #fff);
      position: absolute;
      border-radius: 100px;
    }
    fieldset {
      &:first-of-type {
        position: relative;
      }

      &:last-of-type {
        position: absolute;
        top: 0;
        left: 0;
        pointer-events: none;
        z-index: 2;
        /* makes room for the legend */
        -webkit-clip-path: inset(1lh 0 0 0);
                clip-path: inset(1lh 0 0 0);
      }
    }
    label:has(+ :checked) {
      background: #0000;
    }
    .mask-layer {
      display: inline-flex;
      background: light-dark(#fff, #000);

      legend {
        opacity: 0;
      }

      label {
        color: light-dark(#000, #fff);
        background: light-dark(#000, #fff);
        border: 1px solid light-dark(#000, #fff);
      }
    }
  }
}

@layer prototype {
  .resize-container {
    resize: both;
    overflow: auto;
    position: relative;
    width: 260px;
    height: 195px;
    font-family: monospace;
    padding: 2rem;
  }


  .fields {
    position: relative;
  }
  fieldset {
    border: 0;
  }

  fieldset, fieldset > div {
    display: inline-flex;
    flex-wrap: wrap;
    gap: 0.5rem;
  }

  legend {
    font-size: 0.875rem;
    font-weight: 300;
    letter-spacing: 0.1em;
    margin-bottom: 0.5rem;
    font-variant: small-caps;
    translate: 2ch 0;
  }
  fieldset label {
    padding: 0.5rem 1rem;
    border-radius: 100px;
    background: #0000;
    border: 1px solid color-mix(in srgb, canvasText, canvas);
    cursor: pointer;
    white-space: nowrap;
    transition-property: color, background;
    transition-duration: calc(var(--duration, 0.2) * 1s), 0.2s;
    transition-timing-function: ease-out;
    -webkit-user-select: none;
       -moz-user-select: none;
        -ms-user-select: none;
            user-select: none;

    &:hover {
      background: light-dark(
        color-mix(in srgb, canvasText, #0000 95%),
        color-mix(in srgb, canvasText, #fff0 75%)
      );
    }
  }
  label:has(+ :checked) {
    background: light-dark(#000, #fff);
    color: light-dark(#fff, #000);
  }
  .pill {
    z-index: -1;
  }
  .mask-layer,
  .pill {
    display: none;
  }
}

@layer base {
  :root {
    --font-size-min: 16;
    --font-size-max: 20;
    --font-ratio-min: 1.2;
    --font-ratio-max: 1.33;
    --font-width-min: 375;
    --font-width-max: 1500;
  }

  html {
    color-scheme: light dark;
  }

  [data-theme='light'] {
    color-scheme: light only;
  }

  [data-theme='dark'] {
    color-scheme: dark only;
  }

  [data-raw='true'] div.tp-dfwv {
    display: none;
  }

  [data-raw='true'] {
    scrollbar-color: #0000 #0000;
  }

  :where(.fluid) {
    --fluid-min: calc(
      var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
    );
    --fluid-max: calc(
      var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
    );
    --fluid-preferred: calc(
      (var(--fluid-max) - var(--fluid-min)) /
        (var(--font-width-max) - var(--font-width-min))
    );
    --fluid-type: clamp(
      (var(--fluid-min) / 16) * 1rem,
      ((var(--fluid-min) / 16) * 1rem) -
        (((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
        (var(--fluid-preferred) * var(--variable-unit, 100vi)),
      (var(--fluid-max) / 16) * 1rem
    );
    font-size: var(--fluid-type);
  }

  *,
  *:after,
  *:before {
    box-sizing: border-box;
  }

  body {
    background: light-dark(#fff, #000);
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family:
      'SF Pro Text', 'SF Pro Icons', 'AOS Icons', 'Helvetica Neue', Helvetica,
      Arial, sans-serif, system-ui;
  }

  body::before {
    --size: 45px;
    --line: color-mix(in hsl, canvasText, transparent 80%);
    content: '';
    height: 100vh;
    width: 100vw;
    position: fixed;
    background:
      linear-gradient(90deg, var(--line) 1px, transparent 1px var(--size))
        calc(var(--size) * 0.36) 50% / var(--size) var(--size),
      linear-gradient(var(--line) 1px, transparent 1px var(--size)) 0%
        calc(var(--size) * 0.32) / var(--size) var(--size);
    -webkit-mask: linear-gradient(140deg, #0000 65%, #fff);
            mask: linear-gradient(140deg, #0000 65%, #fff);
    top: 0;
    transform-style: flat;
    pointer-events: none;
    z-index: -1;
  }

  .bear-link {
    color: canvasText;
    position: fixed;
    top: 1rem;
    left: 1rem;
    width: 48px;
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    opacity: 0.8;
  }

  :where(.x-link, .bear-link):is(:hover, :focus-visible) {
    opacity: 1;
  }

  .bear-link svg {
    width: 75%;
  }

  /* Utilities */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
}

div.tp-dfwv {
  width: 256px;
  view-transition-name: prototype-config;
}
import gsap from 'https://esm.sh/[email protected]';
import Draggable from 'https://esm.sh/[email protected]/Draggable';
import { Pane } from 'https://esm.sh/[email protected]';
gsap.registerPlugin(Draggable);

const resizeContainer = document.querySelector('.resize-container');
let fieldset;
let fields;
let radios;
let labels;
let initialChecked;
let initialIndex = 0;
let maskContainer;
let checkedIndices;

const svgMask = document.querySelector('.svg-mask');

const config = {
  theme: 'system',
  style: 'hop',
  match: true,
  wrap: true,
  knockout: 'filter',
  duration: 0.2,
  variants: 5,
  options: ['128GB', '256GB', '512GB', '1TB', '2TB', '4TB', '8TB']
};

const ctrl = new Pane({
  title: 'config'
});

const buildFieldsets = () => {
  resizeContainer.innerHTML = `
  <div class="fields">
  <div class="pill"></div>
    <fieldset>
      <legend>capacity</legend>
      <div class="content">
        ${new Array(config.variants).fill().map((_, i) => `
          <label for="${config.options[i]}">
            <span class="variant-option__button-label__text">${config.options[i]}</span>
          </label>
          <input class="sr-only" ${i === initialIndex ? 'checked data-current-checked="true"' : ''} data-input-index="${i}" aria-label="${config.options[i]}" type="radio" name="capacity" value="${config.options[i]}" id="${config.options[i]}">
        `).join('')}
      </div>
    </fieldset>
    <fieldset aria-hidden="true" class="mask-layer">
      <legend>capacity</legend>
      ${new Array(config.variants).fill().map((_, i) => `
        <label>
          <span class="variant-option__button-label__text">${config.options[i]}</span>
        </label>
      `).join('')}
    </fieldset>
  </div>
  `;
  fields = resizeContainer.querySelector('.fields');
  fieldset = resizeContainer.querySelector('fieldset');
  radios = [...fieldset.querySelectorAll('input')];
  labels = [...fieldset.querySelectorAll('label')];
  maskContainer = resizeContainer.querySelector('fieldset:first-of-type > div');

  initialChecked = fieldset.querySelector('input:checked');
  initialIndex = radios.indexOf(initialChecked);
  checkedIndices = [initialIndex];
  fieldset.style.setProperty('--pill-width', `${labels[initialIndex].offsetWidth}px`);

  initialChecked.dataset.currentChecked = true;
};
buildFieldsets();

ctrl.addBinding(config, 'variants', {
  min: 2,
  max: config.options.length,
  step: 1
}).on('change', buildFieldsets);

ctrl.addBinding(config, 'duration', {
  min: 0,
  max: 2,
  step: 0.01
});

ctrl.addBinding(config, 'style', {
  options: {
    anchor: 'anchor',
    hop: 'hop'
  }
});

const wrap = ctrl.addBinding(config, 'wrap', {
  hidden: config.style !== 'hop'
});

const match = ctrl.addBinding(config, 'match', {
  hidden: config.style !== 'hop'
});

const knockout = ctrl.addBinding(config, 'knockout', {
  options: {
    filter: 'filter',
    mask: 'mask',
    none: 'none'
  },
  hidden: config.style === 'hop'
});

ctrl.addBinding(config, 'theme', {
  label: 'theme',
  options: {
    system: 'system',
    light: 'light',
    dark: 'dark'
  }
});

const update = () => {
  document.documentElement.dataset.theme = config.theme;
  document.documentElement.dataset.style = config.style;
  document.documentElement.dataset.match = config.match;
  document.documentElement.dataset.knockout = config.knockout;
  document.documentElement.dataset.wrap = config.wrap;
  document.documentElement.style.setProperty('--duration', config.duration);
  knockout.hidden = config.style === 'hop';
  match.hidden = wrap.hidden = config.style !== 'hop';
  ctrl.refresh();
};

const sync = event => {
  if (!document.startViewTransition || event.target.controller.view.labelElement.innerText !== 'theme') {
    return update();
  }
  document.startViewTransition(() => update());
};

ctrl.on('change', sync);

const isRaw = new URLSearchParams(window.location.search).get('raw') === 'true';
if (isRaw) document.documentElement.dataset.raw = 'true';

const tweakClass = 'div.tp-dfwv';
const d = Draggable.create(tweakClass, {
  type: 'x,y',
  allowEventDefault: true,
  trigger: tweakClass + ' button.tp-rotv_b'
});

document.querySelector(tweakClass).addEventListener('dblclick', () => {
  gsap.to(tweakClass, {
    x: `+=${d[0].x * -1}`,
    y: `+=${d[0].y * -1}`,
    onComplete: () => {
      gsap.set(tweakClass, { clearProps: 'all' });
    }
  });
});
update();

const updateMask = () => {
  const { left, top } = maskContainer.getBoundingClientRect();
  svgMask.setAttribute('viewBox', `0 0 ${maskContainer.offsetWidth} ${maskContainer.offsetHeight}`);

  const maskString = new Array(radios.length).fill().map((_, i) => {
    const { x, y, width, height } = labels[i].getBoundingClientRect();
    return `<rect x="${x - left}" y="${y - top}" width="${width}" height="${height}" rx="${height * 0.5}" ry="${height * 0.5}" fill="#000"></rect>`;
  }).join('');
  
  svgMask.innerHTML = maskString;
  const encoded = encodeURIComponent(svgMask.outerHTML).replace(/'/g, '%27').replace(/"/g, '%22');
  const dataUri = `data:image/svg+xml;utf8,${encoded}`;
  document.documentElement.style.setProperty('--svg-mask', `url(${dataUri})`);
};

const resizeHandler = new ResizeObserver(() => {
  if (config.style === 'anchor') {
    updateMask();
    if (!CSS.supports('anchor-name: --active')) {
      cacheBase();
      syncChecked();
    }
  }
});
resizeHandler.observe(resizeContainer);

const positions = [];
const syncChecked = () => {
  const checked = fieldset.querySelector(':checked');
  const index = radios.indexOf(checked);
  fields.style.setProperty('--top', positions[index].top);
  fields.style.setProperty('--left', positions[index].left);
  fields.style.setProperty('--width', positions[index].width);
  fields.style.setProperty('--height', positions[index].height);
  fields.style.setProperty('--marker-top', positions[index].top);
  fields.style.setProperty('--marker-left', positions[index].left);
};

const cacheBase = () => {
  const { x: fieldX, y: fieldY } = fields.getBoundingClientRect();
  const { x, y } = fieldset.querySelector('label').getBoundingClientRect();
  fields.style.setProperty('--left', x - fieldX);
  fields.style.setProperty('--top', y - fieldY);

  document.documentElement.style.setProperty('--marker-top', y - fieldY);
  document.documentElement.style.setProperty('--marker-left', x - fieldX);
  positions.length = 0;
  for (const label of labels) {
    const { height, width, left, top } = label.getBoundingClientRect();
    positions.push({
      width,
      height,
      left: left - fieldX,
      top: top - fieldY
    });
  }
};

resizeContainer.addEventListener('input', event => {
  if (config.style === 'hop') {
    const inputIndex = Number.parseInt(event.target.dataset.inputIndex || '');
    const [currentIndex, previousIndex] = checkedIndices;
    if (currentIndex !== undefined && radios[currentIndex]) {
      radios[currentIndex].dataset.previousChecked = 'false';
    }
    if (previousIndex !== undefined && radios[previousIndex]) {
      radios[previousIndex].dataset.previousChecked = 'false';
    }

    checkedIndices.unshift(inputIndex);
    checkedIndices.length = Math.min(checkedIndices.length, 2);

    const newCurrentIndex = checkedIndices[0];
    const newPreviousIndex = checkedIndices[1];

    if (newCurrentIndex !== undefined && radios[newCurrentIndex]) {
      var _radios$newCurrentInd;
      radios[newCurrentIndex].dataset.currentChecked = 'true';
      fieldset.style.setProperty(
        '--pill-width-current',
        `${((_radios$newCurrentInd = radios[newCurrentIndex].previousElementSibling) === null || _radios$newCurrentInd === void 0 ? void 0 : _radios$newCurrentInd.offsetWidth) || 0}px`
      );
    }

    if (newPreviousIndex !== undefined && radios[newPreviousIndex]) {
      var _radios$newPreviousIn;
      radios[newPreviousIndex].dataset.previousChecked = 'true';
      radios[newPreviousIndex].dataset.currentChecked = 'false';
      fieldset.style.setProperty(
        '--pill-width-previous',
        `${((_radios$newPreviousIn = radios[newPreviousIndex].previousElementSibling) === null || _radios$newPreviousIn === void 0 ? void 0 : _radios$newPreviousIn.offsetWidth) || 0}px`
      );
    }
  } else if (config.style === 'anchor') {
    if (!CSS.supports('anchor-name: --active')) {
      syncChecked();
    }
  }
});

if (!CSS.supports('anchor-name: --active')) {
  requestAnimationFrame(() => {
    cacheBase();
    syncChecked();
    fields.dataset.initiated = true;
  });
}
CSS-only hyper-realistic glowing volumetric bell with rotating rays and a realistic notification button.
HTML·CSS

CSS Hyper-Realistic Animated Glowing Bell

This showcase demonstrates the extreme rendering potential of pure CSS. By layer-masking realistic gradients, intricate custom clip-path shapes, and dynamic box-shadow depth, it renders a hyper-realistic glowing bell. Toggling the active state switches the glowing volumetric shafts, rotating light rays, and ambient button backlights using only sibling selectors.

Level
Advanced
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Volumetric Shading · Interactive Toggle · Ray Traced Conics · Grain Overlay
Loading preview
<div class="bell-container off" onclick="this.classList.toggle('off')">
	<div class="rope"></div>
	<div class="bell-top"></div>

	<div class="bell-base"></div>
	<div class="bell-base"></div>
	<div class="shadow-l1"></div>
	<div class="shadow-l2"></div>
	<div class="left-glow"></div>
	<div class="left-glow2"></div>
	<div class="r-glow"></div>
	<div class="r-glow2"></div>
	<div class="mid-ring"></div>
	<div class="mid-ring small"></div>

	<div class="glow"></div>
	<div class="glow2"></div>

	<div class="bell-buff-t"></div>
	<div class="bell-buff"></div>

	<div class="bell-btm"></div>
	<div class="bell-btm2"></div>

	<div class="bell-ring-container">
		<div class="bell-ring"></div>
		<div class="bell-rays"></div>
	</div>

	<div class="volumetric">
		<div class="vl"></div>
		<div class="vr"></div>
	</div>

</div>
<div class="button">Notify Me</div>
<div class="grain"></div>
* {
	box-sizing: border-box;
}
html,
body {
	height: 100%;
	overflow: hidden;
}
body {
	display: flex;
	flex-direction: row;
	flex-wrap: wrap;
	justify-content: center;
	align-items: center;
	margin: 0;
	background: #000;

	font-size: calc(var(--_size) * 0.01);
	--_size: min(min(600px, 50vh), 50vw);
	--base-clr: #b7b5b4;
	--degofrot: 0.8;
}

.bell-container {
	width: 80em;
	height: 80em;
	opacity: 1;
	cursor: pointer;

	transform-origin: 50% -50vh;
	animation: 5s ease-in-out infinite bell;
}
@keyframes bell {
	0% {
		rotate: calc(1deg * var(--degofrot));
	}
	50% {
		rotate: calc(-1deg * var(--degofrot));
	}
	100% {
		rotate: calc(1deg * var(--degofrot));
	}
}

* {
	transition: filter 0.4s ease-in-out, box-shadow 0.4s ease-in-out,
		opacity 0.4s ease-in-out, color 0.4s ease-in-out, background 0.4s ease-in-out,
		text-shadow 0.4s ease-in-out;
}

*::before,
*::after {
	transition: filter 0.4s ease-in-out, box-shadow 0.4s ease-in-out,
		opacity 0.4s ease-in-out, color 0.4s ease-in-out, background 0.4s ease-in-out,
		text-shadow 0.4s ease-in-out;
}

.bell-container,
.bell-container * {
	position: absolute;
	left: 0;
	right: 0;
	top: 0;
	bottom: 0;
	margin: auto;
}

.rope {
	height: 50vh;
	width: 2em;
	translate: 0 -52%;
	background: linear-gradient(90deg, #2d54744d 0%, #000b 30%, transparent 100%),
		repeating-linear-gradient(-70deg, #252525, #888 2%, #3a3a3a 3%);
}

.bell-top {
	width: 14%;
	height: 14%;
	border-radius: 50%;
	translate: 0 -28em;
	background: var(--base-clr);
	box-shadow: inset -1em -0.5em 2em 0.5em #fff, inset 1em -1em 2em 3em #000,
		0 -0.1em 0.4em 0.3em #c6eaffa8;
}

.bell-base {
	width: 50%;
	height: 50%;
	border-radius: 50%;
	translate: 0 -24%;
	background: var(--base-clr);
	box-shadow: 0 -0.1em 0.4em 0.2em #c6eaffa8;
}
.bell-base:before {
	content: "";
	background-image: radial-gradient(
		circle at -80% -12%,
		transparent 50em,
		var(--base-clr) 50em
	);
	position: absolute;
	translate: -18em 20em;
	width: 100%;
	height: 80%;
}
.bell-base:after {
	content: "";
	background-image: radial-gradient(
		circle at -80% -12%,
		transparent 50.1em,
		#cacaca 50.3em,
		var(--base-clr) 50.5em
	);
	position: absolute;
	translate: 18em 20em;
	width: 100%;
	height: 80%;
	transform: rotateY(180deg);
}
.bell-base:nth-child(2) {
	filter: brightness(3) blur(1em);
	scale: 0.74 0.84;
	translate: 0em -11em;
}
.shadow-l1 {
	width: 30em;
	height: 42em;
	border-radius: 50%;
	rotate: 12deg;
	translate: -3em -6em;
	filter: blur(2em);
	background: #797a80;
}
.shadow-l2 {
	width: 130%;
	height: 90%;
	filter: blur(5em);
	translate: -6em 9em;
}
.shadow-l2::before {
	display: block;
	content: "";
	width: 68%;
	height: 64%;
	border-radius: 50%;
	rotate: -54deg;
	translate: -8em 2em;
	scale: 1;
	background: red;
	background: #000000;
}
.glow {
	width: 100%;
	height: 100%;
	filter: brightness(2) blur(2em);
}
.glow::before {
	clip-path: polygon(
		9% 83%,
		12% 79%,
		15% 74%,
		18% 68%,
		20% 62%,
		22% 56%,
		23% 50%,
		24% 43%,
		25% 38%,
		25% 34%,
		26% 29%,
		26% 26%,
		27% 22%,
		29% 19%,
		30% 15%,
		32% 13%,
		34% 10%,
		37% 7%,
		41% 5%,
		44% 4%,
		47% 3%,
		51% 3%,
		55% 3%,
		58% 5%,
		62% 6%,
		73% 29%,
		72% 25%,
		70% 20%,
		67% 16%,
		63% 12%,
		60% 9%,
		58% 8%,
		55% 7%,
		52% 6%,
		48% 6%,
		44% 8%,
		41% 9%,
		37% 12%,
		36% 14%,
		33% 16%,
		31% 20%,
		30% 23%,
		29% 26%,
		28% 31%,
		27% 36%,
		27% 39%,
		26% 44%,
		26% 48%,
		26% 52%,
		25% 56%,
		23% 61%,
		22% 65%,
		21% 69%,
		19% 72%,
		17% 75%,
		15% 78%,
		13% 81%
	);
	width: 100%;
	height: 80%;
	translate: 0 6em;
	scale: 0.94 0.94;
	background: #fff3;
	display: block;
	content: "";
}
.glow2 {
	width: 100%;
	height: 100%;
	filter: brightness(1) blur(0.3em);
	opacity: 0.1;
}
.glow2::before {
	clip-path: polygon(
		9.21% 83%,
		12.28% 79%,
		15.35% 74%,
		18.41% 68%,
		20.46% 62%,
		22.51% 56%,
		23.53% 50%,
		24.55% 43%,
		25.58% 34%,
		26.6% 29%,
		27.62% 22%,
		29.16% 18.5%,
		30.95% 15.75%,
		32.74% 13%,
		34.78% 10%,
		37.85% 7%,
		41.94% 5%,
		45.01% 4%,
		48.08% 3%,
		52.17% 3%,
		56.27% 3%,
		64.01% 6.36%,
		55.75% 4.5%,
		47.83% 4.75%,
		42.84% 5.88%,
		39.51% 7.88%,
		36.45% 10.38%,
		33.38% 14.88%,
		30.69% 19%,
		29.67% 22.5%,
		28.8% 26.72%,
		28.21% 31.36%,
		26.92% 38.44%,
		26.57% 43.67%,
		25.51% 48.34%,
		25% 54.34%,
		23.4% 60.69%,
		21.23% 65.38%,
		18.41% 71.5%,
		16.88% 74.75%,
		12.28% 80.5%
	);
	width: 100%;
	height: 84%;
	translate: -1em 8.4em;
	scale: 1;
	background: #fff3;
	display: block;
	content: "";
}
.left-glow {
	--lgc: #5d819666;
	width: 50%;
	height: 50%;
	border-radius: 50%;
	translate: 0 -24%;
	background: transparent;
	box-shadow: inset 1em 0em 1em 0.2em var(--lgc);
	clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
}
.left-glow2 {
	--lgc2: #5d819666;
	width: 49%;
	height: 50%;
	background-image: radial-gradient(
		circle at -80% -12%,
		transparent 50em,
		var(--lgc2) 50.3em,
		transparent 52em
	);
	position: absolute;
	translate: -19em 10.35em;
	clip-path: polygon(0 0, 100% 0, 100% 78%, 0 78%);
}
.r-glow {
	--lgc: #fffaf680;
	width: 50%;
	height: 50%;
	border-radius: 50%;
	translate: 0 -24%;
	background: transparent;
	box-shadow: inset 1em 0em 1em 0.2em var(--lgc);
	clip-path: polygon(0 0, 100% 0, 100% 50%, 0 50%);
	transform: rotateY(180deg);
}
.r-glow2 {
	--lgc2: #fffaf680;
	width: 49%;
	height: 50%;
	background-image: radial-gradient(
		circle at -80% -12%,
		transparent 50em,
		var(--lgc2) 50.3em,
		transparent 52em
	);
	position: absolute;
	translate: 18.2em 10.35em;
	clip-path: polygon(0 0, 100% 0, 100% 78%, 0 78%);
	transform: rotateY(180deg) rotateZ(-2deg);
}
.mid-ring.small {
	translate: 0.04em -8em;
	scale: 0.8 0.5;
}
.mid-ring {
	width: 64%;
	height: 10%;
	border-radius: 50%;
	translate: -0.1em 10em;
	box-shadow: inset -0.3em 1.3em 0.4em -1em #fff5,
		-0.2em -1.2em 0.4em -0.4em #505050, -0.1em -1.8em 0.4em -0.4em #fff5,
		0 -2.5em 0.4em -1em #000000;
	mix-blend-mode: hard-light;
	filter: brightness(0.8);
}
.mid-ring::before,
.mid-ring::after {
	content: "";
	display: block;
	background: #000;
	width: 2em;
	height: 2em;
	top: 10%;
	border-radius: 50%;
	position: absolute;
}
.mid-ring::after {
	right: -2%;
}
.mid-ring::before {
	left: -2%;
}
.bell-buff-t {
	background: #fff2;
	width: 72%;
	height: 12%;
	border-radius: 50%;
	translate: 0 16em;
	filter: blur(1em);
}
.bell-buff {
	background: linear-gradient(90deg, black 40%, var(--base-clr) 90%);
	width: 88%;
	height: 20%;
	border-radius: 50% 50% 50% 50% / 50% 50% 30% 30%;
	translate: 0 20em;
	box-shadow: inset 1em 0 2em -1em #5d819666, inset -1em 0 2em -1em #fff;
}
.bell-btm {
	width: 88%;
	height: 18%;
	border-radius: 50%;
	translate: 0 23em;
	background: linear-gradient(90deg, black 40%, var(--base-clr) 90%);
}
.bell-btm2 {
	width: 74%;
	height: 12%;
	border-radius: 50%;
	translate: 0 24em;
	background: #fffff6;
	box-shadow: 0 0 1em 0.6em #ffe9d4, -0.8em 0.2em 2em 1em #cca37f,
		-5.4em -0.6em 3em -1em #ce6e1abb, 6em -0.6em 3em -1em #ce6e1abb,
		inset 0em 30.3em 0.3em -30em #c7962d, inset 0 -2em 2em -2em #ffe9d4,
		inset 0em -1em 2em 1em #ce6e1a66;
	filter: brightness(1);
}
.off .bell-btm2 {
	filter: brightness(0.02);
}
.bell-ring-container {
	width: 74%;
	height: 24%;
	border-radius: 50% 50% 50% 50% / 25% 25% 0% 0%;
	translate: 0 29.2em;
	overflow: hidden;
}
.bell-ring {
	width: 12em;
	height: 12em;
	background: #fff;
	border-radius: 50%;
	translate: 0 -6em;
	box-shadow: 0 0.8em 1em -0.3em #f8e1d0, inset 0 -6em 4em -4em #e3b695,
		inset 0 1em 3em 1em #fff4, inset 0 2em 3em 1em #fff,
		inset 0 100em 0 100em #2c2c2c;
}
.off .bell-ring {
	background: #000;
	box-shadow: 0 0.8em 1em -0.3em #f8e1d000, inset 0 -6em 4em -4em #e3b69500,
		inset 0 1em 3em 1em #fff0, inset 0 -2em 3em 1em #fff2,
		inset 0 100em 0 100em #000;
}

.bell-rays {
	mix-blend-mode: soft-light;
	box-shadow: inset 0 -21em 4em -20em #000;
	width: 100%;
	height: 140%;
	translate: 0 -4em;
	border-radius: 50%;
}
.bell-rays::before {
	content: "";
	display: block;
	width: 100em;
	height: 100em;
	transform-origin: 50% 50%;
	position: absolute;
	left: -21em;
	top: -77em;
	border-radius: 100%;
	filter: blur(0.6em);
	background: repeating-conic-gradient(
		at 50% 50%,
		#fff2 0%,
		transparent 0.6%,
		#fff2 0.8%
	);
	animation: radiate 1s linear infinite;
}
.off .bell-rays {
	opacity: 0;
}
@keyframes radiate {
	0% {
		rotate: 0deg;
	}
	100% {
		rotate: 6deg;
	}
}

.volumetric {
	width: 98%;
	height: 224%;
	translate: 0 124em;
	opacity: 0.2;
}
.volumetric .vl {
	width: 100%;
	height: 100%;
	transform-origin: 50% 20em;
	rotate: 22deg;
	box-shadow: inset 40em 0 20em -20em #fff1;
}
.volumetric .vr {
	width: 100%;
	height: 100%;
	transform-origin: 50% 20em;
	rotate: -22deg;
	box-shadow: inset -40em 0 20em -20em #fff1;
}
.off .volumetric {
	opacity: 0;
}

.grain {
	z-index: 10;
	position: absolute;
	pointer-events: none;
	width: 100%;
	height: 100%;
	top: 0;
	bottom: 0;
	margin: auto;
	background: radial-gradient(circle at 50% 50%, #000, #0000),
		url("data:image/svg+xml,%3Csvg viewBox='0 0 600 600' xmlns='http://www.w3.org/2000/svg'%3E%3Cfilter id='noiseFilter'%3E%3CfeTurbulence type='fractalNoise' baseFrequency='2' numOctaves='3' stitchTiles='stitch'/%3E%3C/filter%3E%3Crect width='100%25' height='100%25' filter='url(%23noiseFilter)'/%3E%3C/svg%3E");
	filter: contrast(100%) brightness(200%) grayscale(1) opacity(0.168);
	mix-blend-mode: screen;
}

.button {
	position: relative;
	font-size: 6em;
	font-family: monospace;
	background: #000;
	top: 8em;
	width: fit-content;
	height: fit-content;
	color: #000;
	cursor: pointer;
	padding: 0.4em 1.2em;
	border-radius: 0.4em;
	text-shadow: 0 -1px 3px #fff0;
	box-shadow: inset 0 0.04em 0.06em 0 #fff, inset 0 1em 1em 0 #fff5,
		inset 0 0.2em 0.2em 0 #e3b695;
	animation: 5s ease-in-out infinite lumenbtn;
}
.button:hover {
	color: #fff;
	text-shadow: 0 -1px 3px #fff;
	transition: all 0.16s ease-in-out;
}
.button::before,
.button::after {
	content: "";
	display: block;
	width: 100%;
	height: 54%;
	position: absolute;
	top: 6em;
	left: 0;
	right: 0;
}
.button::before {
	background: #e3b695;
	scale: 2;
	z-index: -2;
	filter: blur(12px);
	border-radius: 100%;
	animation: 5s ease-in-out infinite lumen;
}
.button::after {
	background: #000c;
	z-index: -1;
	filter: blur(0.3em);
	border-radius: 30%;
}

@keyframes lumenbtn {
	0% {
		box-shadow: inset 0 0.04em 0.06em 0 #fff,
			inset calc(-0.2em * var(--degofrot)) 1em 1em 0 #fff5,
			inset calc(-0.2em * var(--degofrot)) 0.2em 0.4em 0 #e3b695;
	}
	50% {
		box-shadow: inset 0 0.04em 0.06em 0 #fff,
			inset calc(0.2em * var(--degofrot)) 1em 1em 0 #fff5,
			inset calc(0.2em * var(--degofrot)) 0.2em 0.4em 0 #e3b695;
	}
	100% {
		box-shadow: inset 0 0.04em 0.06em 0 #fff,
			inset calc(-0.2em * var(--degofrot)) 1em 1em 0 #fff5,
			inset calc(-0.2em * var(--degofrot)) 0.2em 0.4em 0 #e3b695;
	}
}
@keyframes lumen {
	0% {
		translate: calc(-0.8em * var(--degofrot));
	}
	50% {
		translate: calc(0.8em * var(--degofrot));
	}
	100% {
		translate: calc(-0.8em * var(--degofrot));
	}
}

.off + .button {
	opacity: 0;
	pointer-events: none;
}
const bell = document.querySelector('.bell-container');
const button = document.querySelector('.button');

if (bell && button) {
  button.addEventListener('click', () => {
    bell.classList.toggle('off');
  });
}
Custom curved scrollbars generated with dynamic SVG path tracing to match container border radius using JavaScript.
SVG·HTML·CSS·JavaScript

Custom Curved SVG Border Radius Scrollbar

This highly innovative scrollbar solution bypasses the strict styling limitations of standard native scrollbars by rendering them as dynamic SVG overlays. By calculating container border-radius, sizing ratios, and scroll progression heights, it dynamically draws an SVG path to follow the container’s exact curvature. Grab states are handled seamlessly via the Pointer Events API and the SVG geometry method getPointAtLength.

Level
Advanced
Browsers
Chrome · Edge · Firefox · Safari
Highlights
SVG Path Tracing · Scrollbar Customization · Pointer Grab · Fluid Calculations
Loading preview
<header>
  <h1>Custom curved scrollbars</h1>
  <p>The scrollbar follows the border-radius of the container, it's length being calculated by the amount of content</p>
</header>


<div class="wrapper">
  <section class="scroll-container " data-scrollbar>
    <div class="scroll-content">
      
      <h2>Grab the scrollbar thumb and scroll up or down.</h2>
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi placeat rem at esse reiciendis ullam veritatis sapiente sit ducimus ea magnam nihil vero corrupti quo quae, tempore dolor, reprehenderit debitis?
      </p>
      <img src='https://images.unsplash.com/photo-1759392658577-4324fb89b991?crop=entropy&cs=tinysrgb&fit=max&fm=jpg&ixid=M3wzMjM4NDZ8MHwxfHJhbmRvbXx8fHx8fHx8fDE3NjEzMzUxNTl8&ixlib=rb-4.1.0&q=80&w=400' alt='random' width="300" height="150">
      <p>Veritatis et repellendus, consequatur dicta saepe vel natus aliquam reprehenderit accusamus voluptatem commodi, minus eos nihil quae. Odit eveniet quia a minus natus qui reiciendis unde! Quam, odit temporibus? Illo.
      </p>
      <p>
        Sunt consectetur asperiores fuga quasi nobis vitae et commodi error deserunt perspiciatis, modi eaque iste eligendi quod esse aliquam. Vitae debitis minima, laborum incidunt ex corporis odit eaque consequatur beatae.
      </p>
      <p>Suscipit possimus mollitia ut minus commodi facilis distinctio assumenda corrupti voluptate, ipsa voluptates minima ex nam exercitationem? Laborum, sequi ducimus! Sunt repellendus dolorum suscipit similique accusamus eius ad laborum sed.</p>
      <p>
      Laboriosam beatae consequatur omnis repellat consequuntur obcaecati! Voluptatibus sunt quibusdam optio nihil delectus cumque unde omnis voluptatum laborum temporibus, labore perspiciatis quis impedit sapiente animi adipisci, explicabo velit nesciunt similique?</p>
      
    
      <p>In id itaque ullam totam, dolor blanditiis praesentium, at laborum esse molestias fugit dolorum sapiente dolores quo animi ducimus provident deserunt tempora, voluptatem recusandae aliquid. Tenetur harum doloremque provident excepturi!</p>
      <p>
      Et suscipit dolores, non ullam consequuntur illo cumque perspiciatis quos voluptate impedit quas? Similique omnis magnam iste quasi aspernatur neque. Quis porro consequuntur facere odio sapiente praesentium sed harum dignissimos.
      </p>
      <p>Quisquam porro esse fuga ducimus inventore, repellendus ad omnis perferendis velit atque commodi natus fugit, aliquid incidunt, eveniet odit. Sunt blanditiis expedita, laudantium fugit dignissimos eius provident aperiam cum deleniti!</p>
    </div>
  </section>

  <section class="scroll-container box-2 " data-scrollbar>
    <div class="scroll-content">
      <h2>Less border radius on this one</h2>
      <p>
      Ut quasi magni rerum magnam, earum officiis asperiores placeat maiores ullam sint debitis culpa! Nemo est, modi dolor eos, blanditiis, neque ullam facere voluptates accusamus quasi nostrum similique. Quod, itaque.
      </p>
      <p>Est rem voluptates nihil culpa fugiat possimus provident a adipisci facilis, molestias quos sed pariatur cum aperiam vitae. Repellat natus iure omnis, sed nisi laboriosam ullam pariatur aliquam dolore! Hic.
      </p>
      <p>In id itaque ullam totam, dolor blanditiis praesentium, at laborum esse molestias fugit dolorum sapiente dolores quo animi ducimus provident deserunt tempora, voluptatem recusandae aliquid. Tenetur harum doloremque provident excepturi!</p>
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi placeat rem at esse reiciendis ullam veritatis sapiente sit ducimus ea magnam nihil vero corrupti quo quae, tempore dolor, reprehenderit debitis?
      </p>
      <p>Veritatis et repellendus, consequatur dicta saepe vel natus aliquam reprehenderit accusamus voluptatem commodi, minus eos nihil quae. Odit eveniet quia a minus natus qui reiciendis unde! Quam, odit temporibus? Illo.
      </p>
      <p>
        Sunt consectetur asperiores fuga quasi nobis vitae et commodi error deserunt perspiciatis, modi eaque iste eligendi quod esse aliquam. Vitae debitis minima, laborum incidunt ex corporis odit eaque consequatur beatae.
      </p>
      <p>Suscipit possimus mollitia ut minus commodi facilis distinctio assumenda corrupti voluptate, ipsa voluptates minima ex nam exercitationem? Laborum, sequi ducimus! Sunt repellendus dolorum suscipit similique accusamus eius ad laborum sed.</p>
      <p>
      Laboriosam beatae consequatur omnis repellat consequuntur obcaecati! Voluptatibus sunt quibusdam optio nihil delectus cumque unde omnis voluptatum laborum temporibus, labore perspiciatis quis impedit sapiente animi adipisci, explicabo velit nesciunt similique?</p>
      <p>
      Ut quasi magni rerum magnam, earum officiis asperiores placeat maiores ullam sint debitis culpa! Nemo est, modi dolor eos, blanditiis, neque ullam facere voluptates accusamus quasi nostrum similique. Quod, itaque.
      </p>
      <p>Est rem voluptates nihil culpa fugiat possimus provident a adipisci facilis, molestias quos sed pariatur cum aperiam vitae. Repellat natus iure omnis, sed nisi laboriosam ullam pariatur aliquam dolore! Hic.
      </p>
      <p>In id itaque ullam totam, dolor blanditiis praesentium, at laborum esse molestias fugit dolorum sapiente dolores quo animi ducimus provident deserunt tempora, voluptatem recusandae aliquid. Tenetur harum doloremque provident excepturi!</p>
      <p>
      Et suscipit dolores, non ullam consequuntur illo cumque perspiciatis quos voluptate impedit quas? Similique omnis magnam iste quasi aspernatur neque. Quis porro consequuntur facere odio sapiente praesentium sed harum dignissimos.
      </p>
      <p>Quisquam porro esse fuga ducimus inventore, repellendus ad omnis perferendis velit atque commodi natus fugit, aliquid incidunt, eveniet odit. Sunt blanditiis expedita, laudantium fugit dignissimos eius provident aperiam cum deleniti!</p>
      <p>Lorem ipsum dolor sit amet consectetur adipisicing elit. Modi placeat rem at esse reiciendis ullam veritatis sapiente sit ducimus ea magnam nihil vero corrupti quo quae, tempore dolor, reprehenderit debitis?
      </p>
      <p>Veritatis et repellendus, consequatur dicta saepe vel natus aliquam reprehenderit accusamus voluptatem commodi, minus eos nihil quae. Odit eveniet quia a minus natus qui reiciendis unde! Quam, odit temporibus? Illo.
      </p>
      <p>
        Sunt consectetur asperiores fuga quasi nobis vitae et commodi error deserunt perspiciatis, modi eaque iste eligendi quod esse aliquam. Vitae debitis minima, laborum incidunt ex corporis odit eaque consequatur beatae.
      </p>
      <p>Suscipit possimus mollitia ut minus commodi facilis distinctio assumenda corrupti voluptate, ipsa voluptates minima ex nam exercitationem? Laborum, sequi ducimus! Sunt repellendus dolorum suscipit similique accusamus eius ad laborum sed.</p>
      <p>
      Laboriosam beatae consequatur omnis repellat consequuntur obcaecati! Voluptatibus sunt quibusdam optio nihil delectus cumque unde omnis voluptatum laborum temporibus, labore perspiciatis quis impedit sapiente animi adipisci, explicabo velit nesciunt similique?</p>
      
      Et suscipit dolores, non ullam consequuntur illo cumque perspiciatis quos voluptate impedit quas? Similique omnis magnam iste quasi aspernatur neque. Quis porro consequuntur facere odio sapiente praesentium sed harum dignissimos.
      </p>
      <p>Quisquam porro esse fuga ducimus inventore, repellendus ad omnis perferendis velit atque commodi natus fugit, aliquid incidunt, eveniet odit. Sunt blanditiis expedita, laudantium fugit dignissimos eius provident aperiam cum deleniti!</p>
    </div>
  </section>
</div>
@import url(https://fonts.bunny.net/css?family=krub:200i,300);
@layers demo,base;


@layer demo{
  
  .wrapper{
    display: grid;
    grid-template-columns: repeat(auto-fit,minmax(250px, 1fr));
    gap: 1rem;
    height: 500px;
    width: min(100%, 600px);
    margin: 0 auto;
  }
  .scroll-container {
		--card-border-radius: 50px;
		
    --thumb-color: rgb(0, 188, 255);
    --thumb-color-active: dodgerblue;
    --thumb-width: 4;
    --thumb-width-active: 6;
    
    --track-color: transparent;
    --track-color-active: light-dark(#EEE,#222);
    --track-width: 4;
    --track-width-active: 6;

		/* styling for the second card */
		&.box-2{
      --card-border-radius: 20px;
    	--thumb-color: rgb(154, 230, 0);
    	--thumb-color-active: rgb(124, 207, 0);
    }
		
		
    position: relative;
    width: 100%;
    contain:size;
    margin: 3rem auto;
    border: 1px solid var(--clr-lines);
    border-radius: var(--card-border-radius);
    overflow: hidden;  
    & h2{
      text-wrap: balance;
      font-size: 1.1rem;
    }
    & img{
      max-width: 100%;
      display: block;
    }
  }
	
  
  .scroll-content {
    height: 100%;
    overflow-y: auto;
    padding:1rem 1.5rem 1rem 1rem; /* space on right for scrollbar */
	
    /* keep scrolling functional */
    overflow-y: scroll;
  
    /* hide scrollbars */
	 	scrollbar-width: none; /* Firefox */
   	-ms-overflow-style: none; /* IE and Edge Legacy */
		&::-webkit-scrollbar {
			display: none; /* Chrome, Safari, and Opera */
		}
	}
  .scrollbar-svg {
    position: absolute;
    inset: 0;
    width: 100%;
    height: 100%;
    pointer-events: none;
    &:has(:active) {
      --thumb-width: var(--thumb-width-active);
      --track-width: var(--track-width-active);
      --track-color: var(--track-color-active);
    }
  }
  
  .scrollbar-track {
    fill: none;
    stroke: var(--track-color);
    stroke-width: var(--track-width);
    stroke-linecap: round;
    transition: stroke-width,stroke;
    transiton-duration: 150ms;
    transition-timing-function:ease-in-out;
  }
  .scrollbar-thumb {
    fill: none;
    stroke: var(--thumb-color);
    stroke-width: var(--thumb-width);
    stroke-linecap: round;
    pointer-events: auto;
    cursor: grab;
    transition: stroke-width,stroke;
    transiton-duration: 150ms;
    transition-timing-function:ease-in-out;
    &:active {
      stroke: var(--thumb-color-active);
      cursor: grabbing;
    }
  }
  
}


@layer base{
  *{
  	box-sizing: border-box;
  }
  :root {
    color-scheme: light dark;
  
    --bg-dark: rgb(11, 11, 11);
    --bg-light: rgb(248, 244, 238);
    --txt-light: rgb(10, 10, 10);
    --txt-dark: rgb(245, 245, 245);
    --line-light: rgba(0 0 0 / .15);
    --line-dark: rgba(255 255 255 / .25);
  
    --clr-bg: light-dark(var(--bg-light), var(--bg-dark));
    --clr-txt: light-dark(var(--txt-light), var(--txt-dark));
    --clr-lines: light-dark(var(--line-light), var(--line-dark));
    --clr-accent: dodgerblue;
  
  }
  
  html, body {
    background-color: var(--clr-bg);
    color: var(--clr-txt);
    font-family: 'Krub', sans-serif;
    margin: 0;
    padding: 0;
    min-height: 100svh;
  }
  
  body {
    display: grid;
    place-content: center;
  	gap: 0;
    padding: 1rem;
    line-height: 1.4;
  }
  header{
    text-align: center;
    text-wrap: balance;
    width: min(100%, 60ch);
    margin: 0 auto;
  }
  h1{
    font-size: 1.2rem;
    margin: 0;
    text-align: center;
		text-transform: capitalize;
  }
  p{
    font-size: 0.8rem;
    opacity: .9;
  }
}
console.clear();

// config
const OFFSET = 7; // distance from edge of container
const EXTRA_INSET = 2;
const MIN_START_RATIO = 0.8;
const MIN_THUMB = 20;
const SEGMENTS = 50;

// init scrollbars - each has their own scoped functions and settings
document.querySelectorAll('[data-scrollbar]').forEach(container => {
  initCurvedScrollbar(container);
});

// function - init scrolled container
function initCurvedScrollbar(container) {
  const content = container.querySelector('.scroll-content');
  
  // create and add SVG
  svg = document.createElementNS("http://www.w3.org/2000/svg", "svg");
  svg.classList.add('scrollbar-svg');
  svg.setAttribute('aria-hidden', 'true');

  const trackPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  trackPath.classList.add('scrollbar-track');

  const thumbPath = document.createElementNS("http://www.w3.org/2000/svg", "path");
  thumbPath.classList.add('scrollbar-thumb');

  svg.appendChild(trackPath);
  svg.appendChild(thumbPath);
  container.appendChild(svg);
  
  // state
  let pathLength = 0;
  let thumbLength = 50;
  let dragging = false;
  let pointerId = null;

  // function - update
  function updatePath() {
    const w = container.clientWidth;
    const h = container.clientHeight;
    const r = parseFloat(getComputedStyle(container).borderRadius) || 0;

    const effectiveRadius = Math.max(r - OFFSET, 0);
    const trackX = w - OFFSET;
    const topY = OFFSET;
    const bottomY = h - OFFSET;
    const cornerX = trackX - effectiveRadius;

    // calculate x start point - ensure that it is never higher than the corner radius start point
    const minStartX = w * MIN_START_RATIO;
    let startX = trackX - effectiveRadius * EXTRA_INSET;
    if (startX < minStartX) startX = minStartX;
    if (startX > cornerX) startX = cornerX;

    // create the dynamic path
    const d = `
      M ${startX} ${topY}
      L ${cornerX} ${topY}                     
      A ${effectiveRadius} ${effectiveRadius} 0 0 1 ${trackX} ${topY + effectiveRadius} 
      L ${trackX} ${bottomY - effectiveRadius} 
      A ${effectiveRadius} ${effectiveRadius} 0 0 1 ${cornerX} ${bottomY} 
      L ${startX} ${bottomY}
    `;
    trackPath.setAttribute('d', d);

    // calculate length of thumb based on content height
    pathLength = trackPath.getTotalLength();
    const ratio = content.clientHeight / content.scrollHeight;
    thumbLength = Math.max(MIN_THUMB, pathLength * ratio);

    updateThumb();
  }

  // function - thumb update
  function updateThumb() {
    const scrollableHeight = content.scrollHeight - content.clientHeight || 1;
    const scrollRatio = content.scrollTop / scrollableHeight;
    const startOffset = (pathLength - thumbLength) * scrollRatio;
    const endOffset = startOffset + thumbLength;

    // the thumb needs to follow the path so we get the generated path points
    // NOTE - the high number of segments help follow the curves
    const points = [];
    for (let i = 0; i <= SEGMENTS; i++) {
      const t = startOffset + ((endOffset - startOffset) / SEGMENTS) * i;
      const p = trackPath.getPointAtLength(t);
      points.push(`${p.x} ${p.y}`);
    }
  
    const segmentD = `M ${points[0]} ${points.slice(1).map(pt => `L ${pt}`).join(' ')}`;
    thumbPath.setAttribute('d', segmentD);
  }

  //  function - grab thumb
  thumbPath.addEventListener('pointerdown', e => {
    e.preventDefault();
    dragging = true;
    pointerId = e.pointerId;
    thumbPath.setPointerCapture(pointerId);
  });
  
  // function - drag thumb
  window.addEventListener('pointermove', e => {
    if (!dragging || e.pointerId !== pointerId) return;
    const rect = container.getBoundingClientRect();
    let ratio = (e.clientY - rect.top) / rect.height;
    ratio = Math.max(0, Math.min(1, ratio));
    content.scrollTop = ratio * (content.scrollHeight - content.clientHeight);
    updateThumb();
  });
	
  // function - release thumb
  window.addEventListener('pointerup', e => {
    if (!dragging || e.pointerId !== pointerId) return;
    dragging = false;
    try { thumbPath.releasePointerCapture(pointerId); } catch {}
    pointerId = null;
  });

  // events
  content.addEventListener('scroll', updateThumb);
  window.addEventListener('resize', updatePath);

  updatePath();
}
Web component range slider supporting custom indicators, labels, various arcs, and keyboard/touch events using Shadow DOM.
HTML·CSS·JavaScript

Customizable Circular Range Slider Web Component

This highly accessible custom element is a masterclass in modern Web Components. It uses shadow DOM adopting stylesheets, semantic aria mappings, and native form integration to construct highly customizable circular dial sliders. By mapping custom trigonometric calculations onto conic gradients and offset paths, it perfectly structures speedometers, ovens, and semi-arc ranges with full keyboard and pointer capture support.

Level
Advanced
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Web Component · Responsive Sliders · Keyboard Native Inputs · Custom Arc Shapes
Loading preview
<form>
  <label id="speedometer-label">Speedometer
    <circular-range active-label="90" class="speedometer" aria-labelledby="speedometer-label" enable-min end="500" indices="50" labels="0:0,20:20,40:40,60:60,80:80,100:100,120:120,140:140,160:160,180:180,200:200" max="200" min="0" shift-step="10" start="220" suffix=" km" value="90">
    </circular-range>
  </label>

  <label id="full-circle-label">Full Circle
    <circular-range class="full-circle" aria-labelledby="full-circle-label" labels="0:0,5:5,10:10,15:15,20:20,25:25,30:30,35:35,40:40,45:45,50:50,55:55,60:60,65:65,70:70,75:75,80:80,85:85,90:90,95:95,100:0" value="50" min="0" max="100" suffix="%" step="5" indices="21"></circular-range>
  </label>

  <label id="oven-label">Oven
    <circular-range class="oven" aria-labelledby="oven-label" value="110" min="0" max="330" suffix="°C" step="5" start="20" end="340" indices="67" labels="0:0,20:20,40:40,60:60,80:80,100:100,120:120,140:140,160:160,180:180,200:200,220:220,240:240,260:260,280:280,300:300,320:320"></circular-range>
  </label>

  <label id="left-arc-label">Left Arc
    <circular-range class="left-arc" reverse aria-labelledby="left-arc-label" value="50" min="0" max="100" suffix="%" step="1" start="180" end="360" indices="11"></circular-range>
  </label>

  <label id="top-arc-label">Top Arc
    <circular-range class="top-arc" aria-labelledby="top-arc-label" value="50" min="0" max="100" suffix="$" step="1" start="270" end="450"></circular-range>
  </label>

  <label id="bottom-arc-label">Bottom Arc
    <circular-range class="bottom-arc" reverse aria-labelledby="bottom-arc-label" value="50" min="0" max="100" suffix="dB" step="1" start="90" end="270"></circular-range>
  </label>
</form>
form {
  --circular-range-w: 320px;
  font-family: system-ui, sans-serif;
  display: grid;
  gap: 2rem;
  grid-template-columns: repeat( auto-fit, minmax(320px, 1fr) );
  margin-inline: auto;
  max-width: 1024px;
}

label {
  display: grid;
  font-weight: 700;
  place-content: center;
  width: 100%;
}

.bottom-arc {
  --circular-range-output-as: start;
  --circular-range-output-gr: 4;
  clip-path: inset(40% 0 0 0);
  margin-top: -140px;
}

.top-arc {
  clip-path: inset(0 0 40% 0);
  margin-bottom: -120px;
}

.full-circle,
.left-arc,
.oven {
  --circular-range-output-as: center;
  --circular-range-output-gr: 3;
}
/**
 * @module CircularRange
 * @version 1.0.8
 * @date 2025-08-19
 * @author Mads Stoumann
 * @description A circular range slider custom element with optional indices and labels.
 */
class CircularRange extends HTMLElement {
  static formAssociated = true;
  static #css = `
    :host {
      --circular-range-bg: #0000;
      --circular-range-bg-mask: none;
      --circular-range-bg-scale: 1;
      --circular-range-fill: #0066cc;
      --circular-range-fill-end: var(--circular-range-fill);
      --circular-range-fill-middle: var(--circular-range-fill);
      --circular-range-fill-start: var(--circular-range-fill);
      --circular-range-indice-bdrs: 0;
      --circular-range-indice-c: #999;
      --circular-range-indice-h: 5px;
      --circular-range-indice-w: 1px;
      --circular-range-indices-w: 80%;
      --circular-range-labels-c: light-dark(#333, #CCC);
      --circular-range-labels-fs: x-small;
      --circular-range-labels-w: 70%;
      --circular-range-output-as: end;
      --circular-range-output-c: inherit;
      --circular-range-output-ff: inherit;
      --circular-range-output-fs: 200%;
      --circular-range-output-fw: 700;
      --circular-range-output-gr: 2;
      --circular-range-rows: 5;
      --circular-range-thumb: var(--circular-range-fill);
      --circular-range-thumb-min: #e0e0e0;
      --circular-range-thumb-bxsh: 0 0 0 2px Canvas;
      --circular-range-thumb-bxsh-focus: 0 0 0 2px Canvas;
      --circular-range-track: #f0f0f0;
      --circular-range-track-sz: 1.5rem;
      --circular-range-maw: 320px;

      --_ga: 1 / 1 / calc(var(--circular-range-rows) + 1) / 1;
      --_mask: radial-gradient(circle farthest-side at center, #0000 calc(100% - var(--circular-range-track-sz) - 1px), var(--circular-range-fill) calc(100% - var(--circular-range-track-sz)));
      --_fill-start-angle: var(--_start);
      --_fill-end-angle: var(--_fill);

      aspect-ratio: 1;
      display: grid;
      grid-template-rows: repeat(var(--circular-range-rows), 1fr);
      max-width: var(--circular-range-maw);
      place-items: center;
      touch-action: none;
      user-select: none;
      width: var(--circular-range-w, 100%);
    }

    [part]:not([part="thumb"]) { 
      pointer-events: none; 
      user-select: none;
      -webkit-user-select: none;
    }
    ::slotted(*) { 
      pointer-events: none; 
      user-select: none;
      -webkit-user-select: none;
    }
    
    /* Prevent selection on all pseudo-elements */
    ::before, ::after {
      user-select: none;
      -webkit-user-select: none;
    }

    :host(:focus-visible) {
      outline: 0;
      [part="fill"] { opacity: .65; }
      [part="thumb"]::before {
        box-shadow: var(--circular-range-thumb-bxsh-focus);
        scale: 1.2;
      }
    }

    /* === TRACK AND FILL === */

    [part="fill"],
    [part="track"] {
      border-radius: 50%;
      grid-area: var(--_ga);
      height: 100%;
      mask: var(--_mask);
      width: 100%;
    }

    [part="fill"] {
      background: conic-gradient(
        from calc(var(--_fill-start) * 1deg),
        var(--circular-range-fill-start) 0deg,
        var(--circular-range-fill-middle) calc(var(--_fill-range) * 0.5deg),
        var(--circular-range-fill-end) calc(var(--_fill-range) * 1deg),
        #0000 calc(var(--_fill-range) * 1deg)
      );
      transition: all 0.2s ease-in-out;
      &::before {
        background: var(--circular-range-fill);
        offset-distance: var(--_tb);
      }
    }

    [part="track"] {
      background: conic-gradient(
        from calc(var(--_start) * 1deg),
        var(--circular-range-track) 0deg,
        var(--circular-range-track) calc((var(--_end) - var(--_start)) * 1deg),
        #0000 calc((var(--_end) - var(--_start)) * 1deg)
      );
      &::before {
        background: var(--circular-range-track);
        offset-distance: var(--_tb);
      }
      &::after {
        background: var(--circular-range-track);
        offset-distance: var(--_tb); }
      }

    [part="fill"]::before,
    [part="track"]::before,
    [part="track"]::after {
      border-radius: 50%;
      content: '';
      display: block;
      height: var(--circular-range-track-sz);
      offset-anchor: top;
      offset-path: content-box;
      width: var(--circular-range-track-sz);
    }

    /* === THUMB === */

    [part="thumb"] {
      grid-area: var(--_ga);
      height: 100%;
      rotate: calc(1deg * var(--_fill, 0));
      width: var(--circular-range-track-sz);
      will-change: transform;
      &::before {
        aspect-ratio: 1;
        background-color: var(--circular-range-thumb);
        border-radius: 50%;
        box-shadow: var(--circular-range-thumb-bxsh);
        content: '';
        display: block;
        transition: all 0.2s ease-in-out;
        width: 100%;
      }
    }

    :host(.at-min) [part="thumb"]::before {
      background-color: var(--circular-range-thumb-min);
    }

    /* === VALUE === */

    :host::after {
      align-self: var(--circular-range-output-as);
      color: var(--circular-range-output-c);
      counter-reset: val var(--_value);
      content: counter(val) attr(suffix);
      font-family: var(--circular-range-output-ff);
      font-size: var(--circular-range-output-fs);
      font-weight: var(--circular-range-output-fw);
      grid-column: 1;
      grid-row: var(--circular-range-output-gr);
      isolation: isolate;
      pointer-events: none;
      text-box: cap alphabetic;
    }

    /* === OPTIONAL BACKGROUND LAYER === */

    :host::before {
      background: var(--circular-range-bg);
      border-radius: 50%;
      content: '';
      display: block;
      grid-area: var(--_ga);
      height: 100%;
      mask: var(--circular-range-bg-mask);
      scale: var(--circular-range-bg-scale);
      width: 100%;
    }

    /* === INDICES === */

    [part="indices"] {
      aspect-ratio: 1;
      border-radius: 50%;
      grid-area: var(--_ga);
      padding: 0;
      width: var(--circular-range-indices-w);
      li {
        background: var(--circular-range-indice-c);
        border-radius: var(--circular-range-indice-bdrs);
        display: inline-block;
        height: var(--circular-range-indice-h);
        offset-anchor: top;
        offset-distance: var(--_p, 0%);
        offset-path: content-box;
        width: var(--circular-range-indice-w);
      }
    }

    /* === LABELS === */

    [part="labels"] {
      aspect-ratio: 1;
      border-radius: 50%;
      color: var(--circular-range-labels-c);
      font-size: var(--circular-range-labels-fs);
      grid-area: var(--_ga);
      padding: 0;
      width: var(--circular-range-labels-w);
      li {
        display: inline-block;
        offset-anchor: top;
        offset-distance: var(--_p, 0%);
        offset-path: content-box;
        offset-rotate: 0deg;
      }
    }
  `;

  #angleAngle;
  #CX;
  #CY;
  #endAngle;
  #internals;
  #lastValue;
  #max;
  #min;
  #radian;
  #range;
  #reverse;
  #shiftStep;
  #startAngle;
  #step;
  #isSafari;
  #safariVisHandler;
  #safariPageShowHandler;

  constructor() {
    super();
    this.#isSafari = /Safari\//.test(navigator.userAgent) && !/(Chrome|Chromium|Edg|OPR|CriOS|FxiOS)/.test(navigator.userAgent);
    this.attachShadow({ mode: 'open' });
    this.#internals = this.attachInternals();
    const sheet = new CSSStyleSheet();
    sheet.replaceSync(CircularRange.#css);
    this.shadowRoot.adoptedStyleSheets = [sheet];
    this.shadowRoot.innerHTML = `
      <div part="track"></div>
      <div part="fill"></div>
      <div part="thumb"></div>
      <ul part="indices"></ul>
      <ol part="labels"></ol>
      <slot></slot>`;
  }

  connectedCallback() {
    this.tabIndex = 0;
    this.#readAttributes();
    this.#convertInitialValue();
    this.shadowRoot.querySelector('[part="indices"]').innerHTML = this.#generateIndices();
    this.#renderLabels();
    this.setAttribute('role', 'slider');
    /* Small timeout-hack to ensure styles are applied before the first update */
    setTimeout(() => {
      this.#update();
      this.#updateActiveLabel();
      const value = Number(this.getAttribute('value')) || 0;
      this.#internals.setFormValue(value);
      this.classList.toggle('at-min', this.hasAttribute('enable-min') && value === this.#min);
      if (this.#isSafari) this.#safariRepaint();
    });

    if (this.#isSafari) {
      this.#safariVisHandler = () => { if (!document.hidden) this.#safariRepaint(); };
      this.#safariPageShowHandler = () => this.#safariRepaint();
      document.addEventListener('visibilitychange', this.#safariVisHandler);
      window.addEventListener('pageshow', this.#safariPageShowHandler);
    }
    this.addEventListener('keydown', this.#keydown);
    this.addEventListener('pointerdown', this.#pointerdown);
  }

  disconnectedCallback() {
    this.removeEventListener('keydown', this.#keydown);
    this.removeEventListener('pointerdown', this.#pointerdown);
    if (this.#isSafari) {
      document.removeEventListener('visibilitychange', this.#safariVisHandler);
      window.removeEventListener('pageshow', this.#safariPageShowHandler);
    }
  }

  static get observedAttributes() { return ['value', 'active-label']; }

  attributeChangedCallback(name, oldValue, newValue) {
    if (oldValue === newValue) return;
    if (name === 'value') {
      this.#update();
    } else if (name === 'active-label') {
      this.#updateActiveLabel();
    }
  }

  get value() {
    const value = Number(this.getAttribute('value')) || 0;
    return this.#reverse ? this.#min + this.#max - value : value;
  }

  set value(newValue) {
    const normalizedValue = this.#reverse ? this.#min + this.#max - newValue : newValue;
    this.#setValue(normalizedValue);
  }

  get valueAsNumber() {
    return Number(this.value);
  }

  get name() { return this.getAttribute('name') ?? ''; }

  set name(newName) {
    if (newName === null || newName === undefined) {
      this.removeAttribute('name');
    } else {
      this.setAttribute('name', String(newName));
    }
  }

  #readAttributes() {
    this.#min = Number(this.getAttribute('min')) || 0;
    this.#max = Number(this.getAttribute('max')) || 100;
    this.setAttribute('aria-valuemin', this.#min);
    this.setAttribute('aria-valuemax', this.#max);
    this.#reverse = this.hasAttribute('reverse');
    this.#step = Number(this.getAttribute('step')) || 1;
    this.#shiftStep = Number(this.getAttribute('shift-step')) || this.#step;
    this.#startAngle = Number(this.getAttribute('start')) || 0;
    this.#endAngle = Number(this.getAttribute('end')) || 360;
    this.#range = this.#max - this.#min;
    this.#angleAngle = this.#endAngle - this.#startAngle;
    this.#radian = this.#angleAngle / this.#range;
    this.style.setProperty('--_start', this.#startAngle);
    this.style.setProperty('--_end', this.#endAngle);

    if (this.#reverse) {
      this.style.setProperty('--_tb', `${(this.#endAngle / 360) * 100}%`);
      this.style.setProperty('--_ta', `${(this.#startAngle / 360) * 100}%`);
    } else {
      this.style.setProperty('--_tb', `${(this.#startAngle / 360) * 100}%`);
      this.style.setProperty('--_ta', `${(this.#endAngle / 360) * 100}%`);
    }
  }

  #convertInitialValue() {
    if (this.#reverse && this.hasAttribute('value')) {
      const userValue = Number(this.getAttribute('value')) || 0;
      const internalValue = this.#min + this.#max - userValue;
      this.setAttribute('value', internalValue);
    }
  }

  #update() {
    const value = Number(this.getAttribute('value')) || 0;
    const displayValue = this.#reverse ? this.#min + this.#max - value : value;
    this.setAttribute('aria-valuenow', displayValue);
    const fillPercentage = this.#range > 0 ? (value - this.#min) / this.#range : 0;
    const fillAngle = this.#startAngle + (fillPercentage * this.#angleAngle);
    this.style.setProperty('--_value', displayValue);
    this.style.setProperty('--_fill', fillAngle);

    if (this.#reverse) {
      this.style.setProperty('--_fill-start', fillAngle);
      this.style.setProperty('--_fill-range', this.#endAngle - fillAngle);
    } else {
      this.style.setProperty('--_fill-start', this.#startAngle);
      this.style.setProperty('--_fill-range', fillAngle - this.#startAngle);
    }
  }

  #safariRepaint() {
    const v = this.getAttribute('value') ?? '0';
    this.getBoundingClientRect();
    this.style.setProperty('--_invalidate', performance.now().toString());
    this.setAttribute('value', v);
  }

  #setValue(newValue) {
    const steppedValue = Math.round((newValue - this.#min) / this.#step) * this.#step + this.#min;
    const clampedValue = Math.max(this.#min, Math.min(this.#max, steppedValue));
    if (this.value === clampedValue) return;
    this.setAttribute('value', clampedValue);
    this.#internals.setFormValue(clampedValue);

    this.classList.toggle('at-min', this.hasAttribute('enable-min') && clampedValue === this.#min);
    this.dispatchEvent(new Event('input', { bubbles: true }));
  }

  #updateActiveLabel() {
    const activeLabelValue = this.getAttribute('active-label');
    const labels = this.shadowRoot.querySelectorAll('[part="labels"] li');
    labels.forEach(label => label.part.remove('active-label'));
    if (activeLabelValue) {
      const activeLabel = this.shadowRoot.querySelector(`[part="labels"] li[value="${activeLabelValue}"]`);
      if (activeLabel) activeLabel.part.add('active-label');
    }
  }

  #pointerdown(event) {
    this.setPointerCapture(event.pointerId);
    this.#lastValue = Number(this.getAttribute('value')) || 0;
    this.#CX = this.offsetWidth / 2;
    this.#CY = this.offsetHeight / 2;
    this.#pointerMove(event);
    this.addEventListener('pointermove', this.#pointerMove);
    this.addEventListener('pointerup', () => this.removeEventListener('pointermove', this.#pointerMove), { once: true });
  }

  #pointerMove(event) {
    const degree = (((Math.atan2(event.offsetY - this.#CY, event.offsetX - this.#CX) * 180 / Math.PI) + 90 + 360) % 360);
    const relativeDegree = (degree - this.#startAngle + 360) % 360;
    let value = (relativeDegree / this.#radian) + this.#min;

    if (this.#angleAngle < 360) {
      if (relativeDegree > this.#angleAngle) {
        this.#setValue(relativeDegree - this.#angleAngle < (360 - this.#angleAngle) / 2 ? this.#max : this.#min);
        return;
      }
    } else if (Math.abs(value - this.#lastValue) > this.#range / 2) {
      value = value > this.#lastValue ? this.#min : this.#max;
    }

    this.#lastValue = value;
    this.#setValue(value);
  }

  #keydown(event) {
    if (!event.key.startsWith('Arrow')) return;
    event.preventDefault();
    const step = event.shiftKey ? this.#shiftStep : this.#step;
    const increment = (event.key === 'ArrowUp' || event.key === 'ArrowRight') ? step : -step;
    this.#setValue((Number(this.getAttribute('value')) || this.#min) + increment);
  }

  #generateIndices() {
    const count = parseInt(this.getAttribute('indices')) || 0;
    if (count === 0) return '';
    const startPercent = this.#startAngle / 360 * 100;
    const rangePercent = this.#angleAngle / 360 * 100;
    const step = rangePercent / (count - 1);
    return Array.from({ length: count }, (_, i) => 
      `<li style="--_p:${startPercent + (i * step)}%"></li>`
    ).join('');
  }

  #renderLabels() {
    const labels = this.getAttribute('labels');
    if (!labels) return;
    const ol = this.shadowRoot.querySelector('[part="labels"]');
    ol.setAttribute('aria-hidden', 'true');
    ol.innerHTML = '';

    const labelsArray = labels.split(',');
    const totalLabels = labelsArray.length;
    const stepSize = this.#range / (totalLabels - 1);

    labelsArray.forEach((pair, index) => {
      const [valueRaw, labelRaw] = pair.split(':');
      if (!valueRaw?.trim() || !labelRaw?.trim()) return;
      
      const value = Number(valueRaw.trim());
      if (value < this.#min || value > this.#max) return;

      const li = document.createElement('li');
      li.setAttribute('value', value);
      li.part.add(`label-${value}`);
      li.textContent = labelRaw.trim();

      const positionValue = this.#reverse 
        ? this.#min + ((totalLabels - 1 - index) * stepSize)
        : value;
      
      const degree = ((positionValue - this.#min) * this.#radian) + this.#startAngle;
      li.style.setProperty('--_p', `${(degree / 360) * 100}%`);

      ol.appendChild(li);
    });
  }
}

customElements.define('circular-range', CircularRange);
export default CircularRange;
Highly interactive drag-and-snap floating popover chat input with smooth transitions and gravity snap zones using GSAP FLIP.
HTML·CSS·JavaScript

FLIP Drag Snap Floating Popover on GSAP

This prototype demonstrates a dynamic draggable chat input combining the native HTML Popover API with GSAP FLIP, Draggable, and Inertia. Pulling the toggle button breaks its resistance to lock onto proximity gravity wells. Toggling the popover morphs the circular button smoothly into a fully functional editor using layout-driven calculations to prevent offscreen rendering.

Level
Advanced
Requires
gsap.js · gsap-draggable.js · gsap-inertia.js · gsap-flip.js · tweakpane.js
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Dynamic Drag Snapping · FLIP Layout Morphs · Popover API Native · Inertial Physics
Loading preview
<button
      aria-label="Start chat"
      class="toggle"
      popovertarget="chat"
      popovertargetaction="toggle"
    >
      <svg
        xmlns="http://www.w3.org/2000/svg"
        width="24"
        height="24"
        viewBox="0 0 24 24"
        fill="none"
        stroke="currentColor"
        stroke-width="2"
        stroke-linecap="round"
        stroke-linejoin="round"
      >
        <circle cx="12" cy="12" r="1"></circle>
        <path
          d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"
        ></path>
        <path
          d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"
        ></path>
      </svg>
    </button>
    <div id="chat" popover="manual">
      <span class="placeholder">
        <svg
          xmlns="http://www.w3.org/2000/svg"
          viewBox="0 0 24 24"
          fill="none"
          stroke="currentColor"
          stroke-width="2"
          stroke-linecap="round"
          stroke-linejoin="round"
        >
          <circle cx="12" cy="12" r="1"></circle>
          <path
            d="M20.2 20.2c2.04-2.03.02-7.36-4.5-11.9-4.54-4.52-9.87-6.54-11.9-4.5-2.04 2.03-.02 7.36 4.5 11.9 4.54 4.52 9.87 6.54 11.9 4.5Z"
          ></path>
          <path
            d="M15.7 15.7c4.52-4.54 6.54-9.87 4.5-11.9-2.03-2.04-7.36-.02-11.9 4.5-4.52 4.54-6.54 9.87-4.5 11.9 2.03 2.04 7.36.02 11.9-4.5Z"
          ></path>
        </svg>
      </span>
      <div class="popover__content">
        <textarea
          placeholder="What do you want to build?"
          name="prompt"
          id="prompt"
          data-gramm="false"
          autocomplete="off"
          spellcheck="false"
        ></textarea>

        <div class="actions">
          <div class="secondary">
            <button aria-label="Attach">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <path d="M13.234 20.252 21 12.3"></path>
                <path
                  d="m16 6-8.414 8.586a2 2 0 0 0 0 2.828 2 2 0 0 0 2.828 0l8.414-8.586a4 4 0 0 0 0-5.656 4 4 0 0 0-5.656 0l-8.415 8.585a6 6 0 1 0 8.486 8.486"
                ></path>
              </svg>
            </button>
            <button aria-label="Search">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <path d="M21.54 15H17a2 2 0 0 0-2 2v4.54"></path>
                <path
                  d="M7 3.34V5a3 3 0 0 0 3 3a2 2 0 0 1 2 2c0 1.1.9 2 2 2a2 2 0 0 0 2-2c0-1.1.9-2 2-2h3.17"
                ></path>
                <path
                  d="M11 21.95V18a2 2 0 0 0-2-2a2 2 0 0 1-2-2v-1a2 2 0 0 0-2-2H2.05"
                ></path>
                <circle cx="12" cy="12" r="10"></circle>
              </svg>
            </button>
            <button aria-label="Cook">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <path
                  d="M17 21a1 1 0 0 0 1-1v-5.35c0-.457.316-.844.727-1.041a4 4 0 0 0-2.134-7.589 5 5 0 0 0-9.186 0 4 4 0 0 0-2.134 7.588c.411.198.727.585.727 1.041V20a1 1 0 0 0 1 1Z"
                ></path>
                <path d="M6 17h12"></path>
              </svg>
            </button>
          </div>
          <div class="primary">
            <button aria-label="Minimize">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <polyline points="4 14 10 14 10 20"></polyline>
                <polyline points="20 10 14 10 14 4"></polyline>
                <line x1="14" x2="21" y1="10" y2="3"></line>
                <line x1="3" x2="10" y1="21" y2="14"></line>
              </svg>
            </button>
            <button aria-label="Submit" class="submit">
              <svg
                xmlns="http://www.w3.org/2000/svg"
                viewBox="0 0 24 24"
                fill="none"
                stroke="currentColor"
                stroke-width="2"
                stroke-linecap="round"
                stroke-linejoin="round"
              >
                <polyline points="9 10 4 15 9 20"></polyline>
                <path d="M20 4v7a4 4 0 0 1-4 4H4"></path>
              </svg>
            </button>
          </div>
        </div>
      </div>
    </div>
    <div class="targets">
      <div class="target target--north"></div>
      <div class="target target--west"></div>
      <div class="target target--south-west"></div>
      <div class="target target--south"></div>
      <div class="target target--south-east"></div>
      <div class="target target--east"></div>
    </div>
    <div class="info">
      <span class="arrow arrow--debug">
        <span>check<br />the debug</span>
        <svg
          viewBox="0 0 122 97"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            d="M116.102 0.0996005C114.952 0.334095 112.7 1.53002 111.433 2.53834C110.869 2.98388 109.368 4.15635 108.077 5.11778C103.455 8.6352 102.61 9.40903 102.187 10.4877C101.39 12.5982 102.798 14.5914 105.097 14.5914C106.13 14.5914 108.241 13.7941 109.696 12.8561C110.424 12.3871 111.01 12.0823 111.01 12.1526C111.01 12.692 107.796 17.8274 106.2 19.8206C102.023 25.0733 95.6642 29.6928 86.2548 34.2889C81.0926 36.8214 77.4555 38.2753 73.9123 39.2367C71.7066 39.823 70.6507 39.9871 67.9053 40.0809C66.0516 40.1513 64.5499 40.1747 64.5499 40.1278C64.5499 40.0809 64.808 38.9788 65.1365 37.6891C65.465 36.3993 65.8404 34.1716 66.0047 32.7647C66.4505 28.3796 65.4884 24.2994 63.4704 22.2359C62.1564 20.8758 60.9363 20.3599 59.0121 20.3599C57.6043 20.3599 57.1115 20.4537 55.7975 21.1103C52.8878 22.5407 50.5648 25.9878 49.5089 30.4197C48.453 34.922 49.2742 38.0877 52.3481 41.1127C53.4744 42.2148 54.46 42.9183 55.9852 43.6921C57.1584 44.2549 58.1439 44.7473 58.1909 44.7708C58.5898 45.0053 54.5304 53.4705 52.0666 57.6211C47.4674 65.3125 39.3486 74.575 30.5728 82.0789C22.2427 89.2309 16.7285 92.4435 9.87677 94.1553C8.28116 94.554 7.13138 94.6478 4.2452 94.6478C1.17131 94.6712 0.608154 94.7181 0.608154 95.023C0.608154 95.234 1.19478 95.5857 2.13337 95.9609C3.54126 96.4768 3.96363 96.5472 7.41296 96.5237C10.5572 96.5237 11.4724 96.4299 13.1149 96.0078C21.7265 93.6863 31.1594 87.1908 42.6102 75.7006C49.2977 69.0175 52.5828 64.9373 56.1494 58.9343C58.0501 55.7217 60.6312 50.6801 61.7575 47.9365L62.5553 45.9902L64.0806 46.1543C71.3547 46.9047 77.7136 45.3101 88.3667 40.034C96.2274 36.1414 101.976 32.3426 106.505 28.0748C108.617 26.0816 111.855 22.2828 112.794 20.7117C113.028 20.313 113.286 19.9847 113.357 19.9847C113.427 19.9847 113.662 20.782 113.873 21.72C114.084 22.6814 114.647 24.276 115.093 25.2609C115.82 26.8085 116.008 27.043 116.454 26.9727C116.876 26.9258 117.228 26.4333 117.956 24.9795C119.317 22.2828 119.833 20.2661 120.772 13.8879C121.757 7.25168 121.781 4.4143 120.889 2.56179C119.95 0.615488 118.12 -0.322489 116.102 0.0996005ZM60.7016 25.7767C61.4525 26.9023 61.8279 29.2942 61.6637 31.9205C61.4759 34.7813 60.5139 38.9788 60.0681 38.9788C59.5284 38.9788 57.1584 37.6422 56.2198 36.8214C54.8354 35.6021 54.3426 34.2889 54.5538 32.2957C54.8589 29.2473 56.1964 26.2223 57.5808 25.3547C58.7306 24.6512 60.0681 24.8388 60.7016 25.7767Z"
            fill="currentColor"
          />
        </svg>
      </span>
      <span class="arrow arrow--instruction">
        <span>open / drag and fling</span>
        <svg viewBox="0 0 97 52" fill="none" xmlns="http://www.w3.org/2000/svg">
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            d="M74.568 0.553803C74.0753 0.881909 73.6295 1.4678 73.3713 2.12401C73.1367 2.70991 72.3858 4.67856 71.6584 6.50658C70.9544 8.35803 69.4526 11.8031 68.3498 14.1936C66.1441 19.0214 65.839 20.2167 66.543 21.576C67.4581 23.3337 69.4527 23.9196 71.3064 22.9821C72.4797 22.3728 74.8965 19.5839 76.9615 16.4435C78.8387 13.5843 78.8387 13.6077 78.1113 18.3418C77.3369 23.4275 76.4687 26.2866 74.5915 30.0364C73.254 32.7316 71.8461 34.6299 69.218 37.3485C65.9563 40.6999 62.2254 42.9732 57.4385 44.4965C53.8718 45.6449 52.3935 45.8324 47.2546 45.8324C43.3594 45.8324 42.1158 45.7386 39.9805 45.2933C32.2604 43.7466 25.3382 40.9577 19.4015 36.9735C15.0839 34.0909 12.5028 31.7004 9.80427 27.9975C6.80073 23.9196 4.36038 17.2403 3.72682 11.475C3.37485 8.1471 3.1402 7.32683 2.43624 7.13934C0.770217 6.71749 0.183578 7.77211 0.0193217 11.5219C-0.26226 18.5996 2.55356 27.1304 7.17619 33.1066C13.8403 41.7545 25.432 48.4103 38.901 51.2696C41.6465 51.8555 42.2566 51.9023 47.4893 51.9023C52.3935 51.9023 53.426 51.832 55.5144 51.3867C62.2723 49.9337 68.5375 46.6292 72.949 42.1998C76.0464 39.1296 78.1113 36.2939 79.8946 32.7081C82.1942 28.0912 83.5317 23.3103 84.2591 17.17C84.3999 15.8576 84.6111 14.7795 84.7284 14.7795C84.8223 14.7795 85.4559 15.1311 86.1364 15.5763C88.037 16.7716 90.3835 17.8965 93.5748 19.0918C96.813 20.3339 97.3996 20.287 96.4141 18.9512C94.9123 16.9122 90.055 11.5219 87.1219 8.63926C84.0949 5.66288 83.8368 5.33477 83.5552 4.1864C83.3909 3.48332 83.0155 2.68649 82.6401 2.31151C82.0065 1.6553 80.4109 1.04595 79.9885 1.30375C79.8712 1.37406 79.2845 1.11626 78.6744 0.717845C77.2431 -0.172727 75.7413 -0.243024 74.568 0.553803Z"
            fill="currentColor"
          />
        </svg>
      </span>
      <span class="arrow arrow--move">
        <span>move these</span>
        <svg
          viewBox="0 0 45 119"
          fill="none"
          xmlns="http://www.w3.org/2000/svg"
        >
          <path
            fill-rule="evenodd"
            clip-rule="evenodd"
            d="M41.9623 0.226546C41.9154 0.320411 41.329 1.82224 40.6723 3.55873C40.039 5.29523 38.1628 9.73034 36.5444 13.4145C31.6425 24.4436 29.8835 28.8084 24.0669 44.2726C19.8921 55.3486 13.4187 75.7642 8.72794 92.6129L7.2269 97.9867L7.08616 94.8422C6.82816 89.3511 5.89001 87.333 3.52116 87.333C2.51263 87.333 1.71519 88.1544 1.45719 89.4919C1.33992 90.0551 1.08195 93.0823 0.894318 96.2502C0.706686 99.4181 0.448692 103.572 0.307967 105.519C0.190697 107.444 0.0499847 110.682 0.0265307 112.677C-0.0438314 115.985 0.00303082 116.384 0.448657 117.135C1.43373 118.825 3.28661 119.435 5.09257 118.684C6.03073 118.285 6.94542 117.206 9.713 113.263C13.9816 107.185 18.907 101.272 23.1991 97.0481C28.0775 92.2609 28.3355 91.7212 25.0754 93.1996C21.0647 94.983 16.6085 97.9867 12.7386 101.483C11.519 102.586 10.487 103.454 10.4401 103.407C10.3463 103.314 13.8409 91.0172 17.101 80.0585C19.7513 71.1883 22.7065 61.6141 24.747 55.419C27.5615 46.8069 34.6681 27.3065 38.2331 18.4598C41.7043 9.82421 43.4399 4.96671 43.9325 2.54969C44.167 1.37638 44.1435 1.11826 43.8386 0.602C43.4633 0.038811 42.2437 -0.219311 41.9623 0.226546Z"
            fill="currentColor"
          />
        </svg>
      </span>
    </div>
    <a
      aria-label="Follow Jhey"
      class="bear-link"
      href="https://twitter.com/intent/follow?screen_name=jh3yy"
      target="_blank"
      rel="noreferrer noopener"
    >
      <svg
        class="w-9"
        viewBox="0 0 969 955"
        fill="none"
        xmlns="http://www.w3.org/2000/svg"
      >
        <circle
          cx="161.191"
          cy="320.191"
          r="133.191"
          stroke="currentColor"
          stroke-width="20"
        ></circle>
        <circle
          cx="806.809"
          cy="320.191"
          r="133.191"
          stroke="currentColor"
          stroke-width="20"
        ></circle>
        <circle
          cx="695.019"
          cy="587.733"
          r="31.4016"
          fill="currentColor"
        ></circle>
        <circle
          cx="272.981"
          cy="587.733"
          r="31.4016"
          fill="currentColor"
        ></circle>
        <path
          d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
          fill="currentColor"
        ></path>
        <rect
          x="310.42"
          y="448.31"
          width="343.468"
          height="51.4986"
          fill="#FF1E1E"
        ></rect>
        <path
          fill-rule="evenodd"
          clip-rule="evenodd"
          d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
          fill="currentColor"
        ></path>
      </svg>
    </a>
@import url('https://unpkg.com/normalize.css') layer(normalize);
@import url('https://fonts.googleapis.com/css2?family=Gloria+Hallelujah&display=swap');

@layer normalize, base, demo, popover;

@layer popover {
  .placeholder {
    width: var(--width);
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    bottom: -1px;
    left: -1px;
    position: absolute;

    svg {
      width: 20px;
      height: 20px;
    }
  }

  [popover] {
    width: 380px;
    max-width: calc(100% - 2rem);
    height: 120px;
    border-radius: 6px;
    margin: 0;
    inset: unset;
    padding: 0.25rem;
    opacity: 0;
    border-color: light-dark(hsl(0 0% 75%), hsl(0 0% 60%));
    border-width: 1px;
    background: canvasText;
    color: canvas;
    --shadow-color: 0 0% 0%;
    box-shadow: 0px 0.5px 0.6px hsl(var(--shadow-color) / 0.07),
      0px 1.7px 2.2px -0.4px hsl(var(--shadow-color) / 0.09),
      0px 3.2px 4.1px -0.7px hsl(var(--shadow-color) / 0.11),
      -0.1px 6.1px 7.8px -1.1px hsl(var(--shadow-color) / 0.13),
      -0.2px 11.2px 14.3px -1.5px hsl(var(--shadow-color) / 0.15);

    .popover__content {
      display: flex;
      flex-direction: column;
      gap: 0.25em;
      height: 100%;
      width: 100%;
      filter: blur(4px);
      opacity: 0;
      width: calc(380px - 0.5em - 2px);
      max-width: calc(100vw - 2.5rem - 2px);
    }

    &:popover-open {
      display: flex;
      flex-direction: column;
    }

    .actions {
      display: flex;
      align-items: center;
      justify-content: space-between;
      height: 2rem;
      padding-left: 2rem;
      width: 100%;

      .primary,
      .secondary {
        display: flex;
        align-items: center;
      }

      .primary {
        gap: 0 0.25rem;
      }

      .primary button:first-of-type,
      .secondary button {
        background: #0000;
      }

      button {
        cursor: pointer;
        width: 32px;
        aspect-ratio: 1;
        display: grid;
        place-items: center;
        padding: 0;
        border: 0;
        border-radius: 6px;
        position: relative;

        &:not([aria-label='Submit'])::after {
          content: '';
          border-radius: 6px;
          position: absolute;
          inset: 0;
          background: red;
          background: color-mix(in oklch, canvasText, #0000 90%);
          opacity: 0;
        }

        &:is(:hover, :focus-visible)::after {
          opacity: 1;
        }

        &:is(:hover, :focus-visible) svg {
          opacity: 1;
        }

        svg {
          width: 16px;
          opacity: 0.5;
        }
      }
      button[aria-label='Submit'] {
        svg {
          opacity: 1;
        }

        &:is(:hover, :focus-visible) {
          background: color-mix(in oklch, canvasText, canvas 30%);
        }
      }
    }

    textarea {
      width: 100%;
      flex: 1;
      resize: none;
      outline-color: color-mix(in oklch, canvas, canvasText 2%);
      outline-color: #0000;
      font-size: 0.875rem;
      font-weight: 400;
      scrollbar-color: hsl(10 90% 50%) #0000;
      background: #0000;
      border-radius: 2px;
      caret-color: red;
      padding: 0.5em;
      border: 0;

      &::-moz-placeholder {
        white-space: nowrap;
        opacity: 0.8;
      }

      &:-ms-input-placeholder {
        white-space: nowrap;
        opacity: 0.8;
      }

      &::placeholder {
        white-space: nowrap;
        opacity: 0.8;
      }
    }
  }
}

@layer demo {
  .arrow {
    display: inline-block;
    opacity: 0.8;
    position: fixed;
    font-size: 0.875rem;
    font-family: 'Gloria Hallelujah', cursive;
    transition: opacity 0.26s ease-out;

    svg {
      color: hsl(0 0% 65%);
    }

    &.arrow--debug {
      top: 200px;
      right: 0px;
      translate: -50% 0;
      width: 60px;
      span {
        rotate: 12deg;
      }
      svg {
        rotate: 20deg;
        bottom: 130%;
        left: 0%;
        width: 100%;
        scale: -1 1;
      }
    }

    &.arrow--move {
      opacity: 0;
      top: 5.5rem;
      left: 50%;
      translate: calc((var(--snap-proximity) * -1px) + -150%) 0;
      width: 60px;
      span {
        rotate: -12deg;
      }
      svg {
        rotate: 270deg;
        top: 0%;
        left: 100%;
        width: 50%;
        scale: -1 1;
        translate: 180% -70%;
      }
    }

    &.arrow--instruction {
      top: 50%;
      left: 50%;
      translate: -130% -70%;

      svg {
        scale: 1 1;
        top: 130%;
        rotate: 25deg;
        left: 0%;
        width: 60%;
        translate: 120% 30%;
      }
    }

    span {
      display: inline-block;
      white-space: nowrap;
    }

    svg {
      position: absolute;
    }
  }
  [data-debug='true'] .arrow--move {
    opacity: 0.8;
  }
  :root {
    --width: 42px;
  }
  [data-dragging='true'] {
    cursor: -webkit-grabbing;
    cursor: grabbing;
  }
  [data-debug='true'] .target {
    opacity: 1;
  }
  .submit {
    background: canvasText;
    color: canvas;
  }
  .toggle {
    width: var(--width);
    aspect-ratio: 1;
    border-radius: 50%;
    display: grid;
    place-items: center;
    padding: 0;
    position: fixed;
    top: 0;
    left: 0;
    color: canvas;
    background: canvasText;
    border-style: solid;
    border-color: color-mix(in oklch, canvasText, #0000 85%);
    border-width: 1px;
    --shadow-color: 0 0% 0%;
    box-shadow: 0px 0.5px 0.6px hsl(var(--shadow-color) / 0.07),
      0px 1.7px 2.2px -0.4px hsl(var(--shadow-color) / 0.09),
      0px 3.2px 4.1px -0.7px hsl(var(--shadow-color) / 0.11),
      -0.1px 6.1px 7.8px -1.1px hsl(var(--shadow-color) / 0.13),
      -0.2px 11.2px 14.3px -1.5px hsl(var(--shadow-color) / 0.15);

    svg {
      width: 20px;
      height: 20px;
    }
  }
  .targets {
    position: fixed;
    inset: 1.5rem;
    pointer-events: none;
  }
  .target {
    pointer-events: all;
    width: var(--width);
    aspect-ratio: 1;
    background: hsl(var(--hue, 10) 80% 50% / 0.2);
    border-radius: 50%;
    opacity: 0;
    transition: opacity 0.26s ease-out;
    position: fixed;

    &[data-active='true'] {
      --hue: 150;
    }

    &::after {
      content: '';
      width: max(var(--width), var(--snap-proximity) * 2px);
      aspect-ratio: 1;
      border-radius: 50%;
      background: hsl(var(--hue, 10) 80% 50% / 0.1);
      outline: 2px dashed hsl(var(--hue, 10) 80% 50% / 0.5);
      position: absolute;
      top: 50%;
      left: 50%;
      translate: -50% -50%;
      pointer-events: none;
    }

    &.target--north {
      top: 1.5rem;
      left: 50%;
      translate: -50% 0%;
    }
    &.target--west {
      left: 1.5rem;
      top: 50%;
      translate: 0% -50%;
    }
    &.target--south-west {
      bottom: 1.5rem;
      left: 1.5rem;
    }
    &.target--south {
      bottom: 1.5rem;
      left: 50%;
      translate: -50% 0;
    }
    &.target--east {
      top: 50%;
      right: 1.5rem;
      translate: 0 -50%;
    }
    &.target--south-east {
      bottom: 1.5rem;
      right: 1.5rem;
    }
  }
}

@layer base {
  :root {
    --font-size-min: 16;
    --font-size-max: 20;
    --font-ratio-min: 1.2;
    --font-ratio-max: 1.33;
    --font-width-min: 375;
    --font-width-max: 1500;
  }

  html {
    color-scheme: light dark;
  }

  [data-theme='light'] {
    color-scheme: light only;
  }

  [data-theme='dark'] {
    color-scheme: dark only;
  }

  :where(.fluid) {
    --fluid-min: calc(
      var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
    );
    --fluid-max: calc(
      var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
    );
    --fluid-preferred: calc(
      (var(--fluid-max) - var(--fluid-min)) /
        (var(--font-width-max) - var(--font-width-min))
    );
    --fluid-type: clamp(
      (var(--fluid-min) / 16) * 1rem,
      ((var(--fluid-min) / 16) * 1rem) -
        (((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
        (var(--fluid-preferred) * var(--variable-unit, 100vi)),
      (var(--fluid-max) / 16) * 1rem
    );
    font-size: var(--fluid-type);
  }

  *,
  *:after,
  *:before {
    box-sizing: border-box;
  }

  body {
    background: light-dark(#fff, #000);
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family: 'SF Pro Text', 'SF Pro Icons', 'AOS Icons', 'Helvetica Neue',
      Helvetica, Arial, sans-serif, system-ui;
  }

  body::before {
    --size: 45px;
    --line: color-mix(in hsl, canvasText, transparent 80%);
    content: '';
    height: 100vh;
    width: 100vw;
    position: fixed;
    background: linear-gradient(
          90deg,
          var(--line) 1px,
          transparent 1px var(--size)
        )
        calc(var(--size) * 0.36) 50% / var(--size) var(--size),
      linear-gradient(var(--line) 1px, transparent 1px var(--size)) 0%
        calc(var(--size) * 0.32) / var(--size) var(--size);
    -webkit-mask: linear-gradient(-20deg, transparent 50%, white);
            mask: linear-gradient(-20deg, transparent 50%, white);
    top: 0;
    transform-style: flat;
    pointer-events: none;
    z-index: -1;
  }

  .bear-link {
    color: canvasText;
    position: fixed;
    top: 1rem;
    left: 1rem;
    width: 48px;
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    opacity: 0.8;
  }

  :where(.x-link, .bear-link):is(:hover, :focus-visible) {
    opacity: 1;
  }

  .bear-link svg {
    width: 75%;
  }

  /* Utilities */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
}
import { Pane } from 'https://esm.sh/[email protected]';
import gsap from 'https://esm.sh/[email protected]';
import { Draggable } from 'https://esm.sh/[email protected]/Draggable';
import { Flip } from 'https://esm.sh/[email protected]/Flip';

// use dynamic loading for the restricted InertiaPlugin
let InertiaPlugin;

const initApp = async () => {
  try {
    const module = await import('https://assets.codepen.io/16327/InertiaPlugin.min.js');
    InertiaPlugin = module.default || window.InertiaPlugin;
  } catch (e) {
    // fallback if esm import is blocked
    InertiaPlugin = window.InertiaPlugin;
  }

  gsap.registerPlugin(Draggable, InertiaPlugin, Flip);

  const config = {
    theme: 'system',
    proximity: 120,
    debug: false,
    duration: 0.35
  };

  const ctrl = new Pane({
    title: 'config',
    expanded: true
  });

  const update = () => {
    document.documentElement.dataset.theme = config.theme;
    document.documentElement.dataset.debug = config.debug;
    document.documentElement.style.setProperty('--snap-proximity', config.proximity);
  };

  const sync = event => {
    if (event.target.controller.view.labelElement.innerText === 'debug') {
      document.querySelector('.arrow--debug').style.opacity = 0;
    }
    if (!document.startViewTransition || event.target.controller.view.labelElement.innerText !== 'theme') {
      return update();
    }
    document.startViewTransition(() => update());
  };

  ctrl.addBinding(config, 'proximity', {
    min: 0,
    max: 300,
    step: 1,
    label: 'proximity'
  });

  ctrl.addBinding(config, 'duration', {
    min: 0.2,
    max: 5,
    step: 0.01,
    label: 'duration(s)'
  });

  ctrl.addBinding(config, 'debug', {
    label: 'debug'
  });

  ctrl.addBinding(config, 'theme', {
    label: 'theme',
    options: {
      System: 'system',
      Light: 'light',
      Dark: 'dark'
    }
  });

  ctrl.on('change', sync);
  update();

  const button = document.querySelector('[aria-label^="Start"]');
  const targets = document.querySelectorAll('.target');

  function getClosestPoint(target, maxDistance = Number.POSITIVE_INFINITY) {
    if (maxDistance !== Number.POSITIVE_INFINITY) {
      document.documentElement.style.setProperty('--snap-proximity', maxDistance);
    }
    let closest = null;
    let minDistance = Number.POSITIVE_INFINITY;

    const coordinates = Array.from(targets).map(el => {
      const rect = el.getBoundingClientRect();
      return {
        x: rect.left + rect.width / 2,
        y: rect.top + rect.height / 2,
        element: el
      };
    });

    for (const coord of coordinates) {
      const dx = Math.min(window.innerWidth, Math.max(0, target.x)) - coord.x;
      const dy = Math.min(window.innerHeight, Math.max(0, target.y)) - coord.y;
      const distance = Math.hypot(dx, dy);
      if (distance < minDistance) {
        minDistance = distance;
        closest = coord;
      }
    }

    if (minDistance <= maxDistance) {
      return {
        coordinate: closest,
        distance: minDistance
      };
    }

    return null;
  }

  const RESISTANCE_PIXELS = 80;
  const DEFAULT_RESISTANCE = 0.75;
  const END_RESISTANCE = 0;
  gsap.set(button, {
    x: window.innerWidth * 0.5,
    y: window.innerHeight * 0.5,
    yPercent: -50,
    xPercent: -50
  });

  for (const target of targets) {
    Draggable.create(target, {
      inertia: false,
      type: 'x,y',
      onDrag: () => {
        document.querySelector('.arrow--move').style.opacity = 0;
      }
    });
  }

  Draggable.create(button, {
    inertia: true,
    allowContextMenu: true,
    type: 'x,y',
    dragResistance: DEFAULT_RESISTANCE,
    resistance: 1800,
    snap: {
      points: function (point) {
        const { isDragging, __unlocked, startX, startY } = this;
        const closestPoint = getClosestPoint(point, config.proximity);
        if (!isDragging && !__unlocked) {
          return { x: startX, y: startY };
        } else if (__unlocked && closestPoint) {
          for (const t of targets) t.dataset.active = false;
          closestPoint.coordinate.element.dataset.active = true;
          return closestPoint.coordinate;
        }
        return point;
      }
    },
    bounds: '.targets',
    onDragStart: function (event) {
      const bounds = this.target.getBoundingClientRect();
      const currentPoint = {
        x: bounds.left + bounds.width / 2,
        y: bounds.top + bounds.height / 2
      };

      this.__start = currentPoint;
      this.dragResistance = DEFAULT_RESISTANCE;
      this.__unlocked = false;
      document.documentElement.dataset.dragging = true;
    },
    onDrag: function (event) {
      const { startX, startY, x, y } = this;
      const distance = Math.hypot(x - startX, y - startY);
      const newResistance = gsap.utils.clamp(
        END_RESISTANCE,
        DEFAULT_RESISTANCE,
        gsap.utils.mapRange(0, RESISTANCE_PIXELS, DEFAULT_RESISTANCE, END_RESISTANCE)(Math.abs(distance))
      );

      if (!this.__unlocked) this.dragResistance = newResistance;
      if (!this.__unlocked && newResistance === END_RESISTANCE) {
        this.__unlocked = true;
        document.querySelector('.arrow--instruction').style.opacity = 0;
        for (const t of targets) t.dataset.active = false;
      }
    },
    onDragEnd: function () {
      this.dragResistance = DEFAULT_RESISTANCE;
      this.__unlocked = false;
    },
    onRelease: () => {
      document.documentElement.dataset.dragging = false;
    },
    onThrowComplete: function () {
      this.dragResistance = DEFAULT_RESISTANCE;
      this.__unlocked = false;
    }
  });

  const pop = document.querySelector('[popover]');
  let f;

  const closePopover = async () => {
    if (f) {
      f.pause();
      await f.reverse(f.time());
      pop.hidePopover();
    } else {
      let closerBounds;
      const pos = pop.getBoundingClientRect();
      const center = {
        x: pos.x + pos.width * 0.5,
        y: pos.y + pos.height * 0.5
      };

      const {
        x: buttonX,
        y: buttonY,
        width: buttonWidth,
        height: buttonHeight
      } = button.getBoundingClientRect();
      const closest = getClosestPoint({ x: center.x, y: center.y }, config.proximity);

      if (closest) {
        closerBounds = closest.coordinate.element.getBoundingClientRect();
        gsap.set(button, {
          opacity: 0,
          x: closest.coordinate.x,
          y: closest.coordinate.y
        });
      }

      const state = Flip.getState(
        [pop, '.placeholder', '.placeholder > svg', '[popover] .popover__content'],
        { nested: true, props: 'borderRadius, scale, opacity, filter, backgroundColor, color' }
      );

      gsap.set(pop, {
        width: buttonWidth,
        height: buttonHeight,
        top: closest ? closerBounds.top + closerBounds.height * 0.5 : buttonY + buttonHeight * 0.5,
        left: closest ? closerBounds.left + closerBounds.width * 0.5 : buttonX + buttonWidth * 0.5,
        opacity: 1,
        borderRadius: '21px',
        x: 0,
        y: 0,
        xPercent: -50,
        yPercent: -50,
        backgroundColor: getComputedStyle(document.body).color,
        color: getComputedStyle(document.body).backgroundColor
      });

      gsap.set('.placeholder > svg', {
        width: 20,
        height: 20,
        opacity: 1
      });

      gsap.set('[popover] .popover__content', {
        y: 0,
        opacity: 0,
        filter: 'blur(4px)'
      });

      await Flip.from(state, {
        duration: config.duration,
        nested: true,
        ease: 'power2.inOut'
      });

      gsap.set(button, { opacity: 1 });
      pop.hidePopover();
    }
  };

  let d;

  pop.addEventListener('toggle', async event => {
    if (event.newState === 'open') {
      const {
        x: buttonX,
        y: buttonY,
        width: buttonWidth,
        height: buttonHeight
      } = button.getBoundingClientRect();
      const center = {
        x: buttonX + buttonWidth * 0.5,
        y: buttonY + buttonHeight * 0.5
      };

      const {
        x: popX,
        y: popY,
        width: popWidth,
        height: popHeight
      } = pop.getBoundingClientRect();

      let vertical = 'center';
      if (center.y < popHeight * 0.5 + 24) vertical = 'bottom';
      if (center.y > window.innerHeight - (popHeight * 0.5 + 24)) vertical = 'top';
      let horizontal = 'center';
      if (center.x < popWidth * 0.5) horizontal = 'left';
      if (center.x > window.innerWidth - popWidth * 0.5) horizontal = 'right';

      gsap.set('[popovertarget]', {
        boxShadow: 'none'
      });

      gsap.set(pop, {
        width: buttonWidth,
        height: buttonHeight,
        top: buttonY,
        left: buttonX,
        opacity: 1,
        borderRadius: '21px'
      });

      const state = Flip.getState(
        [pop, '.placeholder', '.placeholder > svg', '[popover] .popover__content'],
        { nested: true, props: 'borderRadius, scale, opacity, filter, backgroundColor, color' }
      );

      const position = {
        left: center.x,
        top: center.y,
        xPercent: -50,
        yPercent: -50
      };

      if (horizontal === 'left') {
        position.right = 'unset';
        position.left = '1.5rem';
        position.xPercent = 0;
      }
      if (horizontal === 'right') {
        position.left = 'unset';
        position.right = '1.5rem';
        position.xPercent = 0;
      }
      if (vertical === 'bottom') {
        position.top = '1.5rem';
        position.bottom = 'unset';
        position.yPercent = 0;
      }
      if (vertical === 'top') {
        position.bottom = '1.5rem';
        position.top = 'unset';
        position.yPercent = 0;
      }

      gsap.set(pop, { clearProps: 'width,height' });
      gsap.set(pop, {
        ...position,
        opacity: 1,
        borderRadius: '6px',
        backgroundColor: getComputedStyle(document.body).backgroundColor,
        color: getComputedStyle(document.body).color
      });

      gsap.set('.placeholder > svg', {
        width: 16,
        height: 16,
        opacity: 0.5
      });

      gsap.set('[popover] .popover__content', {
        y: 0,
        opacity: 1,
        filter: 'blur(0px)',
        background: '#0000'
      });

      f = Flip.from(state, {
        duration: config.duration,
        nested: true,
        ease: 'power2.inOut'
      });

      window.addEventListener('keydown', closePopover);
      d = Draggable.create(pop, {
        dragClickables: false,
        inertia: true,
        allowContextMenu: true,
        type: 'x,y',
        bounds: '.targets',
        onDrag: () => {
          f = undefined;
          const pos = pop.getBoundingClientRect();
          const center = {
            x: pos.x + pos.width * 0.5,
            y: pos.y + pos.height * 0.5,
            xPercent: -50,
            yPercent: -50
          };

          gsap.set(button, {
            x: center.x,
            y: center.y,
            xPercent: -50,
            yPercent: -50
          });
        },
        onThrowUpdate: () => {
          f = undefined;
          const pos = pop.getBoundingClientRect();
          const center = {
            x: pos.x + pos.width * 0.5,
            y: pos.y + pos.height * 0.5
          };

          gsap.set(button, {
            x: center.x,
            y: center.y
          });
        }
      });
    } else {
      gsap.set([pop, '.placeholder > svg', '[popover] .popover__content'], {
        clearProps: 'all'
      });

      gsap.set(button, {
        clearProps: 'box-shadow',
        opacity: 1
      });

      window.removeEventListener('keydown', closePopover);
    }
  });

  document.querySelector('[aria-label="Minimize"]').addEventListener('click', closePopover);
};

initApp();
Interactive responsive grid of neon mathematical symbols that spin and glow upon pointer hover, built with GSAP and Tweakpane.
HTML·CSS·JavaScript

Interactive Touch Hover Grid on GSAP

This grid hover sandbox delivers slick cursor tracking without a bloated library. It dynamically generates a responsive CSS grid of “+” markers that rotate, brighten, and fade on hover. To maintain seamless feedback on mobile, a Pointer Events API fallback uses elementFromPoint to track touch vectors. It also integrates draggable, double-click resettable Tweakpane configs.

Level
Intermediate
Requires
gsap · gsap-draggable · tweakpane
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Pointer Hover Tracing · CSS Nesting Layers · Tweakpane Bindings · Touch Fallbacks
Loading preview
<main>
  <h1 class="fluid">hey.</h1>
  <div class="grid"></div>
</main>
<a
  aria-label="Follow Jhey"
  class="bear-link"
  href="https://twitter.com/intent/follow?screen_name=jh3yy"
  target="_blank"
  rel="noreferrer noopener"
>
  <svg
    class="w-9"
    viewBox="0 0 969 955"
    fill="none"
    xmlns="http://www.w3.org/2000/svg"
  >
    <circle
      cx="161.191"
      cy="320.191"
      r="133.191"
      stroke="currentColor"
      stroke-width="20"
    ></circle>
    <circle
      cx="806.809"
      cy="320.191"
      r="133.191"
      stroke="currentColor"
      stroke-width="20"
    ></circle>
    <circle
      cx="695.019"
      cy="587.733"
      r="31.4016"
      fill="currentColor"
    ></circle>
    <circle
      cx="272.981"
      cy="587.733"
      r="31.4016"
      fill="currentColor"
    ></circle>
    <path
      d="M564.388 712.083C564.388 743.994 526.035 779.911 483.372 779.911C440.709 779.911 402.356 743.994 402.356 712.083C402.356 680.173 440.709 664.353 483.372 664.353C526.035 664.353 564.388 680.173 564.388 712.083Z"
      fill="currentColor"
    ></path>
    <rect
      x="310.42"
      y="448.31"
      width="343.468"
      height="51.4986"
      fill="#FF1E1E"
    ></rect>
    <path
      fill-rule="evenodd"
      clip-rule="evenodd"
      d="M745.643 288.24C815.368 344.185 854.539 432.623 854.539 511.741H614.938V454.652C614.938 433.113 597.477 415.652 575.938 415.652H388.37C366.831 415.652 349.37 433.113 349.37 454.652V511.741L110.949 511.741C110.949 432.623 150.12 344.185 219.845 288.24C289.57 232.295 384.138 200.865 482.744 200.865C581.35 200.865 675.918 232.295 745.643 288.24Z"
      fill="currentColor"
    ></path>
  </svg>
</a>
@import url("https://unpkg.com/normalize.css") layer(normalize);

@layer normalize, base, demo, mobile;

@layer mobile {
  .grid div[data-hover] {
    transition-property: opacity, rotate, filter;
    transition-duration: 0s;
    rotate: calc(var(--grade, 0) * 90deg);
    filter: grayscale(0) brightness(1.5);
    opacity: 1;
  }
}

@layer demo {
  h1 {
    --font-level: 5;
    translate: 12px;
    margin: 0;
    margin-bottom: 1rem;
    color: color-mix(in hsl, canvasText, canvas 25%);
  }
  .grid {
    touch-action: none;
    display: grid;
    grid-template: repeat(var(--rows, 10), 1fr) / repeat(var(--cols, 10), 1fr);
    font-size: 2rem;
    font-weight: 600;
    line-height: 1;
    font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
    cursor: none;

    div {
      touch-action: none;
      padding: 2px;
      opacity: calc(var(--opacity) * 0.5);
      aspect-ratio: 1;
      display: grid;
      place-items: center;
      transition-property: opacity, rotate, filter;
      transition-duration: 0.8s, 0.4s, 0.6s;
      transition-timing-function: ease-in, ease-out, ease-out;
      scale: 1.5;
      filter: grayscale(1);
      color: hsl(var(--hue) 80% 50%);

      @media (prefers-color-scheme: dark) {
        opacity: calc(var(--opacity) + 0.2);
      }
      @media (hover: hover) and (pointer: fine) {
        &:hover {
          transition-property: opacity, rotate, filter;
          transition-duration: 0s;
          rotate: calc(var(--grade, 0) * 90deg);
          filter: grayscale(0) brightness(1.5);
          opacity: 1;
        }
      }
    }
  }
  [data-theme="dark"] .grid div {
    opacity: calc(var(--opacity) + 0.2);
    @media (hover: hover) and (pointer: fine) {
      &:hover {
        opacity: 1;
      }
    }
  }
}

@layer base {
  :root {
    --font-size-min: 16;
    --font-size-max: 20;
    --font-ratio-min: 1.2;
    --font-ratio-max: 1.33;
    --font-width-min: 375;
    --font-width-max: 1500;
  }

  html {
    color-scheme: light dark;
  }

  [data-theme="light"] {
    color-scheme: light only;
  }

  [data-theme="dark"] {
    color-scheme: dark only;
  }

  :where(.fluid) {
    --fluid-min: calc(
      var(--font-size-min) * pow(var(--font-ratio-min), var(--font-level, 0))
    );
    --fluid-max: calc(
      var(--font-size-max) * pow(var(--font-ratio-max), var(--font-level, 0))
    );
    --fluid-preferred: calc(
      (var(--fluid-max) - var(--fluid-min)) /
        (var(--font-width-max) - var(--font-width-min))
    );
    --fluid-type: clamp(
      (var(--fluid-min) / 16) * 1rem,
      ((var(--fluid-min) / 16) * 1rem) -
        (((var(--fluid-preferred) * var(--font-width-min)) / 16) * 1rem) +
        (var(--fluid-preferred) * var(--variable-unit, 100vi)),
      (var(--fluid-max) / 16) * 1rem
    );
    font-size: var(--fluid-type);
  }

  *,
  *:after,
  *:before {
    box-sizing: border-box;
  }

  body {
    overflow-x: hidden;
    background: light-dark(#fff, #000);
    display: grid;
    place-items: center;
    min-height: 100vh;
    font-family: "SF Pro Text", "SF Pro Icons", "AOS Icons", "Helvetica Neue",
      Helvetica, Arial, sans-serif, system-ui;
  }

  body::before {
    --size: 45px;
    --line: color-mix(in hsl, canvasText, transparent 80%);
    content: "";
    height: 100vh;
    width: 100vw;
    position: fixed;
    background: linear-gradient(
          90deg,
          var(--line) 1px,
          transparent 1px var(--size)
        )
        calc(var(--size) * 0.36) 50% / var(--size) var(--size),
      linear-gradient(var(--line) 1px, transparent 1px var(--size)) 0%
        calc(var(--size) * 0.32) / var(--size) var(--size);
    -webkit-mask: linear-gradient(-20deg, transparent 50%, white);
    mask: linear-gradient(-20deg, transparent 50%, white);
    top: 0;
    transform-style: flat;
    pointer-events: none;
    z-index: -1;
  }

  .bear-link {
    color: canvasText;
    position: fixed;
    top: 1rem;
    left: 1rem;
    width: 48px;
    aspect-ratio: 1;
    display: grid;
    place-items: center;
    opacity: 0.8;
  }

  :where(.x-link, .bear-link):is(:hover, :focus-visible) {
    opacity: 1;
  }

  .bear-link svg {
    width: 75%;
  }

  /* Utilities */
  .sr-only {
    position: absolute;
    width: 1px;
    height: 1px;
    padding: 0;
    margin: -1px;
    overflow: hidden;
    clip: rect(0, 0, 0, 0);
    white-space: nowrap;
    border-width: 0;
  }
}

div.tp-dfwv {
  width: 256px;
  position: fixed;
}
import gsap from "https://esm.sh/[email protected]";
import Draggable from "https://esm.sh/[email protected]/Draggable";
import { Pane } from "https://esm.sh/[email protected]";
gsap.registerPlugin(Draggable);

const config = {
  theme: "system",
  cols: 20,
  rows: 10
};

const ctrl = new Pane({
  title: "config",
  expanded: true
});

const grid = document.querySelector(".grid");
const buildGrid = () => {
  grid.innerHTML = new Array(config.cols * config.rows)
    .fill(0)
    .map(
      (_, index) => `
    <div style="
      --grade: ${Math.floor(Math.random() * 12 - 6)};
      --opacity: ${Math.min(Math.random(), 0.2)};
      --hue: ${Math.floor(Math.random() * 30)};
    ">+</div>
  `
    )
    .join("");
  grid.style.setProperty("--cols", config.cols);
  grid.style.setProperty("--rows", config.rows);
};
buildGrid();

const update = () => {
  document.documentElement.dataset.theme = config.theme;
};

const sync = (event) => {
  if (
    !document.startViewTransition ||
    event.target.controller.view.labelElement.innerText !== "theme"
  )
    return update();
  document.startViewTransition(() => update());
};

ctrl
  .addBinding(config, "cols", {
    min: 5,
    max: 30,
    step: 1
  })
  .on("change", buildGrid);
ctrl
  .addBinding(config, "rows", {
    min: 5,
    max: 30,
    step: 1
  })
  .on("change", buildGrid);

ctrl.addBinding(config, "theme", {
  label: "theme",
  options: {
    system: "system",
    light: "light",
    dark: "dark"
  }
});

ctrl.on("change", sync);
update();

// make tweakpane panel draggable
const tweakClass = "div.tp-dfwv";
const d = Draggable.create(tweakClass, {
  type: "x,y",
  allowEventDefault: true,
  trigger: tweakClass + " button.tp-rotv_b"
});

document.querySelector(tweakClass).addEventListener("dblclick", () => {
  gsap.to(tweakClass, {
    x: `+=${d[0].x * -1}`,
    y: `+=${d[0].y * -1}`,
    onComplete: () => {
      gsap.set(tweakClass, { clearProps: "all" });
    }
  });
});

if (window.matchMedia("(hover: none) and (pointer: coarse)").matches) {
  grid.addEventListener(
    "pointermove",
    (event) => {
      var _document$querySelect;
      (_document$querySelect = document.querySelector("[data-hover]")) ===
        null || _document$querySelect === void 0
        ? void 0
        : _document$querySelect.removeAttribute("data-hover");
      document.elementFromPoint(event.x, event.y).dataset.hover = "true";
    },
    true
  );
  grid.addEventListener(
    "pointerleave",
    (event) => {
      var _document$querySelect2;
      (_document$querySelect2 = document.querySelector("[data-hover]")) ===
        null || _document$querySelect2 === void 0
        ? void 0
        : _document$querySelect2.removeAttribute("data-hover");
    },
    true
  );
}
Hyper-realistic ticket UI replicating a printed invoice slipping out of a terminal slot with payment progress checkpoints.
HTML·CSS

Trip Invoice Card with Slotted Ticket Effect

This creative CSS-only component simulates a skeuomorphic paper invoice emerging from a card dispenser slot. It leverages relative positioning, absolute stacking, and linear gradient overlays to cast believable ticket-dispenser shadows over the receipt. The clean typography and status tracker create a tactical, mobile-focused travel checkout card.

Level
Beginner
Browsers
Chrome · Edge · Firefox · Safari
Highlights
Slotted Ticket Illusion · Depth Shadowing · Progress Timeline · Clean CSS Layout
Loading preview
<section class="container">
  <section class="invoice-container">
    <div class="invoice-slot">
      <div class="slot-hole"></div>
    </div>
    <div class="invoice">
      <h2 class="title">Trip Invoice &mdash; Japan Summer 2025</h2>
      <p class="amount">
        Total <span class="value">$30,000</span>
      </p>
      <p class="amount">
        Per Person <span class="value">$6,000</span>
      </p>

      <hr />

      <ul class="payers-list">
        <li>
          <div class="payer-image-container">
            <img src="https://res.cloudinary.com/dmuyehme1/image/upload/v1761081494/user1_okmfkd.png" />
          </div>
          <p>
            You
            <span class="pay-tag"><i class="fa-solid fa-circle-check"></i> Paid</span>
          </p>
        </li>
        <li>
          <div class="payer-image-container">
            <img src="https://res.cloudinary.com/dmuyehme1/image/upload/v1761082627/user4_dujvoe.svg" />
          </div>
          <p>
            Olabode
            <span class="pay-tag"><i class="fa-solid fa-circle-check"></i> Paid</span>
          </p>
        </li>
        <li>
          <div class="payer-image-container">
            <img src="https://res.cloudinary.com/dmuyehme1/image/upload/v1761082286/user3_ue0zzq.avif" />
          </div>
          <p>
            Lukmon
            <span class="pay-tag"><i class="fa-solid fa-circle-check"></i> Paid</span>
          </p>
        </li>
        <li>
          <div class="payer-image-container">
            <img src="https://res.cloudinary.com/dmuyehme1/image/upload/v1761081494/user1_okmfkd.png" />
          </div>
          <p>
            Hope
            <span class="pay-tag"><i class="fa-solid fa-clock"></i> Unpaid</span>
          </p>
        </li>
        <li>
          <div class="payer-image-container">
            <img src="https://res.cloudinary.com/dmuyehme1/image/upload/v1761082184/user2_b821x7.avif" />
          </div>
          <p>
            Dara
            <span class="pay-tag"><i class="fa-solid fa-clock"></i> Unpaid</span>
          </p>
        </li>
      </ul>

      <div class="payment-status">
        <p class="heading">
          Payment Status
          <span>Unpaid</span>
        </p>
        <div class="status-progress">
          <div class="checkpoint">
            <i class="fa-solid fa-circle-check"></i>
          </div>
          <div class="checkpoint">
            <i class="fa-solid fa-circle-check"></i>
          </div>
          <div class="checkpoint">
            <i class="fa-solid fa-circle-check"></i>
          </div>
          <div class="checkpoint">
            <span class="circle"></span>
          </div>
          <div class="checkpoint">
            <i class="fa-solid fa-stamp"></i>
          </div>
        </div>
      </div>
      <div class="btn-group">
        <button class="btn reminder-btn">Send Reminder</button>
        <button class="btn download-btn">Download Invoice</button>
      </div>
    </div>
  </section>
  <hr />
  <div class="payment-info">
    <p>Payment Method</p>
    <div class="card-info">
      <p>Visa Ending 2986</p>
      <span class="card-icon"></span>
    </div>
  </div>
  <button class="pay-now-btn">Pay Now</button>
</section>
*,
*::before,
*::after {
  margin: 0;
  padding: 0;
  box-sizing: border-box;
}

body {
  font-family: sans-serif;
}

.container {
  width: min(95%, 425px);
  margin: 1em auto;
}

/* ---- invoice ---- */
.invoice-container {
  position: relative;
  margin-bottom: 2em;
  height: 630px;
}

.invoice-slot {
  width: 100%;
  height: 120px;
  background-color: #2b2b2b;
  border: 2px solid #2c2c2c;
  border-radius: 1em;
  box-shadow: 0 0 1px 0 #000, 0 5px 15px 0 rgba(0, 0, 0, 0.45);
}

.slot-hole {
  background-color: #000;
  border-radius: 100vmax;
  width: 90%;
  height: 25px;
  margin: 1em auto;
  border: 1px solid #1b1b1b;
  box-shadow: 0 0 1px 0 #000, 0 5px 15px 0 rgba(0, 0, 0, 0.45);
}

.invoice {
  position: absolute;
  width: 85%;
  top: 1.5em;
  left: 50%;
  transform: translateX(-50%);
  background-color: #fff;
  color: #6b7280;
  padding: 1em;
  border-radius: 0.5em;
  box-shadow: 0 5px 25px 0 rgba(0, 0, 0, 0.15);
}

.invoice::before {
  content: "";
  position: absolute;
  width: 100%;
  height: 80px;
  left: 0;
  top: 0;
  background: linear-gradient(
    180deg,
    rgba(0, 0, 0, 0.95) 0%,
    rgba(0, 0, 0, 0.75) 10%,
    rgba(0, 0, 0, 0.65) 25%,
    rgba(0, 0, 0, 0.45) 40%,
    rgba(0, 0, 0, 0.25) 60%,
    transparent 100%
  );
}

.invoice .title {
  position: relative;
  font-size: 1.15rem;
  padding: 0.55em 0;
  letter-spacing: 0.5px;
  text-align: center;
  margin-bottom: 1.25em;
  font-weight: 500;
  color: #1b1b1b;
}

.invoice .title::before {
  content: "";
  position: absolute;
  height: 1.5px;
  width: 100%;
  top: 0;
  left: 0;
  background-image: repeating-linear-gradient(
    90deg,
    #1b1b1b,
    #1b1b1b 8px,
    transparent 8px,
    transparent 16px
  );
}

.invoice .title::after {
  content: "";
  position: absolute;
  height: 1.5px;
  width: 100%;
  bottom: 0;
  left: 0;
  background-image: repeating-linear-gradient(
    90deg,
    #1b1b1b,
    #1b1b1b 8px,
    transparent 8px,
    transparent 16px
  );
}

.invoice .amount,
.invoice .payment-status .heading {
  display: flex;
  align-items: center;
  justify-content: space-between;
  font-size: 1rem;
  margin-bottom: 0.5em;
}

.invoice .amount .value {
  font-weight: 700;
  color: #000;
}

.invoice .payment-status {
  border: 1px solid #ddd;
  padding: 1em;
  border-radius: 15px;
  margin-top: 1em;
}

.invoice .payment-status .heading span {
  text-transform: uppercase;
  font-weight: 500;
  color: #000;
}

.payers-list {
  list-style-type: none;
  margin: 0.5em 0;
}

.payers-list li {
  border-bottom: 1px solid #eee;
  display: flex;
  align-items: center;
}

.payers-list li p {
  flex-grow: 1;
  display: flex;
  justify-content: space-between;
  align-items: center;
  font-size: 0.9rem;
  padding: 0.5em;
}

.payers-list .payer-image-container {
  padding: 0.5em;
  border-right: 1px solid #eee;
}

.payers-list .payer-image-container img {
  width: 35px;
  border-radius: 50%;
}

.payers-list .pay-tag {
  border: 1px solid #ddd;
  border-radius: 8px;
  padding: 0.3em 0.5em;
}

.fa-circle-check {
  color: #22c55e;
}

.fa-clock {
  color: #d97706;
}

.btn-group {
  display: flex;
  align-items: center;
  gap: 0.75em;
  margin-top: 1.25em;
}

.status-progress {
  position: relative;
  margin: 1.5em 0 0.5em 0;
  height: 6px;
  background-image: linear-gradient(90deg, #000 75%, #eee 75%);
}

.checkpoint {
  position: absolute;
  background: rgba(255, 255, 255, 0.95);
  border: 2px solid red;
  width: 26px;
  height: 26px;
  border-radius: 50%;
  border: 0.5px solid #eee;
  display: flex;
  justify-content: center;
  align-items: center;
  box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
}

.checkpoint .circle {
  display: inline-block;
  width: 15px;
  height: 15px;
  border-radius: 50%;
  background-color: #000;
}

.checkpoint:nth-child(1) {
  left: 0;
  top: 0;
  transform: translate(-5%, -40%);
}

.checkpoint:nth-child(2) {
  left: 25%;
  top: 0;
  transform: translate(-25%, -40%);
}

.checkpoint:nth-child(3) {
  left: 50%;
  top: 0;
  transform: translate(-50%, -40%);
}

.checkpoint:nth-child(4) {
  left: 75%;
  top: 0;
  transform: translate(-75%, -40%);
}

.checkpoint:nth-child(5) {
  right: 0;
  top: 0;
  transform: translate(5%, -40%);
}

.fa-stamp {
  color: #000;
}

.btn {
  flex-grow: 1;
  border-radius: 100vmax;
  padding: 0.5em 0;
  font-size: 0.85rem;
  box-shadow: 0 5px 10px 0 rgba(0, 0, 0, 0.15);
  cursor: pointer;
}

.reminder-btn {
  color: #fff;
  background-color: #111827;
  border: 1px solid #1b1b1b;
}

.download-btn {
  border: 1px solid #eee;
  background-color: #fff;
}

/* ----------- */

hr {
  border: none;
  height: 1px;
  background-color: #ddd;
  margin: 0.75em 0;
}

/* payment info */

.payment-info {
  display: flex;
  justify-content: space-between;
  align-items: center;
  margin: 0.75em 1em;
  font-size: 1rem;
  color: #6b7280;
}

.card-info {
  display: flex;
  align-items: center;
  gap: 0.75em;
}

.card-icon {
  display: inline-block;
  width: 35px;
  height: 26px;
  background-color: #1a43bf;
  border-radius: 5px;
}

.pay-now-btn {
  font-size: 1.15rem;
  background-color: #111827;
  color: #fff;
  width: 100%;
  padding: 0.75em 0;
  border: 2px solid #1b1b1b;
  border-radius: 0.75em;
  box-shadow: 0 0 1px 0 #000, 0 5px 15px 0 rgba(0, 0, 0, 0.45);
  cursor: pointer;
}

@media (max-width: 424px) {
  h1 {
    font-size: 1.2rem;
  }

  .invoice {
    padding: 0.75em;
  }

  .invoice .title {
    position: relative;
    font-size: 1rem;
    padding: 0.5em 0;
  }

  .slot-hole {
    width: 95%;
  }
  .invoice {
    width: 90%;
  }

  .invoice .amount {
    font-size: 0.85rem;
  }

  .payers-list li p {
    font-size: 0.85rem;
  }

  .payers-list .pay-tag {
    font-size: 0.8rem;
  }

  .btn {
    padding: 0.5em 0;
    font-size: 0.8rem;
  }

  .payment-info {
    font-size: 0.95rem;
  }

  .pay-now-btn {
    font-size: 1.05rem;
  }
}
Interactive 3D hover cards where characters pop out of the background frame on hover using CSS perspective.
HTML·CSS

3D Pop-Out Character Card Hover

This component brings flat character illustrations to life by tilting the card background away while scaling up the foreground portrait on hover. Utilizing a grid layout inside semantic <figure> wrappers, it manipulates CSS 3D perspective, transform-origin, and transition curves to create a parallax pop-out illusion. The fold-out caption panel adds a physical card feel, achieving depth completely without JavaScript.

Level
Intermediate
Browsers
Chrome 88+ · Edge 88+ · Firefox 85+ · Safari 15+
Highlights
3D Pop-Out Effect · Tilt Transitions · Pure CSS
Interactive SVG toggle button that spawns a robotic hand to switch states, throwing a rock-on sign before sliding out.
SVG·HTML·CSS·JavaScript

Celebratory Robotic Toggle Switch Animation

This playful interactive toggle switch features an animated robotic arm that physically switches states upon click. Built with GSAP and responsive SVGs, the custom timeline orchestrates a multi-phase performance: the arm slides into view, pushes the indicator dot from off to on, morphs its fingers into a celebratory “rock-on” gesture, vibrates, and slides back out of the masked window.

Level
Advanced
Requires
gsap.js
Browsers
Chrome 55+ · Edge 79+ · Firefox 54+ · Safari 9.1+
Highlights
Robotic Hand Toggle · Celebratory Gestures · SVG Mask Reveal
Parallax film presentation page featuring full-screen slider images, rotating side text, and dynamic text scramble transitions upon navigating.
HTML/Pug·CSS/Stylus·JavaScript

Cinematic Parallax Movie Presentation Slideshow

This media-heavy movie promotion template uses custom GSAP transitions to create an expressive slideshow experience. Built with an object-oriented ES6 structure, the script synchronizes 3D parallax card tilting, vertical captions, and dynamic text-scrambling during slide changes. When a panel is selected, the grid expands, transitioning into a full-bleed details drawer with embedded content.

Level
Advanced
Requires
gsap.js · imagesloaded.js
Browsers
Chrome 65+ · Edge 79+ · Firefox 57+ · Safari 11.1+
Highlights
Text Scramble FX · GSAP 3D Parallax · Expanding Detail Panels
Time slider tracking a curved SVG arc where the thumb shifts colors dynamically along a sunset gradient track.
SVG·HTML·CSS/SCSS·JavaScript

Color Interpolated SVG Arc Time Slider

This interactive time selector maps a custom sliding thumb along an arced vector path using geometric coordinate tracking. By querying getPointAtLength() in real time, the script translates drag coordinates into a 1440-minute day scale, calculating multi-stop RGB color interpolation for the slider head. Tabular numerals in the tooltip prevent text-jitter, resulting in a smooth, high-fidelity clock widget.

Level
Intermediate
Browsers
Chrome 55+ · Edge 79+ · Firefox 52+ · Safari 11.1+
Highlights
SVG Path Tracking · RGB Color Interpolation · Pointer Event Binding
Digital pixel glitch transition effect applied to an image using pure CSS mask-composite and keyframe position offsets.
HTML·CSS

CSS Mask Glitch Image Effect

The CSS Mask Glitch Image Effect creates a high-performance glitch animation on an <img> element using pure CSS. By combining custom properties, an optimized set of -webkit-mask-position coordinates, and mask-composite: exclude (or xor), it slices the image into a dynamically shifting grid. The keyframe animation handles the rapid displacement of the mask slices without relying on external libraries or heavy JavaScript.

Level
Intermediate
Browsers
Chrome 120+ · Edge 120+ · Firefox 53+ · Safari 15.4+
Highlights
Glitch Transition · Mask Compositing · Pure CSS
Grid of images splitting into textured tiles that dynamically flip in 3D using CSS custom properties and math-based delay functions.
HTML·CSS/SCSS·JavaScript

Directional 3D Flip Grid Animation

This component dynamically fragments an image container into a grid of 3D-flipping tiles upon click. The JavaScript controller parses directional formulas to assign coordinated stagger delays via CSS custom properties. Each tile performs an alternate rotation using CSS 3D transforms and keyframe animations, offering fourteen distinct wave-like patterns without requiring canvas drawing or external WebGL libraries.

Level
Advanced
Browsers
Chrome 49+ · Edge 12+ · Firefox 49+ · Safari 10+
Highlights
3D Stagger Animations · Dynamic Image Slicing · Mathematical Delay Equations
Newspaper-style multi-column layout with dynamically generated CSS gradient pull quotes that fade in upon scroll.
HTML·CSS·JavaScript

Dynamic Animated CSS Pull Quotes

This editorial-style multi-column newspaper template dynamically generates floating quote blocks directly from document-contained <aside> nodes. Powered by an Intersection Observer, the script monitors scroll offsets to apply smooth reveal transitions and trigger custom shimmering gradients using animated background-clip: text values. Double linear gradients frame the quote corners to produce bracket borders cleanly without assets.

Level
Intermediate
Requires
font-awesome
Browsers
Chrome 51+ · Edge 15+ · Firefox 55+ · Safari 12.1+
Highlights
Dynamic Pull Quotes · Viewport Intersection · Animated Text Gradients
Retro dog treats landing page featuring grainy noise textures, glassmorphism testimonial cards, and a smooth mouse-tracking gradient aura.
HTML·CSS/SCSS·JavaScript/Babel

Grainy Gradient Interactive Landing Page

This interface features layered testimonial cards that float dynamically above an organic background. Using semantic span elements structured inside a grid container, the glassmorphism review cards apply a frosted blur to isolate customer feedback. Below, an active gradient glow tracks mouse movement to subtly highlight each blockquote, focusing user attention on positive social proof without cluttering the layout.

Level
Intermediate
Browsers
Chrome 76+ · Edge 79+ · Firefox 70+ · Safari 13+
Highlights
Mouse Tracking Aura · Noise Texture Overlay · Glassmorphism Cards
Interactive scrolling webpage displaying butterfly wings and diagram trees that animate upon scroll using GSAP.
HTML·CSS/Sass·JavaScript

Interactive Cicada Genomics Landing Page

This immersive landing page uses an intricate sequence of vector animations and responsive viewport styling to tell a visual story. Built on top of GSAP ScrollTrigger and MotionPathPlugin, it transitions users through interactive text slides and morphs a tree diagram into organic butterfly wings. Smooth motion path travels, audio ambiance, and SVG stroke-drawing create a highly tailored cinematic web experience.

Level
Advanced
Requires
gsap.js · observer.js · splittext.js · drawsvgplugin.js · motionpathplugin.js · scrolltrigger.js · customease.js
Browsers
Chrome 61+ · Edge 79+ · Firefox 52+ · Safari 11+
Highlights
Interactive SVG Tree · Flapping Wings Animation · Smooth Scroll Animations
Robotic hand emerging from a vector switch to reset a toggle and giving a thumbs up animated with GSAP.
SVG·HTML·CSS·JavaScript

Interactive Robotic Hand Toggle Switch

This playful vector toggle switch triggers an elaborate robotic arm animation to reset itself whenever the user clicks it. Choreographed entirely through GSAP timelines, the arm slides into view, physically pushes the knob back to its starting state, and delivers a quick thumbs-up gesture before retreating. The smooth transition between hand states relies on toggling group opacities and precise transform-origin pivots.

Level
Advanced
Requires
gsap.js
Browsers
Chrome 49+ · Edge 12+ · Firefox 49+ · Safari 10+
Highlights
Robotic Hand Toggle · Interactive SVG · Gesture Transitions
Parallax slide layout featuring expanding variable fonts, split background wipes, and offset floating image layers animated via GSAP.
HTML·CSS/SCSS·JavaScript

Interactive Variable Font GSAP Slideshow

This masterfully choreographed full-page slider captures wheel, swipe, and key events to drive a multi-layered transition. Utilizing GSAP Observer, the layout animates parent and child containers in opposite directions, producing a clean split-wipe reveal. Simultaneously, a custom variable font dynamically stretches its width axis (wdth) alongside parallax image transformations to create an organic, tactile sense of momentum.

Level
Advanced
Requires
gsap · gsap-observer
Browsers
Chrome 79+ · Edge 79+ · Firefox 68+ · Safari 13+
Highlights
GSAP Observer Scroll · Variable Font Stretching · Split Parallax Transitions
Liquid gooey particle letters dispersing under the mouse pointer and resolving dynamically in WebGL using PIXI.js.
HTML/Pug·CSS/SCSS·JavaScript/TypeScript

Interactive WebGL Liquid Particle Typography

This interactive backdrop turns text into a dynamic liquid particle cluster over a starry sky. Powered by PIXI.js and WebGL, the engine reads pixel matrices from an offscreen canvas to map a swarm of coordinate-tracking particles. A custom metaball shader merges them with a gooey threshold filter, while pointer-tracking physics disperse the letters elastically on mouse swipe.

Level
Advanced
Requires
pixi.js
Browsers
Chrome 79+ · Edge 79+ · Firefox 79+ · Safari 14.1+
Highlights
WebGL Liquid Metatext · High-Performance Particles · Responsive Canvas Math
Typography poster utilizing CSS mix-blend-mode lighten overlaid on a blurred HTML Canvas bokeh particle background.
SVG·HTML·CSS·JavaScript

Liquid Glow Blend Mode Poster

This design layout overlays sharp, classic typography onto a fluidly shifting background of colored orbs. Underneath the layout, an HTML5 Canvas draws drifting, colored circles that are diffused into a soft glow by a heavy CSS blur filter. By applying mix-blend-mode: lighten (or overlay), the letters dynamically blend with the shifting light underneath, producing a premium print-like digital editorial.

Level
Intermediate
Browsers
Chrome 49+ · Edge 79+ · Firefox 47+ · Safari 10+
Highlights
Canvas Bokeh Glow · CSS Mix Blend Mode · Responsive Typography
Alphabetical contact list sidebar indicator with active letters dynamically scaling using CSS scroll-driven animations and view-timeline.
HTML·CSS·JavaScript/Babel

Native CSS Scroll Snap Time Picker

This date and time wheel selector leverages the cutting-edge CSS Scroll Snap Events API to build an efficient mobile-style picker. By registering event listeners for scrollsnapchanging and scrollsnapchange, the layout dynamically updates the select state in real time as the wheels rotate. It uses native CSS scroll-start-target-block to set initial positions, offering a smooth, lightweight solution.

Level
Advanced
Browsers
Chrome 129+ · Edge 129+ · Safari 18+
Highlights
Scroll Snap Events · Scroll Start Target · Pure CSS Spinner
Minimalist vertical timeline on a muted beige backdrop, dynamically revealing articles and years with smooth horizontal slide-ins.
HTML·CSS·JavaScript

Observer-Animated Responsive Vertical Timeline

This chronological content layout uses an Intersection Observer to fade and slide events smoothly into view as the user scrolls. Utilizing Faker to populate mock data, the ES6 script generates semantic <time> and <small> nodes on load. On larger viewports, the grid transitions to a split layout with left-aligned dates, while a native @media query auto-toggles dark mode.

Level
Intermediate
Requires
faker.js
Browsers
Chrome 51+ · Edge 15+ · Firefox 55+ · Safari 12.1+
Highlights
Intersection Observer Reveal · Responsive Split Layout · Prefers Color Scheme Dark
Alphabetical contact list sidebar indicator with active letters dynamically scaling using CSS scroll-driven animations and view-timeline.
HTML·CSS·JavaScript/Babel

Scroll-Driven CSS Alphabet Indicator Sidebar

The “Scroll-Driven CSS Alphabet Indicator Sidebar” connects a scrollable contact list to a sidebar index using CSS Scroll-Driven Animations. It defines a timeline-scope on the parent and a view-timeline on each alphabetical section. Floating sidebar letters scale dynamically as sections cross the viewport, creating smooth, scroll-linked feedback without high-overhead JS scroll listeners.

Level
Advanced
Requires
react.js · react-dom.js · faker.js
Browsers
Chrome 115+ · Edge 115+ · Safari 26+
Highlights
Scroll Animations · View Timeline · Sticky Headers
Animated list with draggable items that dynamically swap positions with smooth CSS transform translations upon drag and drop.
HTML·CSS·JavaScript

Smooth Kinetic Drag and Drop List

This interactive UI panel achieves seamless list reordering by pairing raw pointer event listeners with smooth layout shifts. Rather than manipulating complex external layout engines, the script computes spatial intersections in real time, translating idle cards up or down with hardware-accelerated CSS transitions. Once dropped, the list items natively re-sort themselves within the DOM, preserving page flow.

Level
Intermediate
Browsers
Chrome 51+ · Edge 15+ · Firefox 48+ · Safari 10+
Highlights
Kinetic Reordering · Custom Drag Handle · Pure JS Physics
Pair of skeuomorphic range sliders with tactile plastic dials and smooth linear-gradient color fill tracks.
HTML·CSS/SCSS·JavaScript

Tactile CSS Variable Range Slider

This clean interactive range selector combines standard native inputs with a beautifully modeled plastic interface. The lightweight JavaScript companion reads standard input updates and translates them into a parent-scoped --slider-value CSS custom property. A combination of CSS math calculations and overlay blend-modes handles the fluid thumb positioning and track highlight filling completely inside stylesheets.

Level
Beginner
Browsers
Chrome 49+ · Edge 15+ · Firefox 49+ · Safari 10+
Highlights
CSS Variable Binding · Skeuomorphic Design · Fluid Track Blending
Grid of three textured teal cards featuring shifted 3D overlapping backdrop layers on a base64 speculary-lit SVG noise background.
SVG·HTML/Pug·CSS/Sass

Textured 3D Layered Grid Cards

This lightweight grid layout styles nested cards with an organic paper feel entirely through vector code. It utilizes a base64-encoded SVG filter featuring specular lighting and turbulence to inject a matte noise texture over solid colors. By applying an offset ::after pseudo-element rotated by 8deg with mix-blend-mode: overlay, each card gains a shifted tactile depth layer without heavy graphics.

Level
Beginner
Browsers
Chrome 105+ · Edge 105+ · Firefox 103+ · Safari 16+
Highlights
SVG Specular Noise · Layered Overlay Shifting · Conic Gradient BG
Interactive delete button that tilts open an SVG trash can, dynamically vacuums the letter spans inside, and fills the bin using pure CSS keyframes.
SVG·HTML·CSS·JavaScript

Trash Bin Letter Disposal Delete Button

This tactile delete button triggers a playful, paper-shredding trash disposal animation on click. Using vanilla JavaScript to toggle data attributes alongside meticulously timed CSS keyframes, the component skews an SVG garbage bin open while dynamically vacuuming separate text letter spans inside. It leverages a native animationend event listener to reset the visual cycle.

Level
Advanced
Browsers
Chrome 55+ · Edge 79+ · Firefox 54+ · Safari 9.1+
Highlights
SVG Disposal Keyframes · Sequential Letter Drops · Animation Reset Watcher
Surreal 3D isometric 404 page animation showing a cube rolling continuously along a track of sliding numbers.
HTML/Pug·CSS/SCSS

3D Rolling Cube 404 Page Animation

This surreal 404 page template features a continuous, 3D animated scene. Built entirely with HTML and CSS, the layout coordinates multiple keyframe animations to move a dimensional cube along an infinite sliding rail track. As the cube rolls 90 degrees with an elastic bounce, its pseudo-elements dynamically swap text strings between ‘4’ and ‘0’ to match the passing terrain.

Level
Advanced
Browsers
Chrome 36+ · Edge 12+ · Firefox 16+ · Safari 9+
Highlights
3D Rolling Cube · Continuous Rail Slide · Dynamic Text Swap
Interactive SVG hamburger buttons transitioning elastically into close icons, featuring dash-offset animations and accessible ARIA toggles.
SVG·HTML·CSS/SCSS·JavaScript

Animated SVG Hamburger Toggle Buttons

This typographic button suite features three highly responsive, accessible SVG hamburger menu icons. By pairing lightweight JavaScript attribute toggles with custom SCSS keyframes and variables, the buttons morph dynamically into close ‘X’ symbols. Techniques include staggered CSS execution delays, vector shrinkage via stroke-dashoffset loops, and continuous path bending.

Level
Intermediate
Browsers
Chrome 49+ · Edge 12+ · Firefox 49+ · Safari 9.1+
Highlights
SVG Line Morphing · Staged CSS Transitions · ARIA Toggles
Interactive React-powered tag list container displaying multiple rows of text badges sliding infinitely in opposite directions with soft side fades.
HTML·CSS/SCSS·JavaScript/Babel

Bi-Directional Infinite Scrolling Tag Cloud

This typographic dashboard component showcases multiple rows of infinitely scrolling keyword tags. By combining React with custom CSS keyframes and variables, the script randomizes the scroll speed and flips the direction of alternating rows. A linear gradient mask overlay wraps the container boundaries to create a smooth, feathered blending fade.

Level
Intermediate
Requires
react.js · react-dom.js
Browsers
Chrome 49+ · Edge 12+ · Firefox 49+ · Safari 9.1+
Highlights
Bi-Directional Scroll · CSS Gradient Fade · Procedural Randomizer
Full-screen WebGL canvas rendering a glowing sine wave with dynamic RGB chromatic aberration and distortion effects.
HTML·CSS·JavaScript

Chromatic Aberration WebGL Sine Wave

A mesmerizing WebGL background effect rendering a glowing, undulating sine wave. Built with Three.js and custom GLSL shaders, the script calculates pixel coordinates to generate a continuous mathematical wave. The fragment shader applies a radial distortion multiplier to separate the red, green, and blue color channels, resulting in a dynamic chromatic aberration effect that shifts over time.

Level
Advanced
Requires
three.js
Browsers
Chrome 9+ · Edge 12+ · Firefox 4+ · Safari 5.1+
Highlights
GLSL Shaders · Chromatic Aberration · Math Animation
Abstract geometric grid composed of colorful CSS shapes that dynamically morph their positions, sizes, and border-radii using data-attribute transitions.
HTML·CSS·JavaScript

Dynamic CSS Shape Morphing Grid

A mesmerizing geometric animation driven entirely by CSS transitions and HTML data-* attributes. A lightweight JavaScript interval randomly cycles through predefined configuration states, updating the parent wrapper’s data attributes. The CSS responds by smoothly interpolating the left, top, width, height, and border-radius properties of seven colored child elements, creating fluid, abstract shape-shifting compositions.

Level
Beginner
Browsers
Chrome 36+ · Edge 12+ · Firefox 16+ · Safari 9+
Highlights
Data-Attribute State · CSS Transitions · Geometric Morphing