Glitch
import { useState } from 'react';
const css = `
@keyframes toggle-glitch-go {
0% { transform: translateX(0); }
18% { transform: translateX(11px); }
30% { transform: translateX(5px); }
46% { transform: translateX(24px); }
58% { transform: translateX(19px); }
74% { transform: translateX(31px); }
86% { transform: translateX(36px); }
100% { transform: translateX(34px); }
}
@keyframes toggle-glitch-back {
0% { transform: translateX(34px); }
18% { transform: translateX(23px); }
30% { transform: translateX(29px); }
46% { transform: translateX(10px); }
58% { transform: translateX(15px); }
74% { transform: translateX(3px); }
86% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
@keyframes toggle-glitch-ghost-a { 0%, 85% { opacity: 0.8; transform: translateX(-5px); } 40% { transform: translateX(4px); } 100% { opacity: 0; transform: none; } }
@keyframes toggle-glitch-ghost-b { 0%, 85% { opacity: 0.8; transform: translateX(-5px); } 40% { transform: translateX(4px); } 100% { opacity: 0; transform: none; } }
@keyframes toggle-glitch-slice-a {
0%, 100% { clip-path: inset(0); transform: none; }
20% { clip-path: inset(0 0 60% 0); transform: translateX(3px); }
40% { clip-path: inset(40% 0 0 0); transform: translateX(-3px); }
60% { clip-path: inset(20% 0 30% 0); transform: translateX(2px); }
}
@keyframes toggle-glitch-slice-b {
0%, 100% { clip-path: inset(0); transform: none; }
20% { clip-path: inset(50% 0 0 0); transform: translateX(-3px); }
40% { clip-path: inset(0 0 45% 0); transform: translateX(3px); }
60% { clip-path: inset(30% 0 20% 0); transform: translateX(-2px); }
}
.toggle-glitch { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-glitch__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-glitch__track { position: relative; width: 64px; height: 30px; border-radius: 999px; }
.toggle-glitch__input:focus-visible + .toggle-glitch__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-glitch__bg {
position: absolute;
inset: 0;
border-radius: 999px;
background: repeating-linear-gradient(0deg, transparent 0 2px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 35%, transparent) 2px 3px), light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
transition: background-color 0.2s;
}
.toggle-glitch__thumb { position: absolute; left: 4px; top: 4px; width: 22px; height: 22px; }
.toggle-glitch__thumb i, .toggle-glitch__thumb::before, .toggle-glitch__thumb::after { position: absolute; inset: 0; border-radius: 50%; }
.toggle-glitch__thumb i { background: light-dark(#42454c, #e5e6e9); transition: background-color 0.2s; }
.toggle-glitch__thumb::before, .toggle-glitch__thumb::after { content: ''; opacity: 0; }
.toggle-glitch__thumb::before { background: light-dark(#5f626a, #a7abb2); }
.toggle-glitch__thumb::after { background: light-dark(#cbced3, #393c41); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__bg { background-color: light-dark(#cbced3, #393c41); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb { transform: translateX(34px); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb i { background: light-dark(#373a40, #f1f2f3); }
/* The thumb teleports in jumps, trailed by offset ghosts, while the track tears into slices. */
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb { animation: toggle-glitch-go 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb { animation: toggle-glitch-back 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb::before { animation: toggle-glitch-ghost-a 0.42s steps(2); }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb::after { animation: toggle-glitch-ghost-a 0.42s steps(3) reverse; }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb::before { animation: toggle-glitch-ghost-b 0.42s steps(2); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb::after { animation: toggle-glitch-ghost-b 0.42s steps(3) reverse; }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__bg { animation: toggle-glitch-slice-a 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__bg { animation: toggle-glitch-slice-b 0.42s steps(1); }
@media (prefers-reduced-motion: reduce) {
.toggle-glitch__track *, .toggle-glitch__track *::before, .toggle-glitch__track *::after { animation: none !important; }
}
`;
export function GlitchToggle() {
const [armed, setArmed] = useState(false);
return (
<>
<style>
{css}
</style>
<label className="toggle-glitch" data-armed={armed || undefined}>
<input className="toggle-glitch__input" type="checkbox" role="switch" onChange={() => setArmed(true)} />
<span className="toggle-glitch__track" aria-hidden="true">
<span className="toggle-glitch__bg" />
<span className="toggle-glitch__thumb">
<i />
</span>
</span>
Glitch effects
</label>
</>
);
}Markup where the component goes, CSS in your stylesheet, script before </body>. In Vue, Svelte or Angular, call initToggleGlitch(el) on the mounted element instead.
<label class="toggle-glitch"><input class="toggle-glitch__input" type="checkbox" role="switch"/><span class="toggle-glitch__track" aria-hidden="true"><span class="toggle-glitch__bg"></span><span class="toggle-glitch__thumb"><i></i></span></span>Glitch effects</label>@keyframes toggle-glitch-go {
0% { transform: translateX(0); }
18% { transform: translateX(11px); }
30% { transform: translateX(5px); }
46% { transform: translateX(24px); }
58% { transform: translateX(19px); }
74% { transform: translateX(31px); }
86% { transform: translateX(36px); }
100% { transform: translateX(34px); }
}
@keyframes toggle-glitch-back {
0% { transform: translateX(34px); }
18% { transform: translateX(23px); }
30% { transform: translateX(29px); }
46% { transform: translateX(10px); }
58% { transform: translateX(15px); }
74% { transform: translateX(3px); }
86% { transform: translateX(-2px); }
100% { transform: translateX(0); }
}
@keyframes toggle-glitch-ghost-a { 0%, 85% { opacity: 0.8; transform: translateX(-5px); } 40% { transform: translateX(4px); } 100% { opacity: 0; transform: none; } }
@keyframes toggle-glitch-ghost-b { 0%, 85% { opacity: 0.8; transform: translateX(-5px); } 40% { transform: translateX(4px); } 100% { opacity: 0; transform: none; } }
@keyframes toggle-glitch-slice-a {
0%, 100% { clip-path: inset(0); transform: none; }
20% { clip-path: inset(0 0 60% 0); transform: translateX(3px); }
40% { clip-path: inset(40% 0 0 0); transform: translateX(-3px); }
60% { clip-path: inset(20% 0 30% 0); transform: translateX(2px); }
}
@keyframes toggle-glitch-slice-b {
0%, 100% { clip-path: inset(0); transform: none; }
20% { clip-path: inset(50% 0 0 0); transform: translateX(-3px); }
40% { clip-path: inset(0 0 45% 0); transform: translateX(3px); }
60% { clip-path: inset(30% 0 20% 0); transform: translateX(-2px); }
}
.toggle-glitch { position: relative; display: inline-flex; align-items: center; gap: 12px; color: light-dark(#42454c, #e5e6e9); font-size: 14px; cursor: pointer; user-select: none; }
.toggle-glitch__input { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
.toggle-glitch__track { position: relative; width: 64px; height: 30px; border-radius: 999px; }
.toggle-glitch__input:focus-visible + .toggle-glitch__track { outline: 2px solid light-dark(#5f626a, #a7abb2); outline-offset: 3px; }
.toggle-glitch__bg {
position: absolute;
inset: 0;
border-radius: 999px;
background: repeating-linear-gradient(0deg, transparent 0 2px, color-mix(in oklab, light-dark(#fbfcfd, #121417) 35%, transparent) 2px 3px), light-dark(#ebedf0, #232529);
box-shadow: inset 0 0 0 1px light-dark(#dbdde1, #2e3035);
transition: background-color 0.2s;
}
.toggle-glitch__thumb { position: absolute; left: 4px; top: 4px; width: 22px; height: 22px; }
.toggle-glitch__thumb i, .toggle-glitch__thumb::before, .toggle-glitch__thumb::after { position: absolute; inset: 0; border-radius: 50%; }
.toggle-glitch__thumb i { background: light-dark(#42454c, #e5e6e9); transition: background-color 0.2s; }
.toggle-glitch__thumb::before, .toggle-glitch__thumb::after { content: ''; opacity: 0; }
.toggle-glitch__thumb::before { background: light-dark(#5f626a, #a7abb2); }
.toggle-glitch__thumb::after { background: light-dark(#cbced3, #393c41); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__bg { background-color: light-dark(#cbced3, #393c41); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb { transform: translateX(34px); }
.toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb i { background: light-dark(#373a40, #f1f2f3); }
/* The thumb teleports in jumps, trailed by offset ghosts, while the track tears into slices. */
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb { animation: toggle-glitch-go 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb { animation: toggle-glitch-back 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb::before { animation: toggle-glitch-ghost-a 0.42s steps(2); }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__thumb::after { animation: toggle-glitch-ghost-a 0.42s steps(3) reverse; }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb::before { animation: toggle-glitch-ghost-b 0.42s steps(2); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__thumb::after { animation: toggle-glitch-ghost-b 0.42s steps(3) reverse; }
.toggle-glitch[data-armed] .toggle-glitch__input:checked + .toggle-glitch__track .toggle-glitch__bg { animation: toggle-glitch-slice-a 0.42s steps(1); }
.toggle-glitch[data-armed] .toggle-glitch__input:not(:checked) + .toggle-glitch__track .toggle-glitch__bg { animation: toggle-glitch-slice-b 0.42s steps(1); }
@media (prefers-reduced-motion: reduce) {
.toggle-glitch__track *, .toggle-glitch__track *::before, .toggle-glitch__track *::after { animation: none !important; }
}function initToggleGlitch(root) {
var input = root.querySelector('input');
if (!input) return;
// Animate only after the first change, so nothing plays on page load.
function arm() { root.setAttribute('data-armed', ''); }
input.addEventListener('change', arm);
return function () { input.removeEventListener('change', arm); };
}
document.querySelectorAll('.toggle-glitch').forEach(initToggleGlitch);Goal: Create a "Glitch" toggle.
Visual behavior: A glitch switch: the thumb teleports across in jerky jumps, trailed by offset ghost copies, while the scanlined track tears into displaced slices.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The thumb moves on a keyframe with steps(1) timing, so it jumps between uneven positions instead of sliding.
- Two ghost layers (::before, ::after) flash at offsets; the track shows torn slices with clip-path: inset() bands and small shifts.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; checked and unchecked use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion it changes without glitching.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: neutral only. Black, white and grays, no hues. Use the color values above as written, or my project's closest neutral tokens.
Return a single self-contained component. No dependencies.Goal: Create a "Glitch" toggle.
Visual behavior: A glitch switch: the thumb teleports across in jerky jumps, trailed by offset ghost copies, while the scanlined track tears into displaced slices.
Technique:
- A real native checkbox with role="switch" (visually hidden: absolute, 1x1, opacity 0) inside a <label>; every visual is aria-hidden and styled from the input with :checked + sibling selectors.
- The thumb moves on a keyframe with steps(1) timing, so it jumps between uneven positions instead of sliding.
- Two ghost layers (::before, ::after) flash at offsets; the track shows torn slices with clip-path: inset() bands and small shifts.
- The keyframes only apply once a data-armed flag is set on the first change, so nothing plays on page load; checked and unchecked use differently named keyframes, so each change restarts the right one.
Accessibility: A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion it changes without glitching.
My stack: {{USER_STACK}}
My styling: {{USER_STYLING}}
My constraints: {{USER_CONSTRAINTS}}
My colors: use my project's existing color tokens. The color values above are grayscale placeholders. Map each one by its role (surface, text, border, accent, glow) to my theme, and keep their relative lightness so contrast and the effect still read.
Return a single self-contained component. No dependencies.Paste into Claude, ChatGPT or Cursor after filling in {{USER_STACK}}, {{USER_STYLING}} and {{USER_CONSTRAINTS}}. Neutral keeps the greys you see here; Match my project asks the model to map them onto your own colour tokens.
Free to use in any project, commercial included. MIT-0, no attribution required. Licence
About this component
A glitch switch: the thumb teleports across in jerky jumps, trailed by offset ghost copies, while the scanlined track tears into displaced slices.
- Dependencies
- None
- Difficulty
- Intermediate
- Triggers
- click, state change
- Techniques
- keyframes, clip path, pseudo elements, state machine
- Reduced motion
- Respected
- Theme
- Light and dark (follows your page's color-scheme)
A native checkbox with role="switch" inside a <label>, so Space toggles it and screen readers announce a switch; the visible label names it and :focus-visible on the input outlines the visual. Under prefers-reduced-motion it changes without glitching.