GODRICH
#css애니메이션#버튼#로딩#전환효과#스크롤#대시보드#실전ui#디자인트렌드

css masonry layout 9가지 — 높이가 다른 카드가 빈틈없이 자리를 찾는 배열

css masonry layout 을 columns 나 grid 한 줄로만 짜면 읽는 순서가 세로로 꼬이고 창 크기가 바뀔 때 카드가 순간이동한다 — 01번·02번·04번이 최단 열 배치·행 순서·FLIP 스프링으로 푼다.

아홉 가지 벽돌 배열을 화면에서 바로 만져 본다

자동 재생 · 눌러서 해당 항목으로 · 전부 한 zip

css masonry layout 9가지 — 열 높이·행 단위·스프링을 계산해야 빈틈이 닫힌다

장면 하나는 480×300 무대 안에 폭 최대 440px, 높이 216px 의 벽을 두고, 번호만 적힌 그라디언트 카드를 쌓는다. 카드 비율은 시드를 고정한 난수열이 뽑으므로 같은 시각이면 언제나 같은 프레임이 나오고, 벽 위 막대의 버튼으로 열 수·모드·필터를 직접 바꿀 수 있다.

벽돌 놓기는 두 층으로 나뉜다. 카드 높이를 미리 알면 열 끝이 어디인지만 기억해도 목표 자리가 정해지는 장면이 많고, 그 자리를 화면이 곧바로 쓰지 않고 따로 따라간다. 목표 자리를 구하는 규칙도 따라가는 방식도 장면마다 다르다. 속도를 쥔 스프링은 04 하나이고 09 는 중력을 적분하며, 01·02·06 은 목표를 향한 지수 접근이고 07 은 속도에 시간을 곱해 더하며 08 은 시간 이징이다. 03 과 05 는 배치 자체를 그 자리에서 계산한다. 아홉 파일 모두 CSS transition 과 @keyframes 가 한 줄도 없고 라이브러리·외부 주소 요청·Math.random 도 없다.

이미지 비율이 제각각인 갤러리가 목적이면 03 부터, 필터 버튼이 붙는 목록이면 08 부터, 창 폭이 바뀌는 반응형 화면이면 04 부터 열면 된다. 슬라이드·확대·라이트박스 같은 갤러리의 나머지 효과는 이미지 갤러리 CSS 9종 — 메이슨리부터 라이트박스까지와 CSS 이미지 슬라이드 애니메이션 9종이 맡고, 이 편은 카드가 어느 자리에 앉는지만 다룬다.

css masonry grid 를 기법별 코드로 본다

코드는 장면마다 index.html 의 <script> 안에서 이어진 줄만 가져왔고 조각 길이는 25줄 이하다. css masonry effect 라고 부르는 움직임은 대부분 여기 실린 자리 계산 위에 따라가기 한 겹을 얹은 것이라, 계산부터 읽으면 값을 바꿨을 때 무엇이 달라질지 짐작이 된다.

01가장 낮은 열에 한 장씩 얹는 최단 열 배치

카드를 순서대로 읽으며 그 순간 가장 낮은 열(동률이면 왼쪽)에 놓고, 그 열 높이에 카드 높이와 간격 8px 을 더한다. 핀 보드나 포트폴리오 격자처럼 높이가 제각각인 카드를 빈틈 적게 쌓고 싶을 때 맞고, 열 아래 가는 막대가 열 끝 높이를 보여 준다. 누적면적 47.7%에 움직임이 잡힌 프레임은 23/23 이었다.

가장 낮은 열을 찾아 한 장씩 얹는 최단 열 배치
새 창에서 열기

