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

css kanban board 9가지 — 카드가 따라붙고 놓을 자리가 열리는 보드

css kanban board 를 카드 순서 교체만으로 만들면 집은 카드는 커서와 따로 놀고, 놓을 자리는 안 보이고, 가득 찬 열에도 카드가 들어간다 — 01번·02번·04번이 계산으로 푼다.

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

css kanban board 9가지 — 포인터·간격·스프링을 계산해야 보드가 손에 붙는다

아홉 장면은 보드 하나를 쓰는 순서대로 놓았다. 01 은 카드가 커서를 따라오는 방식, 02 는 놓을 자리가 열리는 간격, 03 은 놓은 뒤 내려앉는 스프링이고, 04 는 열이 찰수록 한도를 알리는 압력을 다룬다. 05 는 가장자리에서 보드가 밀리는 속도, 06 은 열 머리의 숫자 배지, 07 은 정렬 버튼이 카드를 옮기는 순서, 08 은 완료 열이 정하는 진행 링, 09 는 키보드로 옮기고 문장으로 알리는 장면이다.

이런 화면에서 쓴다

작업 관리 보드를 소개하는 화면에는 01 과 03 이 어울린다. 01 은 카드가 커서를 따라오고 03 은 놓은 카드가 내려앉아, 보드를 처음 만지는 순간의 움직임을 보여 주기 때문이다. 열 사이 이동을 설명하는 자리에는 02, 진행 중 작업 수 제한을 안내하는 자리에는 04 가 맞는다. 열이 많은 넓은 보드는 05, 열 머리의 카드 수 표시는 06, 우선순위 정렬 버튼 소개는 07, 프로젝트 진행률 요약 카드는 08 을 쓴다. 포인터 없이 쓰는 보드의 접근성을 설명할 때는 09 가 답이다.

아홉 장면이 같은 보드 규격을 쓴다. 칸 단위 U 는 화면 높이가 260px 이상이면 6px, 아니면 4px 이고, 카드 높이는 6U, 카드 한 단은 7U, 열 머리는 5U 다. 그래서 같은 코드가 화면 높이에 맞춰 두 단계 크기로 바뀐다.

이웃 편과 무엇이 다른가

드래그 앤 드롭 UI 리스트 — 목표 인덱스 계산을 얹은 9종은 드래그해서 순서를 바꾸는 리스트가 중심이고, 대시보드 컴포넌트 9종 — 화면 통째로 말고 이 부품만에는 정적인 3열 칸반 카드가 들어 있다. 이 편은 그 사이를 메운다. 카드가 포인터를 따라붙는 계수, 놓을 자리가 열리는 간격, 열이 차는 압력을 각각 숫자로 정한다. 차트 쪽 인터랙션은 대시보드 차트 디자인 인터랙션 9종 — 호버부터 드릴다운까지가, 줄 세우기 화면은 랭킹 ui 디자인 9가지 — 랭킹 페이지 디자인도 같은 원리다가 맡는다.

기법별 코드로 보는 칸반

코드는 라이브러리 없이 일반 JS 와 CSS 로만 쓰였다. 01~05 는 pointerdown 과 setPointerCapture 로 카드를 잡고, 06~09 는 포인터 드래그 없이 클릭·정렬 버튼·키보드로 움직인다. 아홉 파일 모두 matchMedia 로 움직임 줄이기 설정을 읽고, requestAnimationFrame 으로 프레임을 돌린다. 아래에는 각 장면에서 움직임을 정하는 계산 구간만 소스 그대로 옮겼다.

01커서를 따라붙는 카드와 이동량 기울기

집은 카드는 매 프레임 포인터와의 거리 가운데 FOLLOW_LERP 만큼만 좁히고, 그 프레임의 이동량에 비례해 기울었다가 멈추면 원래 각도로 돌아온다. 이 장면은 열을 바꾸지 않아서 놓으면 카드가 원래 자리로 돌아간다. 시연 중 누적면적은 36.0% 이고 움직인 프레임은 23개 중 23개다.

커서를 따라붙는 카드와 이동량 기울기
새 창에서 열기

