51 CSS Buttons16 / 51
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
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><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;
}
}@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;
}
}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;
}
}
```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
--offsetcontrols both the shadow distance and the press travel; they must match or the flush-press illusion breaks.- Swap
--ghost-1/--ghost-2from cyan/magenta to your brand pair for a subtler glitch. - Slow the steps to
steps(1,end)for a harsher two-frame stutter; raise tosteps(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-spacingabove.06emat 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-pathon 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
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 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).