01 · 가장 낮은 열을 찾아 한 장씩 얹는 최단 열 배치 — 측정 누적면적 47.727% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COLUMNSGAPSETTLESEED
    function pack(hs, cols, cw, gap) {
        const colH = [];
        for (let k = 0; k < cols; k += 1) colH.push(0);
        const out = [];
        for (let i = 0; i < hs.length; i += 1) {
            let best = 0;
            for (let k = 1; k < cols; k += 1) {
                if (colH[k] < colH[best] - 0.5) best = k;
            }
            out.push({ x: best * (cw + gap), y: colH[best] });
            colH[best] += hs[i] + gap;
        }

02읽는 순서를 지키도록 열을 고르는 세 가지 규칙

세로 채움은 번호가 열을 따라 내려가 읽는 순서가 세로로 꼬이는 규칙이고, 가로 순서는 번호를 열 수로 나눈 나머지로 열을 정하며, 균형은 가로 순서로 가다가 열이 너무 높아지면 가장 낮은 열로 보낸다. 같은 카드 아홉 장이 규칙만 바꿔 열 끝 차이가 세로 5px·가로 72px·균형 30px 로 나오고(폭 424px 의 3열), 균형은 카드 5·6 만 자리를 맞바꾼다. 누적면적 53.1%에 움직임이 잡힌 프레임은 19/23 이었다.

왼쪽에서 오른쪽으로 읽히도록 순서를 바로잡은 열 배치
새 창에서 열기

02 · 왼쪽에서 오른쪽으로 읽히도록 순서를 바로잡은 열 배치 — 측정 누적면적 53.068% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

COLUMNSBALANCEGAPFOLLOW
            const c = cards[i];
            const h = c.ratio * cw;
            let col = i % COLUMNS;
            if (mode === 0) {
                col = Math.min(COLUMNS - 1, Math.floor(i / perCol));
            } else if (mode === 2) {
                let low = 0;
                for (let k = 1; k < COLUMNS; k += 1) {
                    if (colH[k] < colH[low]) low = k;
                }
                if (colH[col] - colH[low] > BALANCE * h) col = low;
            }
            c.w = cw;
            c.h = h;
            c.tx = col * (cw + GAP);
            c.ty = colH[col];
            colH[col] += h + GAP;

03행 칸 수를 계산해 grid 에 맡기는 스팬

배치는 진짜 CSS grid 가 하고 JS 는 span = ceil((h + 8) / 16) 으로 행 칸 수만 정해 gridRowEnd 에 적는다. 이미지 비율이 제각각인 갤러리에 맞고, 카드 안 번호 자리에는 그 칸 수가 보인다. 누적면적 60.2%에 움직임이 잡힌 프레임은 23/23 이었다.

이미지 비율로 행 칸 수를 계산하는 그리드 스팬
새 창에서 열기

03 · 이미지 비율로 행 칸 수를 계산하는 그리드 스팬 — 측정 누적면적 60.184% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

ROW_UNITGAPSPAN_BIASCOLUMNS
    function spanOf(h) {
        return Math.ceil((h + GAP) / (ROW_UNIT + GAP)) + SPAN_BIAS;
    }
    function relayout() {
        wall.style.width = Math.round(root.clientWidth * frac) + "px";
        const cw = (wall.clientWidth - GAP * (COLUMNS - 1)) / COLUMNS;
        for (let i = 0; i < cards.length; i += 1) {
            const n = spanOf(cw * cards[i].ratio);
            if (n !== spans[i]) {
                spans[i] = n;
                cards[i].el.style.gridRowEnd = "span " + n;
                const badge = cards[i].el.firstChild;
                if (badge) badge.textContent = String(n);
            }
        }
        spanEl.textContent = String(spans[0]);
        shownFrac = frac;
        ready = true;
    }

04열 수가 바뀔 때 스프링으로 제자리를 찾는 FLIP 재배치

새 자리를 구한 뒤 옛 위치와의 차이를 오프셋으로 남기고(relayout), 프레임마다 kick 이 속도와 강성으로 그 오프셋을 0 으로 끌고 가는 방식이 FLIP 이다. 폭 차이도 scale 로 보상해서 3열에서 4열로 갈 때는 +0.36 늘어난 채 출발하고, 열 수를 도중에 또 바꿔도 지금 화면 위치에서 이어져 점프가 없다. 누적면적 48.4%에 움직임이 잡힌 프레임은 19/23 이었다.

열 수가 바뀔 때 스프링으로 제자리를 찾는 FLIP 재배치
새 창에서 열기

04 · 열 수가 바뀔 때 스프링으로 제자리를 찾는 FLIP 재배치 — 측정 누적면적 48.372% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STIFFNESSDAMPINGSTAGGERAREA
    function kick(o, v, f) {
        const nv = (v - STIFFNESS * o * f) * Math.pow(DAMPING, f);
        return [o + nv * f, nv];
    }
        for (let i = 0; i < cards.length; i += 1) {
            const c = cards[i];
            if (ready && !reduce) {
                // First: 지금 화면에 보이는 자리와 크기에서 새 자리와 크기를 뺀 값이 오프셋이 된다.
                c.ox = c.tx + c.ox - at[i].x;
                c.oy = c.ty + c.oy - at[i].y;
                c.osx = (c.tw * (1 + c.osx)) / cw - 1;
                c.osy = (c.th * (1 + c.osy)) / hs[i] - 1;
                c.wait = i * STAGGER;
            }
            c.tx = at[i].x;
            c.ty = at[i].y;
            c.tw = cw;
            c.th = hs[i];
            c.w = cw;
            c.h = hs[i];
            if (!ready || reduce) {
                c.ox = 0;
                c.oy = 0;
                c.osx = 0;
                c.osy = 0;
            }
        }

05줄 높이를 맞춰 오른쪽 끝을 나란히 하는 균등 행

가로세로 비율을 더해 가다 줄 폭이 벽 폭에 닿으면 줄을 끊고, 그 줄의 높이에 배율 s 를 한 번 곱해 오른쪽 끝을 맞춘다. 가득 찬 줄은 늘 s ≤ 1 이라 줄어들기만 하고, 사진 앨범이나 결과 목록에 맞는다. 누적면적 44.8%에 움직임이 잡힌 프레임은 19/23 이었다.

한 줄 너비에 딱 맞게 높이를 맞추는 균등 행
새 창에서 열기

05 · 한 줄 너비에 딱 맞게 높이를 맞추는 균등 행 — 측정 누적면적 44.764% · 움직인 프레임 19/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

TARGET_HGAPMAX_STRETCHLERP
        while (i < cards.length) {
            const from = i;
            let sum = 0;
            while (i < cards.length) {
                sum += cards[i].ratio;
                i += 1;
                if (sum * th + GAP * (i - from - 1) >= width) break;
            }
            const n = i - from;
            let s = (width - GAP * (n - 1)) / (sum * th);
            // 마지막 줄이 너무 짧으면 늘리지 않고 목표 높이를 지킨다.
            if (s > 1 + MAX_STRETCH) s = 1;
            const rowH = th * s;
            let x = 0;
            for (let j = from; j < i; j += 1) {
                const c = cards[j];
                c.w = cards[j].ratio * rowH;
                c.h = rowH;
                c.x = x;
                c.y = y;
                x += c.w + GAP;
            }
            y += rowH + GAP;
            rows += 1;
        }

06포인터 곁에서 이웃이 비켜서는 렌즈 배열

카드 중심과 포인터의 거리 d 에 가우시안 감쇠를 곱해 목표 배율과 비켜서는 거리를 정하고, 현재 값은 지수 접근으로 따라간다. 가까운 카드는 커지고 먼 카드는 살짝 눌리므로 호버로 훑어보는 이미지 보드에 맞는다. 누적면적 54.4%에 움직임이 잡힌 프레임은 23/23 이었다.

포인터 가까이에서 이웃이 비켜서는 렌즈 배열
새 창에서 열기

06 · 포인터 가까이에서 이웃이 비켜서는 렌즈 배열 — 측정 누적면적 54.377% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LENS_RADIUSGAINFALLOFFLERP
            if (has) {
                const dx = c.tx + c.w / 2 - px;
                const dy = c.ty + c.h / 2 - py;
                const d = Math.sqrt(dx * dx + dy * dy);
                const g = Math.exp(-FALLOFF * (d / LENS_RADIUS) * (d / LENS_RADIUS));
                ds = 1 + GAIN * g - 0.06 * (1 - g);
                if (d > 1) {
                    ux = (dx / d) * PUSH * 4 * g * (1 - g);
                    uy = (dy / d) * PUSH * 4 * g * (1 - g);
                }
                lift = g;
                if (d < bestD) {
                    bestD = d;
                    best = i;
                }
            }
            const k = reduce ? 1 : 1 - Math.pow(1 - LERP, f);
            c.s += (ds - c.s) * k;
            c.ox += (ux - c.ox) * k;
            c.oy += (uy - c.oy) * k;

07열마다 반대로 흘러가는 무한 벽돌

열마다 방향을 번갈아 정하고 SPEED × DEPTH[k] 로 속도를 달리해 위치를 직접 더한 뒤, 열 높이로 나눈 나머지로 되감아 이음새를 숨긴다. 열마다 카드를 2벌 이어 붙이므로 DOM 카드가 20장에서 40개가 되고, 랜딩 배경이나 로고 벽에 맞는다. 누적면적 62.3%로 아홉 장면 가운데 가장 넓고 움직임이 잡힌 프레임은 23/23 이었다.

열마다 반대 방향으로 흘러가는 무한 벽돌
새 창에서 열기

07 · 열마다 반대 방향으로 흘러가는 무한 벽돌 — 측정 누적면적 62.272% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SPEEDDEPTHCOLUMNSGAP
            for (let n = 0; n < copies; n += 1) {
                let y = n * total;
                for (let i = 0; i < PER; i += 1) {
                    const h = ratios[k * PER + i] * cw;
                    const el = document.createElement("div");
                    el.className = CLS.card;
                    el.setAttribute("data-c", String((k * PER + i) % 6));
                    el.innerHTML = "<i>" + (k * PER + i + 1) + "</i>";
                    if (n > 0) el.setAttribute("aria-hidden", "true");
                    el.style.width = cw.toFixed(1) + "px";
                    el.style.height = h.toFixed(1) + "px";
                    el.style.transform = "translate3d(0," + y.toFixed(1) + "px,0)";
                    col.appendChild(el);
                    y += h + GAP;
                }
            }

08필터를 누르면 순번대로 자리를 옮기는 셔플

걸러진 카드만 다시 최단 열 배치로 목표 자리를 구하고, 카드마다 출발을 순번에 STAGGER 를 곱한 만큼 늦춰 시간 이징으로 옮긴다. 걸러지는 카드는 제자리에서 배율 0.5 와 투명도 0 으로 접히며, 카테고리 필터가 있는 갤러리에 맞는다. 누적면적 50.9%에 움직임이 잡힌 프레임은 22/23 이었다.

필터를 누르면 자리가 순서대로 옮겨 가는 셔플
새 창에서 열기

08 · 필터를 누르면 자리가 순서대로 옮겨 가는 셔플 — 측정 누적면적 50.885% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STAGGERDURATIONEASE_POWERCOLUMNS
            if (visible(c)) {
                c.tx = at[rank].x;
                c.ty = at[rank].y;
                c.ds = 1;
                c.dop = 1;
                c.t0 = now + rank * STAGGER;
                rank += 1;
            } else {
                c.ds = 0.5;
                c.dop = 0;
                c.t0 = now;
            }

09위에서 떨어져 튕기며 쌓이는 낙하 벽돌

카드마다 DROP_DELAY 만큼 늦게 출발해 중력으로 떨어지고, 목표 높이에 닿으면 속도의 0.32배로 튕기며 세로로 눌렸다 펴진다. 첫 착지는 233ms 에서 1517ms 사이로 흩어지고 전원이 멈추는 데 1850ms 가 걸려, 로딩 직후 등장 연출에 맞는다. 누적면적 46.3%에 움직임이 잡힌 프레임은 22/23 이었다.

위에서 떨어져 튕기며 쌓이는 낙하 벽돌
새 창에서 열기

09 · 위에서 떨어져 튕기며 쌓이는 낙하 벽돌 — 측정 누적면적 46.294% · 움직인 프레임 22/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

GRAVITYBOUNCESQUASHDROP_DELAY
        for (let s = 0; s < n; s += 1) {
            for (let i = 0; i < cards.length; i += 1) {
                const c = cards[i];
                c.sq *= Math.exp(-14 * h);
                if (c.rest === 1 || ts - startAt < i * DROP_DELAY) continue;
                c.vy += GRAVITY * h;
                c.y += c.vy * h;
                if (c.y >= c.ty) {
                    const hit = c.vy;
                    c.y = c.ty;
                    if (hit > 90) {
                        c.vy = -hit * BOUNCE;
                        c.sq = Math.max(c.sq, Math.min(0.3, (SQUASH * hit) / 4200));
                    } else {
                        c.vy = 0;
                        c.rest = 1;
                    }
                }
            }
        }

값이 배열을 바꾸는 곳 — 열 수·스프링 강성·렌즈 반경

값 하나로 모양이 크게 달라지는 곳은 셋이다. 아래 숫자는 소스의 같은 함수를 노드에서 직접 불러 센 값이고 벽 폭 440px 기준이라, 브라우저 렌더로 잰 수치와는 구분한다.

열을 늘리면 카드는 좁아지고 열 끝은 가지런해진다

01 의 COLUMNS 를 3 에서 5 로 바꾸면 카드 폭이 141px 에서 82px 로 줄고, 가장 긴 열의 끝이 190px 에서 66px 로 낮아지며, 열 끝 높이 차이도 54px 에서 36px 로 줄어든다. 같은 아홉 장이 더 넓게 퍼지니 열 하나에 쌓이는 높이가 짧아지는 셈이다.

강성을 올리면 넘침은 두 배가 되고 정착은 늦어진다

04 의 STIFFNESS 를 0.12 에서 0.3 으로 올리고 DAMPING 은 그대로 두면 100px 이동에서 목표를 넘치는 폭이 21px 에서 41px 로 늘어난다. 처음 1px 안에 드는 때는 약 0.28초로 거의 같고, 그 뒤 계속 1px 안에 머무는 때는 0.45초에서 0.55초로 늦어진다.

렌즈 반경을 넓히면 눌리던 카드가 풀린다

06 의 LENS_RADIUS 를 140 에서 240 으로 넓히고 포인터를 벽 가운데에 두면, 1.1배를 넘게 커진 카드가 12장 중 5장에서 6장으로 늘고 가장 큰 배율은 1.31 에서 1.45 로 오른다. 0.97 미만으로 눌린 카드는 6장에서 0장이 되어 렌즈가 넓을수록 벽 전체가 한꺼번에 부푼다.

브라우저에서 실제로 렌더해 얻은 측정은 다음 표에 모았다. 가장 작은 장면이 05 의 44.8%이고 가장 큰 것이 07 의 62.3%이며, 움직인 프레임이 23 에 못 미치는 장면은 02(19)·04(19)·05(19)·08(22)·09(22) 다섯이고 그 원인은 확인하지 않았다.

항목 누적면적 평균 강도 움직인 프레임
01 최단 열 배치 47.7% 102.9 23/23
02 열 고르는 세 규칙 53.1% 58.7 19/23
03 grid 스팬 60.2% 123.3 23/23
04 FLIP 스프링 48.4% 115.0 19/23
05 균등 행 44.8% 65.5 19/23
06 렌즈 배열 54.4% 98.4 23/23
07 흐르는 열 62.3% 159.2 23/23
08 필터 셔플 50.9% 63.6 22/23
09 낙하 쌓기 46.3% 126.8 22/23

어디서 깨지는가 — 카드가 수백 개일 때·좁은 화면·움직임 줄이기 설정

카드 높이가 비율 곱이라 실제 글 카드에는 그대로 못 쓴다

01·02·03·06·07·08·09 일곱 장면은 높이를 비율 × 열 폭 으로 정하고, 04 는 폭이 줄면 길어지는 비율 × AREA / 열 폭, 05 는 줄 높이에 가로세로 비율을 곱해 폭을 정한다. 이미지나 글이 들어간 실제 카드는 내용이 높이를 정하므로 한 번 그려 본 뒤 그 높이를 재서 pack 에 넘겨야 한다.

최단 열은 읽는 순서를 버린다

01 의 pack 은 높이만 보고 열을 고르기 때문에 번호가 열 사이를 건너뛴다. 가로로 읽혀야 하는 목록이면 02 의 가로 순서를 쓰되 열 끝 차이가 5px 에서 72px 로 벌어지는 값을 치르고, 균형 규칙이 30px 로 중간에서 타협한다.

grid 스팬은 칸마다 빈 틈을 남긴다

03 은 span × 16 - 8 이 카드 높이를 덮도록 올림하므로 카드마다 최대 15px 의 여분이 생기고 행 단위를 키울수록 커진다. 폭이 바뀌면 span 이 다시 계산되어 카드 높이가 한 행 단위씩 보간 없이 뛴다.

FLIP 의 폭 보상이 글자를 잠깐 찌그러뜨린다

04 가 열 수를 바꿀 때 폭 차이를 scale 로 되돌리므로 3열에서 4열은 +0.36 만큼 늘어난 채 출발한다. 번호 글자와 모서리가 그만큼 일그러졌다가 펴지고, 강성만 올리면 21px 이던 넘침이 41px 가 된다.

균등 행의 마지막 줄은 오른쪽 끝이 안 맞는다

05 에서 마지막 줄은 늘어나는 배율이 1.35 를 넘으면 늘리지 않으므로 오른쪽 끝이 비어 있다. 목표 높이 44 에서 둘째 줄 5장은 오른쪽 끝이 237.6px 에서 끝나고, 55 에서는 7장씩 두 줄이 모두 440px 에 정확히 맞는다.

렌즈는 터치와 키보드에서 다르게 켜진다

06 은 포인터가 마우스가 아니면 손을 뗀 뒤 pointerup 에서 렌즈를 끄고, 키보드로는 카드 12장이 tabIndex 를 가져 Tab 으로 한 장씩 포커스를 옮길 때만 그 카드 중심에 렌즈가 놓인다. 확대된 카드는 벽 안으로 붙잡히므로 모서리에서는 밀려나는 방향이 막힌다.

폭이 바뀔 때마다 DOM 이 다시 만들어진다

07 은 폭이 변하면 build() 로 카드를 통째로 다시 만든다. 지금은 40개라 부담이 없지만 카드가 수백 개이면 창 폭을 조절하는 동안 끊길 수 있다.

낙하 벽돌은 서로 부딪히지 않는다

09 에는 같은 열 카드끼리의 충돌 검사가 없고 각자 목표 높이에서만 멈춘다. 도착 시각이 어긋나 겹침을 피할 뿐이라 DROP_DELAY 를 줄이면 위 카드가 아래 카드의 튕김과 포개진다.

직접 고쳐 보며 경계를 확인하는 법

01 의 COLUMNS, 04 의 STIFFNESS, 06 의 LENS_RADIUS 를 하나씩 바꿔 보면 위 숫자가 어디서 바뀌는지 손으로 확인할 수 있다. 전체 파일은 이어지는 압축을 vbqsakg9 로 풀면 나오고, 여기에 아홉 장면의 소스와 리액트 판이 함께 담겨 있다.

변형 세 가지와 접근성

이름 값 느낌
열 수 바꾸기 01 COLUMNS 3 → 5 카드 폭이 141px 에서 82px 로 좁아져 가장 긴 열 끝이 190px 에서 66px 로 낮아지고 열 끝 차이는 54px 에서 36px 로 줄어든다.
스프링 강성 바꾸기 04 STIFFNESS 0.12 → 0.3 처음 닿는 때는 약 0.28초로 같지만 넘침이 21px 에서 41px 로 두 배가 되고 정착은 0.45초에서 0.55초로 늦어진다.
렌즈 반경 바꾸기 06 LENS_RADIUS 140 → 240 1.1배를 넘게 커진 카드가 5장에서 6장으로, 가장 큰 배율이 1.31 에서 1.45 로 늘고 눌린 카드는 6장에서 0장이 된다.

세 변형의 숫자는 노드에서 소스 함수를 직접 불러 센 값이다.

조작은 06 을 뺀 여덟 장면에서 진짜 <button> 이 맡는다. 06 은 버튼 없이 카드 12장이 tabIndex 를 가져 Tab 으로 닿는다. keydown 핸들러와 preventDefault 는 아홉 파일 어디에도 없어 키보드는 버튼의 기본 동작인 Enter·Space 로만 움직인다. 크기가 바뀌면 상태 줄의 aria-live 가 시연 중에는 꺼져 있다가 입력이 오면 polite 로 켜지고, 아홉 장면 모두 ResizeObserver 로 폭 변화를 받아 자리를 다시 계산한다.

prefers-reduced-motion 이 켜져 있으면 아홉 장면 모두 시연과 애니메이션 루프를 시작하지 않고 결과 배치만 한 번 그린다. 입력은 보간 없이 바로 반영되고, 01 의 다시 쌓기 와 07 의 멈춤 버튼은 이때 숨겨진다. 08 에서 걸러진 카드는 투명도만 0 이고 DOM 에는 남으므로 링크 카드로 바꿔 쓸 때는 hidden 이나 inert 를 따로 달아야 한다.

css masonry flexbox 만으로 충분한가?

열 안에 카드를 차례로 담는 방식은 02 의 세로 채움과 같은 순서가 되므로 그 수치로 짐작할 수 있다. 이 데모에서는 열 끝 차이가 5px 로 가장 고르지만 번호가 열을 따라 내려가 읽는 순서는 가장 많이 어긋난다. 가로 순서를 지키려면 02 의 규칙처럼 카드 하나마다 열을 계산해야 하고, 높이가 다른 카드를 한 줄에 맞추는 일은 05 처럼 줄 단위로 따로 계산해야 한다. 이 편의 아홉 파일은 카드 자리를 모두 계산으로 정하며, 행 칸 수를 grid 에 넘기는 03 만 배치를 CSS 에 맡긴다.

장면마다 맞는 곳은 다르다. 01 은 핀 보드, 02 는 읽는 순서가 중요한 카드 목록, 03 은 이미지 비율이 제각각인 갤러리, 04 는 반응형 전환, 05 는 사진 앨범, 06 은 호버로 훑는 이미지 보드, 07 은 랜딩 배경, 08 은 카테고리 필터 갤러리, 09 는 첫 등장 연출에 붙는다. 작품을 한데 모으는 화면의 구성은 포트폴리오 갤러리 UI 9종 — 드래그 정렬부터 모프 전환까지에서, 카드를 눌러 크게 여는 전환은 css 이미지 확대 애니메이션 9종 — 라이트박스 전환 모음에서 이어진다. 폭 변화를 받는 관찰자는 ResizeObserver에, 열 나누기의 기본 규칙은 CSS multi-column layout에 정리돼 있다.

css masonry layout codepen 예제를 가져다 쓰기 전에 자주 묻는 질문

css masonry grid generator 처럼 값을 바꿔 보려면 어디를 만지나?

각 장면 <script> 안의 const 값을 고친다. 열 수는 COLUMNS, 간격은 GAP, 카드 높이를 정하는 난수열은 SEED 이고, 칩에 적힌 이름이 그 장면에서 바꿔 볼 값이다.

카드 수가 많아져도 그대로 쓸 수 있나?

아홉 장면은 카드 8~20장에서 재서 그 이상은 확인하지 않았다. pack 은 카드 하나마다 열 수만큼 비교하므로 늘어나는 양은 카드 수에 비례하지만, 07 처럼 DOM 을 다시 만드는 장면은 수백 장에서 따로 재 봐야 한다.

라이브러리나 이미지가 필요한가?

필요 없다. 아홉 파일 모두 외부 주소 요청이 없고 번호만 적힌 그라디언트 상자로 돌아서, 한 장면의 index.html 과 style.css 만 옮겨도 동작한다.

압축 비밀번호를 입력하세요

비밀번호는 이 글 본문 안에 있습니다. 읽다 보면 나옵니다.