01 · 커서를 따라붙는 카드와 이동량 기울기 — 측정 누적면적 35.966% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FOLLOW_LERPTILT_MAXTILT_PER_MOVELIFT_SHADOW
  // 핵심: 집은 카드는 목표와의 거리 FOLLOW_LERP 만큼만 따라가고, 그 이동량으로 기울기가 정해진다
  function stepCard(c, dt) {
    var f = dt * 60;
    if (c === held) {
      var k = reduce ? 1 : 1 - Math.pow(1 - FOLLOW_LERP, f);
      var nx = c.x + (tgt.x - c.x) * k;
      var ny = c.y + (tgt.y - c.y) * k;
      var move = (nx - c.x) / f;
      c.vx = (nx - c.x) / dt;
      c.vy = (ny - c.y) / dt;
      c.x = nx;
      c.y = ny;
      var want = clamp(move * TILT_PER_MOVE, -TILT_MAX, TILT_MAX);
      c.rot += (want - c.rot) * (1 - Math.pow(0.7, f));
      c.lift += (1 - c.lift) * (1 - Math.pow(0.75, f));
    } else {
      var h = targetOf(c);
      spring(c, h.x, h.y, 180, 22, dt);
      c.rot += (0 - c.rot) * (1 - Math.pow(0.8, f));
      c.lift += (0 - c.lift) * (1 - Math.pow(0.75, f));
      settleClass(c, h);
    }
    c.sc = 1 + 0.05 * c.lift;
    c.el.style.setProperty('--lift-px', (c.lift * LIFT_SHADOW).toFixed(1) + 'px');
  }

02놓을 자리가 열리는 열

포인터 높이를 각 카드의 중간선과 비교해 몇 번째 자리에 들어갈지 구하고, 그 자리 아래 카드는 스프링으로 SLOT_GAP 만큼 내려가 빈 곳을 연다. 단위가 카드 한 단이라 값 1 이 카드 하나만큼의 빈 자리다. 누적면적은 22.6% 이고 움직인 프레임은 23개 중 23개다.

놓을 자리가 열리는 열
새 창에서 열기

02 · 놓을 자리가 열리는 열 — 측정 누적면적 22.584% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SLOT_GAPSPRING_KSPRING_DAMPMIDLINE_BIAS
  function insertionAt(px, py) {
    var c = clamp(Math.floor(px / (colW + GAP)), 0, COLS - 1);
    var r = 0;
    for (var k = 0; k < cols[c].length; k++) {
      if (slotY(k) + CARD_H / 2 + MIDLINE_BIAS < py) r = k + 1;
    }
    return { c: c, r: r };
  }
  // 놓을 자리 이후의 카드는 SLOT_GAP 칸만큼 아래로 목표가 밀린다
  function targetOf(c) {
    if (c === held) return tgt;
    var s = locate(c.i);
    var y = slotY(s.r);
    if (ins && ins.c === s.c && s.r >= ins.r) y += SLOT_GAP * STEP;
    return { x: slotX(s.c), y: y };
  }

03놓으면 자리에 내려앉는 카드

집은 카드는 포인터보다 HOVER_PX 만큼 위에 떠 있다가, 놓으면 SETTLE 스프링으로 슬롯까지 내려오며 목표를 한 번 넘었다 돌아온다. 그림자는 슬롯과의 높이 차에 비례해 줄어든다. 누적면적은 27.4% 이고 움직인 프레임은 23개 중 20개다.

놓으면 자리에 내려앉는 카드
새 창에서 열기

03 · 놓으면 자리에 내려앉는 카드 — 측정 누적면적 27.447% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

