51 CSS Buttons16 / 51

Pure CSSMIT licensed

CSS Brutalist Glitch Button

Neo-brutalist slab with a hard offset shadow that snaps flat when pressed, and a hover glitch built from two duplicate labels sliced by clip-path and pushed apart in cyan and magenta. Steps-based timing keeps it digital — no easing, no blur, no JavaScript.

Published Updated

Live Demo
Try it

The code

<div class="cb-16">
  <div class="cb-16__stage">
    <button class="cb-16__btn" type="button" data-label="Enter the archive">Enter the archive</button>
  </div>
</div>
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');

.cb-16 {
  --bg: #e8e6df;
  --slab: #f7f5ef;
  --ink: #0a0a0a;
  --accent: #facc15;
  --ghost-1: #22d3ee;
  --ghost-2: #f43f5e;
  --offset: 6px;
  --mono: "Space Mono",ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: repeating-linear-gradient(90deg, color-mix(in oklab,var(--ink) 5%,transparent) 0 1px, transparent 1px 3rem), repeating-linear-gradient(0deg, color-mix(in oklab,var(--ink) 5%,transparent) 0 1px, transparent 1px 3rem), var(--bg);
  font-family: var(--mono);
}

@supports (color: oklch(50% 0 0)) {
  .cb-16 {
    --bg: oklch(92% .008 95);
    --accent: oklch(87% .17 92);
    --ghost-1: oklch(82% .13 200);
    --ghost-2: oklch(66% .21 12);
  }
}

@media (prefers-color-scheme: dark) {
  .cb-16:not([data-theme="light"]) {
    --bg: #101010;
    --slab: #1b1b1b;
    --ink: #f5f5f0;
  }
}

.cb-16[data-theme="dark"] {
  --bg: #101010;
  --slab: #1b1b1b;
  --ink: #f5f5f0;
}

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

.cb-16__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-16__btn {
  position: relative;
  cursor: pointer;
  display: grid;
  place-items: center;
  min-height: 3.5rem;
  padding: 0 2rem;
  border: 2px solid var(--ink);
  border-radius: 0;
  font: inherit;
  font-size: .9375rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--ink);
  background: var(--slab);
  box-shadow: var(--offset) var(--offset) 0 0 var(--ink);
  transition: translate .12s steps(2,end), box-shadow .12s steps(2,end), background-color .15s steps(2,end);
}

.cb-16__btn::before,
.cb-16__btn::after {
  content: attr(data-label);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0 2rem;
  pointer-events: none;
  opacity: 0;
  white-space: nowrap;
}

.cb-16__btn::before {
  color: var(--ghost-1);
  clip-path: inset(0 0 55% 0);
}

.cb-16__btn::after {
  color: var(--ghost-2);
  clip-path: inset(55% 0 0 0);
}

.cb-16__btn:is(:hover,:focus-visible) {
  background: var(--accent);
}

.cb-16__btn:is(:hover,:focus-visible)::before {
  animation: cb-16-a .48s steps(2,end) infinite;
}

.cb-16__btn:is(:hover,:focus-visible)::after {
  animation: cb-16-b .56s steps(2,end) infinite;
}

@keyframes cb-16-a {
  0%,
    100% {
    translate: 0 0;
    opacity: 0;
  }

  20% {
    translate: -3px -2px;
    opacity: 1;
  }

  45% {
    translate: 4px 1px;
    opacity: 1;
    clip-path: inset(0 0 68% 0);
  }

  70% {
    translate: -2px 2px;
    opacity: 1;
    clip-path: inset(0 0 42% 0);
  }
}

@keyframes cb-16-b {
  0%,
    100% {
    translate: 0 0;
    opacity: 0;
  }

  25% {
    translate: 3px 2px;
    opacity: 1;
  }

  50% {
    translate: -4px -1px;
    opacity: 1;
    clip-path: inset(48% 0 0 0);
  }

  75% {
    translate: 2px -2px;
    opacity: 1;
    clip-path: inset(62% 0 0 0);
  }
}

.cb-16__btn:focus-visible {
  outline: 3px dashed var(--ink);
  outline-offset: 5px;
}

.cb-16__btn:active {
  translate: var(--offset) var(--offset);
  box-shadow: 0 0 0 0 var(--ink);
}

.cb-16__btn:disabled {
  cursor: not-allowed;
  color: color-mix(in oklab,var(--ink) 40%,var(--slab));
  box-shadow: var(--offset) var(--offset) 0 0 color-mix(in oklab,var(--ink) 35%,var(--slab));
}

