02Neo-brutalist offset
A thick black outline and a hard shadow offset to the lower right lift the card like cut paper, and the card drops into that shadow and back up to feel pressed.
Best forIndie products and portfolios that need loud contrast and a playful, hand-cut look
box-shadowtranslatearia-pressed

<main class="stage">
<div class="nb is-demo">
<article class="nb__card">
<div class="nb__head">
<div class="nb__avatar">DH</div>
<div class="nb__who">
<p class="nb__name">Dohyun Park</p>
<p class="nb__role">Front-end dev</p>
</div>
</div>
<div class="nb__stats">
<div class="nb__stat"><strong class="nb__num">1,842</strong><span class="nb__lab">Followers</span></div>
<div class="nb__stat"><strong class="nb__num">36</strong><span class="nb__lab">Posts</span></div>
</div>
<button class="nb__btn" type="button" id="nb-btn" aria-pressed="false">
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M168 100a60 60 0 1 1-60-60a60 60 0 0 1 60 60" opacity=".2"/><path d="M256 136a8 8 0 0 1-8 8h-16v16a8 8 0 0 1-16 0v-16h-16a8 8 0 0 1 0-16h16v-16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 8 8m-57.87 58.85a8 8 0 0 1-12.26 10.3C165.75 181.19 138.09 168 108 168s-57.75 13.19-77.87 37.15a8 8 0 0 1-12.25-10.3c14.94-17.78 33.52-30.41 54.17-37.17a68 68 0 1 1 71.9 0c20.65 6.76 39.23 19.39 54.18 37.17M108 152a52 52 0 1 0-52-52a52.06 52.06 0 0 0 52 52"/></g></svg>
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true" hidden><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg>
<span id="nb-label">Follow</span>
</button>
</article>
</div>
</main>@use "../../../../site/static/demo-tokens" as *;
$duration: $dur-quick;
$easing: $ease-out;
$color: $subject-blue;
@include stage($stage-yellow);
// 네오브루탈리즘 — 3px 잉크 테두리 + 오프셋 단색 그림자. 누르면 그림자 자리로 내려앉는다.
.nb {
position: relative;
width: 308px; // 카드 300 + 그림자 8 (눌림 여유)
height: 204px; // 카드 196 + 8
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.nb__card {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: $sp-3;
width: 300px;
height: 196px;
padding: 0 $sp-4;
background: #fff;
border: 3px solid $subject-ink;
border-radius: $r-card;
box-shadow: 8px 8px 0 $subject-ink;
color: $subject-ink;
}
.nb__head { display: flex; align-items: center; gap: $sp-3; }
.nb__avatar {
display: flex; align-items: center; justify-content: center;
box-sizing: border-box;
width: 56px; height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, $subject-sky, $subject-blue);
border: 3px solid $subject-ink;
color: $subject-ink;
font-size: 17px; line-height: 21px; font-weight: 800;
}
.nb__who { display: flex; flex-direction: column; align-items: flex-start; gap: $sp-1; }
.nb__name { margin: 0; font-size: 20px; line-height: 24px; font-weight: 800; }
.nb__role {
margin: 0;
display: inline-flex; align-items: center;
padding: $sp-1 $sp-2;
background: $color;
border-radius: $r-pill;
color: #fff;
font-size: 11px; line-height: 13px; font-weight: 700;
}
.nb__stats { display: flex; gap: $sp-2; }
.nb__stat {
display: flex; align-items: baseline; gap: $sp-1;
padding: $sp-1 $sp-2;
background: $stage-yellow;
border: 3px solid $subject-ink;
border-radius: $r-sm;
}
.nb__num { font-size: 17px; line-height: 20px; font-weight: 800; font-variant-numeric: tabular-nums; }
.nb__lab { font-size: 11px; line-height: 13px; font-weight: 700; }
.nb__btn {
display: inline-flex; align-items: center; justify-content: center;
gap: $sp-1;
height: 36px;
padding: 0 $sp-4;
background: $subject-ink;
border: 3px solid $subject-ink;
border-radius: $r-control;
box-shadow: 4px 4px 0 $subject-ink;
color: #fff;
font-size: 13px; line-height: 16px; font-weight: 700;
cursor: pointer;
transition: transform $duration $easing, box-shadow $duration $easing, background $duration $easing;
}
.nb__btn svg { display: block; }
.nb__btn svg[hidden] { display: none; }
.nb__btn:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
.nb__btn[aria-pressed="true"] { background: $color; }
.nb__card:active { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
.nb__btn:active { transform: translate(4px, 4px); box-shadow: 0 0 0 $subject-ink; }
// ── 자동 루프: 카드가 그림자 자리로 내려앉았다 올라온다(두 번). 복귀는 68% 안에 끝 — 끝 프레임 = 첫 프레임 ──
.nb.is-demo .nb__card { animation: nb-press $dur-loop $ease-out infinite; }
@keyframes nb-press {
0%, 100% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
9% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
20% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
46% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
55% { transform: translate(8px, 8px); box-shadow: 0 0 0 $subject-ink; }
68% { transform: translate(0, 0); box-shadow: 8px 8px 0 $subject-ink; }
}
@media (max-width: 340px) {
.nb { width: 280px; height: 158px; }
.nb__card { width: 272px; height: 150px; gap: $sp-1; padding: 0 $sp-3; }
.nb__head { gap: $sp-2; }
.nb__avatar { width: 44px; height: 44px; font-size: 14px; line-height: 17px; }
.nb__who { gap: 0; }
.nb__name { font-size: 14px; line-height: 18px; }
.nb__stats { gap: $sp-1; }
.nb__stat { gap: 0; padding: $sp-1; }
.nb__num { font-size: 13px; line-height: 16px; }
.nb__lab { font-size: 11px; line-height: 13px; }
.nb__btn { height: 32px; padding: 0 $sp-3; font-size: 12px; line-height: 14px; }
}
@media (prefers-reduced-motion: reduce) {
.nb.is-demo .nb__card { animation: none !important; }
.nb__card, .nb__btn { transition: none; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ffd23f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.nb {
position: relative;
width: 308px;
height: 204px;
overflow: hidden;
user-select: none;
-webkit-user-select: none;
}
.nb__card {
box-sizing: border-box;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
width: 300px;
height: 196px;
padding: 0 16px;
background: #fff;
border: 3px solid #17141a;
border-radius: 16px;
box-shadow: 8px 8px 0 #17141a;
color: #17141a;
}
.nb__head {
display: flex;
align-items: center;
gap: 12px;
}
.nb__avatar {
display: flex;
align-items: center;
justify-content: center;
box-sizing: border-box;
width: 56px;
height: 56px;
border-radius: 50%;
background: linear-gradient(135deg, #6ec8ff, #2f6df6);
border: 3px solid #17141a;
color: #17141a;
font-size: 17px;
line-height: 21px;
font-weight: 800;
}
.nb__who {
display: flex;
flex-direction: column;
align-items: flex-start;
gap: 4px;
}
.nb__name {
margin: 0;
font-size: 20px;
line-height: 24px;
font-weight: 800;
}
.nb__role {
margin: 0;
display: inline-flex;
align-items: center;
padding: 4px 8px;
background: #2f6df6;
border-radius: 999px;
color: #fff;
font-size: 11px;
line-height: 13px;
font-weight: 700;
}
.nb__stats {
display: flex;
gap: 8px;
}
.nb__stat {
display: flex;
align-items: baseline;
gap: 4px;
padding: 4px 8px;
background: #ffd23f;
border: 3px solid #17141a;
border-radius: 8px;
}
.nb__num {
font-size: 17px;
line-height: 20px;
font-weight: 800;
font-variant-numeric: tabular-nums;
}
.nb__lab {
font-size: 11px;
line-height: 13px;
font-weight: 700;
}
.nb__btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 4px;
height: 36px;
padding: 0 16px;
background: #17141a;
border: 3px solid #17141a;
border-radius: 12px;
box-shadow: 4px 4px 0 #17141a;
color: #fff;
font-size: 13px;
line-height: 16px;
font-weight: 700;
cursor: pointer;
transition: transform 220ms ease-out, box-shadow 220ms ease-out, background 220ms ease-out;
}
.nb__btn svg {
display: block;
}
.nb__btn svg[hidden] {
display: none;
}
.nb__btn:focus-visible {
outline: 2px solid #2f6df6;
outline-offset: 2px;
}
.nb__btn[aria-pressed=true] {
background: #2f6df6;
}
.nb__card:active {
transform: translate(8px, 8px);
box-shadow: 0 0 0 #17141a;
}
.nb__btn:active {
transform: translate(4px, 4px);
box-shadow: 0 0 0 #17141a;
}
.nb.is-demo .nb__card {
animation: nb-press 2s ease-out infinite;
}
@keyframes nb-press {
0%, 100% {
transform: translate(0, 0);
box-shadow: 8px 8px 0 #17141a;
}
9% {
transform: translate(8px, 8px);
box-shadow: 0 0 0 #17141a;
}
20% {
transform: translate(0, 0);
box-shadow: 8px 8px 0 #17141a;
}
46% {
transform: translate(0, 0);
box-shadow: 8px 8px 0 #17141a;
}
55% {
transform: translate(8px, 8px);
box-shadow: 0 0 0 #17141a;
}
68% {
transform: translate(0, 0);
box-shadow: 8px 8px 0 #17141a;
}
}
@media (max-width: 340px) {
.nb {
width: 280px;
height: 158px;
}
.nb__card {
width: 272px;
height: 150px;
gap: 4px;
padding: 0 12px;
}
.nb__head {
gap: 8px;
}
.nb__avatar {
width: 44px;
height: 44px;
font-size: 14px;
line-height: 17px;
}
.nb__who {
gap: 0;
}
.nb__name {
font-size: 14px;
line-height: 18px;
}
.nb__stats {
gap: 4px;
}
.nb__stat {
gap: 0;
padding: 4px;
}
.nb__num {
font-size: 13px;
line-height: 16px;
}
.nb__lab {
font-size: 11px;
line-height: 13px;
}
.nb__btn {
height: 32px;
padding: 0 12px;
font-size: 12px;
line-height: 14px;
}
}
@media (prefers-reduced-motion: reduce) {
.nb.is-demo .nb__card {
animation: none !important;
}
.nb__card, .nb__btn {
transition: none;
}
}// 실제 동작: Follow 버튼 토글. 첫 조작에 자동 루프가 끊기고 :active 눌림이 그대로 살아 있다.
var root = document.querySelector('.nb');
var btn = document.getElementById('nb-btn');
var label = document.getElementById('nb-label');
var icons = btn.querySelectorAll('svg');
root.addEventListener('pointerdown', function () { root.classList.remove('is-demo'); });
btn.addEventListener('click', function () {
root.classList.remove('is-demo');
var on = btn.getAttribute('aria-pressed') !== 'true';
btn.setAttribute('aria-pressed', String(on));
icons[0].hidden = on;
icons[1].hidden = !on;
label.textContent = on ? 'Following' : 'Follow';
});import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./NeoBrutalCard.module.scss";
export type NeoBrutalCardProps = {
duration?: number;
easing?: string;
color?: string;
};
type ClickEvt = { currentTarget: { blur(): void } };
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
const IconFollow = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M168 100a60 60 0 1 1-60-60a60 60 0 0 1 60 60" opacity=".2"/><path d="M256 136a8 8 0 0 1-8 8h-16v16a8 8 0 0 1-16 0v-16h-16a8 8 0 0 1 0-16h16v-16a8 8 0 0 1 16 0v16h16a8 8 0 0 1 8 8m-57.87 58.85a8 8 0 0 1-12.26 10.3C165.75 181.19 138.09 168 108 168s-57.75 13.19-77.87 37.15a8 8 0 0 1-12.25-10.3c14.94-17.78 33.52-30.41 54.17-37.17a68 68 0 1 1 71.9 0c20.65 6.76 39.23 19.39 54.18 37.17M108 152a52 52 0 1 0-52-52a52.06 52.06 0 0 0 52 52"/></g></svg>
);
const IconCheck = (): ReactNode => (
<svg viewBox="0 0 256 256" width="16" height="16" aria-hidden="true"><g fill="currentColor"><path d="M232 56v144a16 16 0 0 1-16 16H40a16 16 0 0 1-16-16V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16" opacity=".2"/><path d="m205.66 85.66l-96 96a8 8 0 0 1-11.32 0l-40-40a8 8 0 0 1 11.32-11.32L104 164.69l90.34-90.35a8 8 0 0 1 11.32 11.32"/></g></svg>
);
export default function NeoBrutalCard({
duration = 220,
easing = "ease-out",
color = "#2f6df6",
}: NeoBrutalCardProps) {
const [on, setOn] = useState(false);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
function toggle(e: ClickEvt): void {
setOn((v: boolean): boolean => !v);
e.currentTarget.blur();
}
return (
<div className={styles.nb}>
<article className={styles.card}>
<div className={styles.head}>
<div className={styles.avatar}>DH</div>
<div className={styles.who}>
<p className={styles.name}>박도현</p>
<p className={styles.role}>프론트엔드 개발자</p>
</div>
</div>
<div className={styles.stats}>
<div className={styles.stat}><strong className={styles.num}>1,842</strong><span className={styles.lab}>팔로워</span></div>
<div className={styles.stat}><strong className={styles.num}>36</strong><span className={styles.lab}>게시물</span></div>
</div>
<button type="button" className={styles.btn} aria-pressed={on} onClick={toggle}>
{on ? <IconCheck /> : <IconFollow />}
<span>{on ? "팔로잉" : "팔로우"}</span>
</button>
</article>
</div>
);
}02 Neo-brutalist offset — Measured area 18.2% · moving frames 11/23Original by GODRICH · SCSS and React ship in the zip · 9 Profile Card Design Styles, Copy-Paste Ready
The post this demo lives in
9 Profile Card Design Styles, Copy-Paste ReadyProfile card design is the job of deciding what material a fixed block of content wears: avatar, name, role, stats, foll See all demos in this post