SETTLE_KSETTLE_DAMPDROP_SHADOWOVERSHOOT_CAP
  // 핵심: 놓은 카드는 SETTLE_K·SETTLE_DAMP 스프링으로 슬롯에 내려앉고, 그림자는 슬롯과의 높이 차에 비례해 줄어든다
  function stepCard(c, dt) {
    var f = dt * 60;
    if (c === held) {
      var k = reduce ? 1 : 1 - Math.pow(0.5, f);
      c.vx = ((tgt.x - c.x) * k) / dt;
      c.vy = ((tgt.y - c.y) * k) / dt;
      c.x += (tgt.x - c.x) * k;
      c.y += (tgt.y - c.y) * k;
      c.lift += (1 - c.lift) * (1 - Math.pow(0.75, f));
    } else if (c === dropped) {
      var h = targetOf(c);
      spring(c, h.x, h.y, SETTLE_K, SETTLE_DAMP, dt);
      var over = c.y - h.y;
      if (over > OVERSHOOT_CAP) {
        c.y = h.y + OVERSHOOT_CAP;
        if (c.vy > 0) c.vy = 0;
      }
      var want = clamp((h.y - c.y) / HOVER_PX, 0, 1);
      c.lift += (want - c.lift) * (1 - Math.pow(0.5, f));
      if (Math.abs(over) < 0.4 && Math.abs(c.vy) < 12 && Math.abs(c.x - h.x) < 0.5) {
        dropped = null;
        c.el.classList.remove(CLS.held);
      }

04한도가 차오르는 열 머리

가운데 열의 카드 수를 WIP_LIMIT 로 나눈 값이 압력이 되고, 압력은 머리 게이지와 배경 농도를 올린다. 한도에 찬 열 위에서는 자리가 열리지 않아, 놓은 카드가 원래 열로 스프링을 타고 돌아간다. 누적면적은 29.1% 이고 움직인 프레임은 23개 중 23개다.

한도가 차오르는 열 머리
새 창에서 열기

04 · 한도가 차오르는 열 머리 — 측정 누적면적 29.077% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

WIP_LIMITPRESSURE_LERPRETURN_KTINT_MAX
  function pressureGoal() {
    var n = cols[DOING].length;
    if (held) n = blocked ? WIP_LIMIT : n + (ins && ins.c === DOING ? 1 : 0);
    return Math.min(1, n / WIP_LIMIT);
  }
  function grab(c, px, py) {
    origin = locate(c.i);
    cols[origin.c].splice(origin.r, 1);
    blocked = s.c === DOING && cols[DOING].length >= WIP_LIMIT;
    ins = blocked ? null : s;
  }

05가장자리에서 보드가 움직이는 자동 스크롤

포인터가 보드 가장자리 구역에 들어간 깊이를 0 에서 1 로 구해 RAMP_POWER 제곱하므로, 끝에 가까울수록 한 프레임에 밀리는 거리가 급격히 커진다. 깊이 0.5 에서는 프레임당 3.5px, 1 에서는 14px 다. 열이 여섯 개인 보드를 쓰는 이 장면의 누적면적은 51.0% 이고 움직인 프레임은 23개 중 23개다.

가장자리에서 보드가 움직이는 자동 스크롤
새 창에서 열기

05 · 가장자리에서 보드가 움직이는 자동 스크롤 — 측정 누적면적 50.992% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

EDGE_ZONEMAX_STEPRAMP_POWERCOLUMN_COUNT
  // 핵심: 가장자리 구역에 들어간 깊이(0 에서 1)를 RAMP_POWER 제곱해 한 프레임 이동량으로 쓴다
  function rampStep(x) {
    var left = clamp((EDGE_ZONE - x) / EDGE_ZONE, 0, 1);
    var right = clamp((x - (boardW - EDGE_ZONE)) / EDGE_ZONE, 0, 1);
    return MAX_STEP * (Math.pow(right, RAMP_POWER) - Math.pow(left, RAMP_POWER));
  }

06카드 수가 바뀌는 열 배지

카드를 누르면 카드가 다음 열 끝으로 옮겨지고, 열 머리의 숫자 띠가 새 카드 수 위치로 스프링 이동한다. 숫자가 늘면 띠가 위로, 줄면 아래로 가고 바뀐 순간 배지가 한 번 커졌다 돌아온다. 누적면적은 37.3% 이고 움직인 프레임은 23개 중 23개다.

카드 수가 바뀌는 열 배지
새 창에서 열기

06 · 카드 수가 바뀌는 열 배지 — 측정 누적면적 37.273% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

STRIP_KSTRIP_DAMPDIGIT_HEIGHTPULSE_SCALE
  // 핵심: 열의 카드 수가 목표 숫자 위치이고, 숫자 띠가 그 위치로 스프링 이동한다 (늘면 위로, 줄면 아래로)
  afterStep = function (dt) {
    strips.forEach(function (s, c) {
      var n = cols[c].length;
      if (n !== s.n) {
        s.flash.classList.toggle(CLS.up, n > s.n);
        s.flash.classList.toggle(CLS.down, n < s.n);
        s.n = n;
        s.pulse = 1;
      }
      s.v += (STRIP_K * (n - s.p) - STRIP_DAMP * s.v) * dt;
      s.p += s.v * dt;
      s.pulse *= Math.pow(0.9, dt * 60);
    });
    paintStrips();
  };

07우선순위 정렬이 카드를 옮기는 장면

정렬 버튼을 누르면 카드마다 옮기기 전 좌표와 옮긴 뒤 좌표의 차를 구해 거꾸로 적용한 다음 0 으로 보내고, 순번이 늦을수록 STAGGER_STEP 만큼 늦게 출발한다. 카드가 일곱 장이라 마지막 카드는 420 + 6 × 45 = 690ms 에 도착한다. 누적면적은 27.6% 이고 움직인 프레임은 23개 중 20개다.

우선순위 정렬이 카드를 옮기는 장면
새 창에서 열기

07 · 우선순위 정렬이 카드를 옮기는 장면 — 측정 누적면적 27.56% · 움직인 프레임 20/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

FLIP_DURATIONSTAGGER_STEPPRIORITY_ORDEREASE_POWER
  // 핵심: 옮기기 전 좌표(First)와 옮긴 뒤 좌표(Last)의 차를 거꾸로 적용하고, 순번마다 늦게 시작해 0 으로 보낸다
  function toggleSort() {
    sorted = !sorted;
    cols = sorted ? sortedCols() : initialCols();
    var rank = 0;
    cols.forEach(function (list) {
      list.forEach(function (i) {
        var c = cards[i];
        var t = targetOf(c);
        flips[i] = { fx: c.x, fy: c.y, tx: t.x, ty: t.y, start: nowMs, delay: rank * STAGGER_STEP };
        rank++;
      });
    });
    btn.setAttribute('aria-pressed', sorted ? 'true' : 'false');
  }
  function stepCard(c, dt) {
    var f = flips[c.i];
    var p = clamp((nowMs - f.start - f.delay) / FLIP_DURATION, 0, 1);
    var e = 1 - Math.pow(1 - p, EASE_POWER);
    var invX = f.fx - f.tx;
    var invY = f.fy - f.ty;
    c.x = f.tx + invX * (1 - e);
    c.y = f.ty + invY * (1 - e);
    c.lift = p > 0 && p < 1 ? Math.sin(p * Math.PI) : 0;
    c.sc = 1 + 0.06 * c.lift;
    c.rot = clamp((invX * (1 - e)) / 24, -5, 5);
    c.el.style.setProperty('--lift-px', (c.lift * 12).toFixed(1) + 'px');
  }

08카드 이동으로 길이가 정해지는 진행 링

완료 열의 카드 수를 전체 카드 수로 나눈 값이 호의 길이이고, 카드가 열을 건널 때마다 새 목표를 향해 RING_LERP 만큼씩 따라간다. 4장이라 호는 0·25·50·75·100% 다섯 단계만 가진다. 누적면적은 35.8% 이고 움직인 프레임은 23개 중 23개다.

카드 이동으로 길이가 정해지는 진행 링
새 창에서 열기

08 · 카드 이동으로 길이가 정해지는 진행 링 — 측정 누적면적 35.783% · 움직인 프레임 23/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

RING_RADIUSRING_LERPDONE_COLUMNARC_START
  // 핵심: 호의 길이 = 완료 열 카드 수 ÷ 전체 카드 수. 목표가 바뀌면 RING_LERP 만큼씩 따라간다
  function goal() {
    return cols[DONE_COLUMN].length / cards.length;
  }
  function paintRing() {
    var rest = CIRC * (1 - ring);
    arcA.style.strokeDashoffset = rest.toFixed(2);
    arcB.style.strokeDashoffset = rest.toFixed(2);
    arcB.style.opacity = (ring * ring).toFixed(3);
    pctEl.textContent = Math.round(ring * 100) + '%';
    doneEl.textContent = cols[DONE_COLUMN].length + '/' + cards.length;
  }
  afterStep = function (dt) {
    ring += (goal() - ring) * (1 - Math.pow(1 - RING_LERP, dt * 60));
    paintRing();
  };

09키보드로 옮기는 카드와 안내 문장

Space 나 Enter 로 카드를 집고 방향키로 열과 순서를 옮기며, 옮길 때마다 보조 기술이 읽는 영역에 문장을 써 넣는다. Esc 는 처음 열과 행으로 돌려보내고 취소 문장을 알린다. 누적면적은 38.5% 이고 움직인 프레임은 23개 중 21개다.

키보드로 옮기는 카드와 안내 문장
새 창에서 열기

09 · 키보드로 옮기는 카드와 안내 문장 — 측정 누적면적 38.513% · 움직인 프레임 21/23GODRICH 원작 · zip 에 SCSS·React 포함 · 비밀번호 보기

LIVE_REGIONMOVE_STEPFOCUS_RINGANNOUNCE_TEXT
  function press(k, c) {
    var s = locate(c.i);
    if (k === ' ' || k === 'Enter') {
      if (picked === c) {
        setPicked(null);
        say('drop', c);
      } else if (!picked) {
        origin = { c: s.c, r: s.r };
        setPicked(c);
        say('pick', c);
      }
      return;
    }
    if (picked !== c) return;
    if (k === 'Escape') {
      moveCard(c, origin.c, Math.min(origin.r, cols[origin.c].length));
      setPicked(null);
      say('cancel', c);
      return;
    }
    var nc = s.c;
    var nr = s.r;
    if (k === 'ArrowRight') nc = Math.min(COLS - 1, s.c + MOVE_STEP);
    if (k === 'ArrowLeft') nc = Math.max(0, s.c - MOVE_STEP);
    if (k === 'ArrowDown') nr = s.r + MOVE_STEP;
    if (k === 'ArrowUp') nr = s.r - MOVE_STEP;
    nr = nc === s.c ? clamp(nr, 0, cols[nc].length - 1) : Math.min(nr, cols[nc].length);
    if (nc === s.c && nr === s.r) return;
    moveCard(c, nc, nr);
    say('move', c);
  }

값이 장면을 바꾸는 곳 — 추종 계수·간격 크기·WIP 한도

세 상수가 카드가 커서를 따라오는 속도, 빈 자리의 크기, 열이 막히는 시점을 정한다. 표에 적은 수치는 시연을 렌더한 프레임을 하나씩 비교해 장면마다 따로 얻은 값이다.

항목 장면 누적면적 % 강도 평균 움직인 프레임
01 커서 추종과 기울기 36.0% 66.0 23/23
02 놓을 자리 열기 22.6% 138.0 23/23
03 놓은 뒤 안착 27.4% 76.0 20/23
04 WIP 한도 압력 29.1% 116.9 23/23
05 가장자리 자동 스크롤 51.0% 71.2 23/23
06 열 카드 수 배지 37.3% 142.4 23/23
07 우선순위 정렬 27.6% 78.9 20/23
08 진행 링 35.8% 151.0 23/23
09 키보드 이동 38.5% 94.2 21/23

추종 계수가 카드와 커서의 거리를 정한다

FOLLOW_LERP 0.25 면 거리의 90% 를 좁히는 데 8프레임, 약 133ms 가 든다. 0.1 로 낮추면 약 22프레임, 약 364ms 가 들어서, 같은 속도로 끌어도 카드가 커서보다 한참 뒤에 따라온다.

간격 크기가 빈 자리의 모양을 정한다

SLOT_GAP 은 카드 한 단 기준이라 1 이면 아래 카드가 28px 또는 42px 내려간다. 0.5 면 14px 또는 21px 만 내려가 카드 높이 24px 또는 36px 보다 작아지므로 자리 표시와 겹친다.

WIP 한도가 열이 막히는 시점을 정한다

WIP_LIMIT 4 에서 가운데 열의 카드는 처음 3장이라 한 장은 들어가고 두 번째는 튕겨 나온다. 값을 2 로 낮추면 처음부터 그 열이 가득 차 압력 게이지가 끝까지 올라 있다.

어디서 깨지는가 — 좁은 화면·빈 열에 놓을 때·키보드만 쓸 때

열이 다섯 장이 되면 아래가 잘린다

보드 높이는 카드 네 줄을 전제로 4U 단위로 고정되어 있다. 열에 다섯 번째 카드가 오면 그 자리가 보드 높이를 넘어 카드 아래쪽 24px 가 잘린다. 02 처럼 자리를 여는 장면에서는 네 장이 찬 열에 놓을 자리가 다섯째 슬롯으로 열리므로, 카드를 늘릴 때는 보드 높이 줄 수를 같이 올려야 한다.

화면 높이로 단위가 바뀐다

칸 단위 U 는 창의 높이가 260px 미만이면 4px, 이상이면 6px 이다. 임베드 높이를 낮게 잡으면 카드와 스프링 거리가 함께 줄어드니, 고정 픽셀로 쓴 다른 요소는 같이 줄지 않는다.

빈 열에 놓으면 행 0 으로 들어간다

09 에서 키보드로 빈 열로 옮기면 행이 0 으로 잡히고, 같은 열 안에서 위아래 키는 끝에서 멈춘다. 방향키로 열을 옮길 때 다른 열이 더 짧으면 행이 그 열 길이로 잘리므로, 카드가 예상보다 위에 놓일 수 있다.

높이 차가 크면 오버슈트가 잘린다

03 은 기본 HOVER_PX 18 에서 넘침이 약 4.5px 라 OVERSHOOT_CAP 6 이 걸리지 않는다. HOVER_PX 를 24px 보다 크게 잡으면 캡이 걸려 스프링이 도중에 잘려 보인다.

한도가 낮으면 같은 열 재정렬도 막힌다

WIP_LIMIT 를 2 로 내리면 가운데 열 카드를 집어도 뺀 뒤 2장이 남아 한도에 차므로, 같은 열 안에서 순서를 바꾸는 것도 막힌다. 한도 검사가 집은 카드를 뺀 열 길이로 이뤄지고 원래 열을 예외로 두지 않기 때문이니, 재정렬을 허용하려면 원래 열 위에서는 검사를 건너뛰어야 한다.

칸반 아홉 장면의 소스와 스타일을 파일째 받아 상수를 직접 바꿔 보려면, 아래 압축 파일을 비밀번호 n98rrum3 로 열어 쓰면 된다.

변형 세 가지와 접근성

항목 상수 값 느낌
01 FOLLOW_LERP 0.25 → 0.1 카드가 커서보다 약 364ms 뒤에 따라오고 기울기도 약해진다
02 SLOT_GAP 1 → 0.5 아래 카드가 14·21px 만 밀려 빈 자리가 카드 높이보다 좁고 자리 표시와 겹친다
04 WIP_LIMIT 4 → 2 가운데 열이 처음부터 막혀 어느 카드도 들어가지 못한다

01~05 와 07 의 카드는 div role="button" 에 tabindex 를 준 요소이고, 06·08·09 의 카드는 <button> 이다. 07 은 정렬 버튼만 <button> 이라 키 입력은 그 버튼이 받는다. prefers-reduced-motion 이 reduce 이면 프레임 루프도 자동 시연도 돌리지 않고, 입력이 올 때마다 모든 카드를 목표 좌표에 바로 놓는다. reduce 에서 05 는 가장자리에 가만히 있으면 스크롤되지 않고 포인터가 움직일 때만 한 걸음씩 밀리며, 09 는 움직임이 즉시여도 안내 문장은 같다. 09 에만 있는 안내 영역은 aria-live 로 보조 기술에 읽힌다.

html css kanban board 는 카드 순서만 바꾸면 충분한가?

카드를 집고 놓는 일이 목적이면 순서만 바꿔도 된다. 다만 그 방식은 집은 카드가 커서와 따로 놀고, 놓을 자리가 따로 안 열리고, 열 한도도 없다. 01 은 추종 계수, 02 는 간격, 04 는 압력으로 그 빈틈을 채우니, 보드를 소개하거나 직접 쓰게 할 때 그 세 겹을 골라 얹으면 된다. 포인터 이벤트는 Pointer events 문서가 기준이다.

css kanban board 전에 자주 묻는 질문

kanban board html css js 로 만들면 라이브러리가 꼭 필요한가요?

필요하지 않다. 이 편의 아홉 장면은 일반 JS 와 CSS 만 쓰고, 스프링과 보간도 직접 쓴 몇 줄이다.

tailwind css kanban board 에도 같은 계산을 쓸 수 있나요?

계산은 좌표를 정하는 JS 라 클래스 이름과 무관하다. 카드 좌표를 transform 값으로 쓰는 부분만 가져가면 Tailwind 클래스로 꾸민 카드에도 같은 좌표 계산을 얹을 수 있다.

kanban board template html css 로 쓰려면 무엇부터 바꾸나요?

열 개수와 카드 수, 그리고 보드 높이 줄 수를 먼저 바꾼다. 그다음 01 의 FOLLOW_LERP 와 04 의 WIP_LIMIT 를 서비스에 맞는 값으로 조정하면 된다.

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

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