25 CSS Button Groups22 / 25

Pure CSSMIT licensed

CSS Filter Button Group with RGB-Split Glitch Active State (Cyberpunk Toggle)

A filter button group with a cyberpunk twist: selecting a filter fires a brief RGB-split glitch — the label tears into cyan and magenta ghosts, jitters through clip-path slices, then locks into a neon active state. The head-turning 'CSS RGB-split glitch active state button group' for gaming, music and creative-tool UIs.

Published Updated

Live Demo
Try it

The code

<section class="btn-22" aria-label="RGB-split glitch filter group demo">
  <div class="btn-22__wrap">
    <p class="btn-22__label">Audio filter</p>
    <div class="btn-22__group" role="radiogroup" aria-label="Audio filter">
      <input type="radio" name="btn-22" id="btn-22-1" class="btn-22__in" checked>
      <label for="btn-22-1" class="btn-22__btn" data-txt="CLEAN">CLEAN</label>
      <input type="radio" name="btn-22" id="btn-22-2" class="btn-22__in">
      <label for="btn-22-2" class="btn-22__btn" data-txt="BASS">BASS</label>
      <input type="radio" name="btn-22" id="btn-22-3" class="btn-22__in">
      <label for="btn-22-3" class="btn-22__btn" data-txt="VOCAL">VOCAL</label>
      <input type="radio" name="btn-22" id="btn-22-4" class="btn-22__in">
      <label for="btn-22-4" class="btn-22__btn" data-txt="LO-FI">LO-FI</label>
    </div>
  </div>
</section>
.btn-22,
.btn-22 *,
.btn-22 *::before,
.btn-22 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-22 {
  --accent: oklch(0.78 0.2 155);
  font-family: ui-monospace,'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #050506;
  padding: 44px 20px;
}

.btn-22__wrap {
  width: min(520px,100%);
}

.btn-22__label {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: #5a5a62;
  margin: 0 0 16px 2px;
}

.btn-22__group {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  gap: 10px;
}

.btn-22__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-22__btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: .1em;
  color: #7d7d86;
  background: #101014;
  border: 1.5px solid #22222a;
  border-radius: 10px;
  cursor: pointer;
  transition: color .2s,border-color .2s,background .2s;
}

.btn-22__btn::before,
.btn-22__btn::after {
  content: attr(data-txt);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
}

.btn-22__btn::before {
  color: #00f6ff;
}

.btn-22__btn::after {
  color: #ff2bd0;
}

.btn-22__btn:hover {
  color: #c7c7d0;
  border-color: #33333f;
}

.btn-22__in:checked + .btn-22__btn {
  color: var(--accent);
  border-color: var(--accent);
  background: #0c1410;
  text-shadow: 0 0 10px color-mix(in oklab,var(--accent) 70%,transparent);
  box-shadow: 0 0 20px -8px var(--accent),inset 0 0 12px -8px var(--accent);
  animation: btn-22-glitch .45s steps(2,end);
}

.btn-22__in:checked + .btn-22__btn::before {
  animation: btn-22-c .45s steps(2,end);
}

.btn-22__in:checked + .btn-22__btn::after {
  animation: btn-22-m .45s steps(2,end);
}