@media (prefers-reduced-motion: reduce) {
  .cb-16__btn {
    transition: none;
  }

  .cb-16__btn::before,
    .cb-16__btn::after {
    animation: none !important;
    display: none;
  }
}

@media (forced-colors: active) {
  .cb-16__btn {
    background: ButtonFace;
    color: ButtonText;
    border-color: ButtonText;
    box-shadow: var(--offset) var(--offset) 0 0 ButtonText;
  }

  .cb-16__btn::before,
    .cb-16__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 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 Brutalist Glitch Button
Source: https://codefronts.com/components/css-buttons/css-brutalist-glitch-button/

Neo-brutalist slab with a hard offset shadow that snaps flat when pressed, and a hover glitch built from two duplicate labels sliced by clip-path and pushed apart in cyan and magenta. Steps-based timing keeps it digital — no easing, no blur, no JavaScript.
## HTML
```html
<div class="cb-16">
  <div class="cb-16__stage">
    <button class="cb-16__btn" type="button" data-label="Enter the archive">Enter the archive</button>
  </div>
</div>
```
## CSS
```css
@import url('https://fonts.googleapis.com/css2?family=Space+Mono:wght@400;700&display=swap');

.cb-16 {
  --bg: #e8e6df;
  --slab: #f7f5ef;
  --ink: #0a0a0a;
  --accent: #facc15;
  --ghost-1: #22d3ee;
  --ghost-2: #f43f5e;
  --offset: 6px;
  --mono: "Space Mono",ui-monospace,"SFMono-Regular",Menlo,Consolas,monospace;
  width: 100%;
  min-height: 100vh;
  min-height: 100svh;
  display: block;
  background: repeating-linear-gradient(90deg, color-mix(in oklab,var(--ink) 5%,transparent) 0 1px, transparent 1px 3rem), repeating-linear-gradient(0deg, color-mix(in oklab,var(--ink) 5%,transparent) 0 1px, transparent 1px 3rem), var(--bg);
  font-family: var(--mono);
}

@supports (color: oklch(50% 0 0)) {
  .cb-16 {
    --bg: oklch(92% .008 95);
    --accent: oklch(87% .17 92);
    --ghost-1: oklch(82% .13 200);
    --ghost-2: oklch(66% .21 12);
  }
}

@media (prefers-color-scheme: dark) {
  .cb-16:not([data-theme="light"]) {
    --bg: #101010;
    --slab: #1b1b1b;
    --ink: #f5f5f0;
  }
}

.cb-16[data-theme="dark"] {
  --bg: #101010;
  --slab: #1b1b1b;
  --ink: #f5f5f0;
}

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

.cb-16__stage {
  min-height: 100vh;
  min-height: 100svh;
  display: grid;
  place-items: center;
  padding: clamp(1.5rem,6vw,4rem);
}

.cb-16__btn {
  position: relative;
  cursor: pointer;
  display: grid;
  place-items: center;
  min-height: 3.5rem;
  padding: 0 2rem;
  border: 2px solid var(--ink);
  border-radius: 0;
  font: inherit;
  font-size: .9375rem;
  font-weight: 700;
  text-transform: uppercase;
  letter-spacing: .1em;
  color: var(--ink);
  background: var(--slab);
  box-shadow: var(--offset) var(--offset) 0 0 var(--ink);
  transition: translate .12s steps(2,end), box-shadow .12s steps(2,end), background-color .15s steps(2,end);
}

.cb-16__btn::before,
.cb-16__btn::after {
  content: attr(data-label);
  position: absolute;
  inset: 0;
  display: grid;
  place-items: center;
  padding: 0 2rem;
  pointer-events: none;
  opacity: 0;
  white-space: nowrap;
}

.cb-16__btn::before {
  color: var(--ghost-1);
  clip-path: inset(0 0 55% 0);
}

.cb-16__btn::after {
  color: var(--ghost-2);
  clip-path: inset(55% 0 0 0);
}

.cb-16__btn:is(:hover,:focus-visible) {
  background: var(--accent);
}

.cb-16__btn:is(:hover,:focus-visible)::before {
  animation: cb-16-a .48s steps(2,end) infinite;
}

.cb-16__btn:is(:hover,:focus-visible)::after {
  animation: cb-16-b .56s steps(2,end) infinite;
}

@keyframes cb-16-a {
  0%,
    100% {
    translate: 0 0;
    opacity: 0;
  }

  20% {
    translate: -3px -2px;
    opacity: 1;
  }

  45% {
    translate: 4px 1px;
    opacity: 1;
    clip-path: inset(0 0 68% 0);
  }

  70% {
    translate: -2px 2px;
    opacity: 1;
    clip-path: inset(0 0 42% 0);
  }
}

@keyframes cb-16-b {
  0%,
    100% {
    translate: 0 0;
    opacity: 0;
  }

  25% {
    translate: 3px 2px;
    opacity: 1;
  }

  50% {
    translate: -4px -1px;
    opacity: 1;
    clip-path: inset(48% 0 0 0);
  }

  75% {
    translate: 2px -2px;
    opacity: 1;
    clip-path: inset(62% 0 0 0);
  }
}

.cb-16__btn:focus-visible {
  outline: 3px dashed var(--ink);
  outline-offset: 5px;
}

.cb-16__btn:active {
  translate: var(--offset) var(--offset);
  box-shadow: 0 0 0 0 var(--ink);
}

.cb-16__btn:disabled {
  cursor: not-allowed;
  color: color-mix(in oklab,var(--ink) 40%,var(--slab));
  box-shadow: var(--offset) var(--offset) 0 0 color-mix(in oklab,var(--ink) 35%,var(--slab));
}

@media (prefers-reduced-motion: reduce) {
  .cb-16__btn {
    transition: none;
  }

  .cb-16__btn::before,
    .cb-16__btn::after {
    animation: none !important;
    display: none;
  }
}

@media (forced-colors: active) {
  .cb-16__btn {
    background: ButtonFace;
    color: ButtonText;
    border-color: ButtonText;
    box-shadow: var(--offset) var(--offset) 0 0 ButtonText;
  }

  .cb-16__btn::before,
    .cb-16__btn::after {
    display: none;
  }
}
```

How this works

The two glitch layers are ::before and ::after reading the label from a data- attribute, so there is only one source of truth for the text:

.cb-16__btn::before,.cb-16__btn::after{
  content:attr(data-label);
  position:absolute; inset:0; display:grid; place-items:center;
  clip-path:inset(0 0 55% 0);   /* top slice */
}
.cb-16__btn::after{ clip-path:inset(55% 0 0 0); }  /* bottom slice */

On hover each layer animates on steps() timing so the offsets jump rather than glide, which is what makes it read as signal corruption:

@keyframes cb-16-glitch-a{
  0%,100%{ translate:0 0; opacity:0; }
  20%{ translate:-3px -2px; opacity:1; }
  40%{ translate:4px 1px; clip-path:inset(0 0 68% 0); }
  60%{ translate:-2px 2px; clip-path:inset(0 0 40% 0); }
}
.cb-16__btn:hover::before{ animation:cb-16-glitch-a .5s steps(2,end) infinite; }

The shadow is a solid box-shadow with zero blur — the brutalist signature — and the press moves the button by exactly the shadow's offset so the two meet flush, which is why it feels mechanical rather than soft.

Make it yours

  • --offset controls both the shadow distance and the press travel; they must match or the flush-press illusion breaks.
  • Swap --ghost-1/--ghost-2 from cyan/magenta to your brand pair for a subtler glitch.
  • Slow the steps to steps(1,end) for a harsher two-frame stutter; raise to steps(4) for a busier one.
  • Remove the glitch entirely and keep the slab: this is a perfectly good brutalist button with just the shadow and the border.
  • Uppercase + monospace is doing a lot of the work here — keep letter-spacing above .06em at these sizes.

Gotchas — read before shipping

  • Glitch animations are high-risk for photosensitive users. This one is hover-only, short, and fully disabled under prefers-reduced-motion — do not run it on loop by default.
  • content:attr(data-label) is invisible to assistive tech, which is correct here: the real label is the button's own text, and the pseudo-layers are decoration only.
  • Brutalist palettes are usually high-contrast by accident, not by design. Verify the label against the fill — bright yellow on white is a failure.
  • clip-path on a pseudo-element establishes a new clip context; a focus outline drawn on the pseudo would be cut off. Keep the ring on the button.
  • Zero-blur shadows expose subpixel rounding: use whole-pixel offsets or the shadow edge will look soft on non-retina screens.

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 88+.

Everything is baseline: clip-path inset(), steps() timing, attr() in content and individual transform properties (Chrome 104 / Safari 14.1 / Firefox 72 — older engines simply show no offset).

Techniques used in this demo

Search CodeFronts

Loading…