03Name-pattern avatar
Hash bits are spread across cells so the same name always yields the same 5x5 pattern, and the left half is mirrored onto the right to stand it symmetrically.
Best forTeam and repository lists where initials collide
grid-template-columnsanimation-delaytransform-origin
<main class="stage">
<section class="id is-demo" aria-label="Name-pattern avatar">
<div class="id__field">
<label class="id__label" for="id-name">Name</label>
<input class="id__input" id="id-name" type="text" value="Admin" autocomplete="off" spellcheck="false" placeholder="Type a name">
</div>
<!-- 25칸은 JS 가 만든다: 해시 비트를 15칸(열 0·1·2)에 배분하고 열 0↔4 · 1↔3 을 거울처럼 맞춘다 -->
<div class="id__grid" data-tone="1" aria-hidden="true"></div>
<ul class="id__list">
<li class="id__item"><div class="id__mini" data-tone="5" aria-hidden="true"></div><span class="id__name">Designer</span></li>
<li class="id__item"><div class="id__mini" data-tone="4" aria-hidden="true"></div><span class="id__name">Planner</span></li>
<li class="id__item"><div class="id__mini" data-tone="0" aria-hidden="true"></div><span class="id__name">Operator</span></li>
</ul>
</section>
</main>@use "../../../../site/static/demo-tokens" as *;
// 279 아바타 팔레트 — 이니셜 글자는 언제나 $subject-cream. 괄호는 크림 글자 기준 실측 대비비
$av-blue: #7c90be; // 5.29:1
$av-violet: #9791c9; // 5.76:1
$av-teal: #479082; // 5.79:1
$av-plum: #ab5488; // 6.52:1
$av-brick: $error; // 오류 토큰 — 5.13:1. 리터럴 금지 (BD10)
$av-slate: #3b4a63; // 8.40:1
$av-mist: #b6b0c0; // 잉크 무대 보조 글자 8.65:1
$av-deep: #4a4452; // 옐로 무대 보조 글자 6.49:1 · 페이퍼 무대 9.38:1
// ── 변수 3개 ──────────────────────────────────────────
$duration: 1s; // 칸 숨쉬기 한 바퀴 (2s 의 약수)
$easing: $ease-out;
$color: $stage-ink; // 오렌지 무대 글자 5.50:1 — 크림은 3.11:1 로 금지
@include stage($stage-orange);
.id {
display: grid;
justify-items: center;
gap: $sp-3;
width: min(360px, 92vw);
}
.id__field {
display: flex;
align-items: center;
gap: $sp-2;
}
.id__label {
color: $color;
font-weight: 600;
font-size: 13px;
line-height: 1.2;
}
.id__input {
width: 150px;
padding: $sp-2 $sp-3;
border: 2px solid $color;
border-radius: $r-sm;
background: transparent;
color: $color;
font-family: inherit;
font-weight: 600;
font-size: 14px;
line-height: 1.3;
}
.id__input:focus-visible { outline: 2px solid $color; outline-offset: 2px; }
// 큰 무늬 — 5×5. 칸 폭이 정확히 균등해진다
.id__grid {
display: grid;
grid-template-columns: repeat(5, 1fr); // 칩
width: 64px;
height: 64px;
}
// 칸은 켜진 것만 색이 들어간다. 꺼진 칸은 무대 위 희미한 격자로 자리만 지킨다
.id__cell {
transform-origin: center; // 칩 — 제자리에서 숨쉰다
border-radius: $r-xs;
background: rgba(23, 20, 26, .12);
}
.id__grid[data-tone="0"] .id__cell.is-on,
.id__mini[data-tone="0"] .id__cell.is-on { background: $av-blue; }
.id__grid[data-tone="1"] .id__cell.is-on,
.id__mini[data-tone="1"] .id__cell.is-on { background: $av-violet; }
.id__grid[data-tone="2"] .id__cell.is-on,
.id__mini[data-tone="2"] .id__cell.is-on { background: $av-teal; }
.id__grid[data-tone="3"] .id__cell.is-on,
.id__mini[data-tone="3"] .id__cell.is-on { background: $av-plum; }
.id__grid[data-tone="4"] .id__cell.is-on,
.id__mini[data-tone="4"] .id__cell.is-on { background: $av-brick; }
.id__grid[data-tone="5"] .id__cell.is-on,
.id__mini[data-tone="5"] .id__cell.is-on { background: $av-slate; }
.id__list {
display: flex;
gap: $sp-4;
margin: 0;
padding: 0;
list-style: none;
}
.id__item { display: grid; justify-items: center; gap: $sp-1; }
.id__mini {
display: grid;
grid-template-columns: repeat(5, 1fr);
width: 40px;
height: 40px;
}
.id__name {
color: $color;
font-weight: 500;
font-size: 11px;
line-height: 1.2;
}
// ── 격자 미리보기 루프 ────────────────────────────────
// 칸마다 시차를 두고 조용히 숨쉰다. 켜진 칸은 사라지지 않는다 — opacity 로 끄지 않는다(유령 프레임 금지).
// animation 단축은 delay 를 0 으로 리셋하므로 낱개 속성으로만 쓴다
.id.is-demo .id__cell {
animation-name: idBreathe;
animation-duration: $duration;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 40ms);
animation-fill-mode: backwards; // 지연 구간에도 0% 자세(scale .72)가 찍힌다
}
@keyframes idBreathe {
0% { transform: scale(.72); }
50% { transform: scale(1); }
100% { transform: scale(.72); }
}
// 미리보기 루프 동안에는 자동완성 흔적 글자가 겹쳐 보이지 않게 한다
.id.is-demo .id__input::placeholder { color: transparent; }
// 좁은 화면 — 표시와 여백만 줄인다. 값을 올리지 않는다.
@media (max-width: 340px), (max-height: 220px) {
.id { gap: $sp-2; }
.id__input { width: 120px; font-size: 13px; }
.id__grid { width: 56px; height: 56px; }
.id__mini { width: 32px; height: 32px; }
.id__list { gap: $sp-3; }
.id__name { font-size: 10px; }
}
@media (prefers-reduced-motion: reduce) {
// .id.is-demo .id__cell (0,3,0) 를 이기려면 !important 가 필요하다.
// 움직임만 끄고 무늬는 그대로 남긴다
.id__cell { animation: none !important; }
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.id {
display: grid;
justify-items: center;
gap: 12px;
width: min(360px, 92vw);
}
.id__field {
display: flex;
align-items: center;
gap: 8px;
}
.id__label {
color: #17141a;
font-weight: 600;
font-size: 13px;
line-height: 1.2;
}
.id__input {
width: 150px;
padding: 8px 12px;
border: 2px solid #17141a;
border-radius: 8px;
background: transparent;
color: #17141a;
font-family: inherit;
font-weight: 600;
font-size: 14px;
line-height: 1.3;
}
.id__input:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
}
.id__grid {
display: grid;
grid-template-columns: repeat(5, 1fr);
width: 64px;
height: 64px;
}
.id__cell {
transform-origin: center;
border-radius: 4px;
background: rgba(23, 20, 26, 0.12);
}
.id__grid[data-tone="0"] .id__cell.is-on,
.id__mini[data-tone="0"] .id__cell.is-on {
background: #7c90be;
}
.id__grid[data-tone="1"] .id__cell.is-on,
.id__mini[data-tone="1"] .id__cell.is-on {
background: #9791c9;
}
.id__grid[data-tone="2"] .id__cell.is-on,
.id__mini[data-tone="2"] .id__cell.is-on {
background: #479082;
}
.id__grid[data-tone="3"] .id__cell.is-on,
.id__mini[data-tone="3"] .id__cell.is-on {
background: #ab5488;
}
.id__grid[data-tone="4"] .id__cell.is-on,
.id__mini[data-tone="4"] .id__cell.is-on {
background: #c2361a;
}
.id__grid[data-tone="5"] .id__cell.is-on,
.id__mini[data-tone="5"] .id__cell.is-on {
background: #3b4a63;
}
.id__list {
display: flex;
gap: 16px;
margin: 0;
padding: 0;
list-style: none;
}
.id__item {
display: grid;
justify-items: center;
gap: 4px;
}
.id__mini {
display: grid;
grid-template-columns: repeat(5, 1fr);
width: 40px;
height: 40px;
}
.id__name {
color: #17141a;
font-weight: 500;
font-size: 11px;
line-height: 1.2;
}
.id.is-demo .id__cell {
animation-name: idBreathe;
animation-duration: 1s;
animation-timing-function: ease-out;
animation-iteration-count: infinite;
animation-delay: calc(var(--i) * 40ms);
animation-fill-mode: backwards;
}
@keyframes idBreathe {
0% {
transform: scale(0.72);
}
50% {
transform: scale(1);
}
100% {
transform: scale(0.72);
}
}
.id.is-demo .id__input::placeholder {
color: transparent;
}
@media (max-width: 340px), (max-height: 220px) {
.id {
gap: 8px;
}
.id__input {
width: 120px;
font-size: 13px;
}
.id__grid {
width: 56px;
height: 56px;
}
.id__mini {
width: 32px;
height: 32px;
}
.id__list {
gap: 12px;
}
.id__name {
font-size: 10px;
}
}
@media (prefers-reduced-motion: reduce) {
.id__cell {
animation: none !important;
}
}(function () {
var root = document.querySelector('.id');
var input = root.querySelector('.id__input');
var grid = root.querySelector('.id__grid');
// FNV-1a 32비트 — 같은 이름은 늘 같은 정수를 돌려준다
function hash32(text) {
var h = 2166136261;
for (var i = 0; i < text.length; i++) {
h = (h ^ text.charCodeAt(i)) >>> 0;
h = Math.imul(h, 16777619) >>> 0;
}
return h >>> 0;
}
// 비트 15개를 열 0·1·2 에 놓고 열 0↔4 · 1↔3 을 거울처럼 맞춰 25칸을 채운다
function cellsOf(text) {
var h = hash32(text);
var cells = [];
for (var row = 0; row < 5; row++) {
var left = [(h >>> (row * 3)) & 1, (h >>> (row * 3 + 1)) & 1, (h >>> (row * 3 + 2)) & 1];
cells.push(left[0], left[1], left[2], left[1], left[0]);
}
return cells;
}
function buildCells(el) {
for (var i = 0; i < 25; i++) {
var cell = document.createElement('span');
cell.className = 'id__cell';
el.appendChild(cell);
}
}
// 켜진 칸만 클래스가 붙고, 색도 해시로 고른 data-tone 번호가 정한다(scss 가 칠한다)
function render(el, name) {
var cells = cellsOf(name);
el.dataset.tone = String(hash32(name) % 6);
var kids = el.children;
for (var i = 0; i < 25; i++) {
kids[i].classList.toggle('is-on', cells[i] === 1);
kids[i].style.setProperty('--i', String(i));
}
}
buildCells(grid);
render(grid, input.value);
// 작은 무늬 셋 — Name 표시에서 각각 계산한다
var items = root.querySelectorAll('.id__item');
Array.prototype.forEach.call(items, function (item) {
var mini = item.querySelector('.id__mini');
buildCells(mini);
render(mini, item.querySelector('.id__name').textContent);
});
// 입력이 바뀌면 그 자리에서 다시 그린다
input.addEventListener('input', function () { render(grid, input.value); });
// 사람이 손을 대면 미리보기 루프를 뗀다
function stopLoop() { root.classList.remove('is-demo'); }
root.addEventListener('pointerdown', stopLoop);
root.addEventListener('focusin', stopLoop);
})();import { useState } from "react";
import type { CSSProperties, ReactNode } from "react";
import styles from "./AvatarIdenticonPattern.module.scss";
export type AvatarIdenticonPatternProps = {
label?: string;
nameLabel?: string;
placeholder?: string;
name?: string;
smallNames?: string[];
/** 칸 숨쉬기 한 바퀴(ms) — 2초의 약수 */
duration?: number;
easing?: string;
/** 오렌지 무대 글자색 */
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type CellVars = CSSProperties & { "--i"?: string };
// ambient 타입에 React 이벤트 제네릭이 없어 로컬 타입을 쓴다
type InputEvt = { target: { value: string } };
// FNV-1a 32비트 — 같은 이름은 늘 같은 정수를 돌려준다
function hash32(text: string): number {
let h = 2166136261;
for (let i = 0; i < text.length; i++) {
h = (h ^ text.charCodeAt(i)) >>> 0;
h = Math.imul(h, 16777619) >>> 0;
}
return h >>> 0;
}
// 비트 15개를 열 0·1·2 에 놓고 열 0↔4 · 1↔3 을 거울처럼 맞춰 25칸을 채운다
function cellsOf(text: string): number[] {
const h = hash32(text);
const cells: number[] = [];
for (let row = 0; row < 5; row++) {
const left = [(h >>> (row * 3)) & 1, (h >>> (row * 3 + 1)) & 1, (h >>> (row * 3 + 2)) & 1];
cells.push(left[0], left[1], left[2], left[1], left[0]);
}
return cells;
}
function IdenticonGrid({ className, name }: { className: string; name: string }): ReactNode {
// 켜진 칸만 색이 들어간다 — 색도 해시로 고른 data-tone 번호가 정한다(scss 가 칠한다)
return (
<div className={className} data-tone={String(hash32(name) % 6)} aria-hidden="true">
{cellsOf(name).map((on, i) => (
<span
key={i}
className={on ? `${styles.cell} ${styles.cellOn}` : styles.cell}
style={{ "--i": String(i) } as CellVars}
/>
))}
</div>
);
}
export default function AvatarIdenticonPattern({
label = "이름 무늬 아바타",
nameLabel = "이름",
placeholder = "이름을 입력하세요",
name = "관리자",
smallNames = ["디자이너", "기획자", "운영자"],
duration = 1000,
easing = "ease-out",
color = "#17141a",
}: AvatarIdenticonPatternProps): ReactNode {
const [value, setValue] = useState<string>(name);
const [loop, setLoop] = useState<boolean>(true);
const vars: Vars = { "--duration": `${duration}ms`, "--easing": easing, "--color": color };
return (
<section
className={loop ? `${styles.id} ${styles.isDemo}` : styles.id}
style={vars}
aria-label={label}
onPointerDown={() => setLoop(false)}
onFocus={() => setLoop(false)}
>
<div className={styles.field}>
<label className={styles.label} htmlFor="id-name">{nameLabel}</label>
<input
className={styles.input}
id="id-name"
type="text"
value={value}
autoComplete="off"
spellCheck={false}
placeholder={placeholder}
onChange={(e: InputEvt) => setValue(e.target.value)}
/>
</div>
{/* 입력이 바뀌면 그 자리에서 다시 그린다 */}
<IdenticonGrid className={styles.grid} name={value} />
<ul className={styles.list}>
{smallNames.map((smallName) => (
<li key={smallName} className={styles.item}>
<IdenticonGrid className={styles.mini} name={smallName} />
<span className={styles.smallName}>{smallName}</span>
</li>
))}
</ul>
</section>
);
}03 Name-pattern avatar — Measured area 3.54% · moving frames 23/23Original by GODRICH · SCSS and React ship in the zip · Avatar UI Design: 9 Parts, No Image Files
The post this demo lives in
If you like this effect - similar demos
Auth UI05 Password change that opens when three fields agree9 account security settings UI patterns
Auth UI03 Social login — a button group that recomputes column count from how many providers there are9 Login UI Design Patterns (Copy-Paste)
Auth UI03 A 3×3 Tile Board Where Picking the Matching Pattern Flips the Tile Over to Mark ItCSS Captcha: 9 Warped Text and Rotate Checks