@keyframes btn-22-glitch {
  0% {
    clip-path: inset(0 0 0 0);
  }

  20% {
    clip-path: inset(20% 0 55% 0);
  }

  40% {
    clip-path: inset(65% 0 12% 0);
  }

  60% {
    clip-path: inset(35% 0 40% 0);
  }

  80% {
    clip-path: inset(10% 0 75% 0);
  }

  100% {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes btn-22-c {
  0%,
    100% {
    opacity: 0;
    translate: 0 0;
  }

  15% {
    opacity: .9;
    translate: -4px -2px;
  }

  45% {
    opacity: .9;
    translate: 3px 1px;
  }

  70% {
    opacity: .6;
    translate: -2px 2px;
  }
}

@keyframes btn-22-m {
  0%,
    100% {
    opacity: 0;
    translate: 0 0;
  }

  15% {
    opacity: .9;
    translate: 4px 2px;
  }

  45% {
    opacity: .9;
    translate: -3px -1px;
  }

  70% {
    opacity: .6;
    translate: 2px -2px;
  }
}

.btn-22__in:focus-visible + .btn-22__btn {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-22__in:checked + .btn-22__btn,
    .btn-22__in:checked + .btn-22__btn::before,
    .btn-22__in:checked + .btn-22__btn::after {
    animation: none;
  }

  .btn-22__btn::before,
    .btn-22__btn::after {
    display: none;
  }
}
Paste this into ChatGPT, Claude, Cursor, or any coding assistant. The block below is pre-framed with everything the AI needs to integrate this demo into your project — markup, styles, scoping notes, and the source URL. Hit Copy and paste straight into your chat.
Here's a working CSS Button Group from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: CSS Filter Button Group with RGB-Split Glitch Active State (Cyberpunk Toggle)
Source: https://codefronts.com/components/css-button-groups/glitch-grid/

A filter button group with a cyberpunk twist: selecting a filter fires a brief RGB-split glitch — the label tears into cyan and magenta ghosts, jitters through clip-path slices, then locks into a neon active state. The head-turning 'CSS RGB-split glitch active state button group' for gaming, music and creative-tool UIs.
## HTML
```html
<section class="btn-22" aria-label="RGB-split glitch filter group demo">
  <div class="btn-22__wrap">
    <p class="btn-22__label">Audio filter</p>
    <div class="btn-22__group" role="radiogroup" aria-label="Audio filter">
      <input type="radio" name="btn-22" id="btn-22-1" class="btn-22__in" checked>
      <label for="btn-22-1" class="btn-22__btn" data-txt="CLEAN">CLEAN</label>
      <input type="radio" name="btn-22" id="btn-22-2" class="btn-22__in">
      <label for="btn-22-2" class="btn-22__btn" data-txt="BASS">BASS</label>
      <input type="radio" name="btn-22" id="btn-22-3" class="btn-22__in">
      <label for="btn-22-3" class="btn-22__btn" data-txt="VOCAL">VOCAL</label>
      <input type="radio" name="btn-22" id="btn-22-4" class="btn-22__in">
      <label for="btn-22-4" class="btn-22__btn" data-txt="LO-FI">LO-FI</label>
    </div>
  </div>
</section>
```
## CSS
```css
.btn-22,
.btn-22 *,
.btn-22 *::before,
.btn-22 *::after {
  box-sizing: border-box;
  margin: 0;
  padding: 0;
}

.btn-22 {
  --accent: oklch(0.78 0.2 155);
  font-family: ui-monospace,'Segoe UI',system-ui,sans-serif;
  width: 100%;
  min-height: 100vh;
  display: flex;
  align-items: center;
  justify-content: center;
  background: #050506;
  padding: 44px 20px;
}

.btn-22__wrap {
  width: min(520px,100%);
}

.btn-22__label {
  font-size: 11px;
  font-weight: 800;
  letter-spacing: .26em;
  text-transform: uppercase;
  color: #5a5a62;
  margin: 0 0 16px 2px;
}

.btn-22__group {
  display: grid;
  grid-template-columns: repeat(4,1fr);
  gap: 10px;
}

.btn-22__in {
  position: absolute;
  width: 1px;
  height: 1px;
  overflow: hidden;
  clip-path: inset(50%);
}

.btn-22__btn {
  position: relative;
  display: flex;
  align-items: center;
  justify-content: center;
  min-height: 52px;
  font-size: 14px;
  font-weight: 800;
  letter-spacing: .1em;
  color: #7d7d86;
  background: #101014;
  border: 1.5px solid #22222a;
  border-radius: 10px;
  cursor: pointer;
  transition: color .2s,border-color .2s,background .2s;
}

.btn-22__btn::before,
.btn-22__btn::after {
  content: attr(data-txt);
  position: absolute;
  inset: 0;
  display: flex;
  align-items: center;
  justify-content: center;
  opacity: 0;
  pointer-events: none;
}

.btn-22__btn::before {
  color: #00f6ff;
}

.btn-22__btn::after {
  color: #ff2bd0;
}

.btn-22__btn:hover {
  color: #c7c7d0;
  border-color: #33333f;
}

.btn-22__in:checked + .btn-22__btn {
  color: var(--accent);
  border-color: var(--accent);
  background: #0c1410;
  text-shadow: 0 0 10px color-mix(in oklab,var(--accent) 70%,transparent);
  box-shadow: 0 0 20px -8px var(--accent),inset 0 0 12px -8px var(--accent);
  animation: btn-22-glitch .45s steps(2,end);
}

.btn-22__in:checked + .btn-22__btn::before {
  animation: btn-22-c .45s steps(2,end);
}

.btn-22__in:checked + .btn-22__btn::after {
  animation: btn-22-m .45s steps(2,end);
}

@keyframes btn-22-glitch {
  0% {
    clip-path: inset(0 0 0 0);
  }

  20% {
    clip-path: inset(20% 0 55% 0);
  }

  40% {
    clip-path: inset(65% 0 12% 0);
  }

  60% {
    clip-path: inset(35% 0 40% 0);
  }

  80% {
    clip-path: inset(10% 0 75% 0);
  }

  100% {
    clip-path: inset(0 0 0 0);
  }
}

@keyframes btn-22-c {
  0%,
    100% {
    opacity: 0;
    translate: 0 0;
  }

  15% {
    opacity: .9;
    translate: -4px -2px;
  }

  45% {
    opacity: .9;
    translate: 3px 1px;
  }

  70% {
    opacity: .6;
    translate: -2px 2px;
  }
}

@keyframes btn-22-m {
  0%,
    100% {
    opacity: 0;
    translate: 0 0;
  }

  15% {
    opacity: .9;
    translate: 4px 2px;
  }

  45% {
    opacity: .9;
    translate: -3px -1px;
  }

  70% {
    opacity: .6;
    translate: 2px -2px;
  }
}

.btn-22__in:focus-visible + .btn-22__btn {
  outline: 3px solid var(--accent);
  outline-offset: 2px;
}

@media (prefers-reduced-motion: reduce) {
  .btn-22__in:checked + .btn-22__btn,
    .btn-22__in:checked + .btn-22__btn::before,
    .btn-22__in:checked + .btn-22__btn::after {
    animation: none;
  }

  .btn-22__btn::before,
    .btn-22__btn::after {
    display: none;
  }
}
```

How this works

Real radios drive single-select filtering. On :checked, the label runs a short one-shot glitch animation: two pseudo-elements clone the text via content:attr(data-txt), offset in opposite directions and tinted cyan / magenta (the RGB split), while a clip-path keyframe slices the label into shifting horizontal bands for the tear.

The glitch is deliberately brief (~450ms) so it punctuates the selection rather than annoying. After it resolves, the active button holds a clean neon state — border glow and bright text. It's all transforms, clip-path and pseudo-content, so it composites cheaply and never touches layout.

.label::before{ content:attr(data-txt); color:#0ff; /* cyan ghost */ }
.in:checked + .label{ animation:btn-22-glitch .45s steps(2,end) }

Techniques used: attr(data-txt) text clones for RGB split · clip-path band-slice glitch keyframes · cyan/magenta channel offset · one-shot punctuating animation · neon hold state · reduced-motion skips straight to active

Make it yours

  • Split channels = the ::before/::after colors (cyan/magenta here).
  • Glitch length/intensity = the btn-22-glitch duration + offsets.
  • --accent sets the resolved neon hold color.
  • Increase band count in the clip-path keyframe for a harsher tear.

Gotchas — read before shipping

  • Glitch effects can trigger motion sensitivity — keep them brief and fully removed under prefers-reduced-motion (jump straight to active).
  • Mirror the visible label into data-txt so the ghosts match; keep it in sync if labels are dynamic.
  • Don't glitch on every hover — reserve it for the selection moment so it stays special.
  • Ensure the resolved state (not the glitch) carries the accessible contrast.

Browser support

ChromeSafariFirefoxEdge
111+16.2+113+111+

Floor set by oklch(), color-mix() as this demo is written. The core technique itself goes back further — Chrome all.

attr(), clip-path and CSS animation are Baseline. The effect is decorative; the radio selection is fully functional everywhere.

Techniques used in this demo

Search CodeFronts

Loading…