Web InnoventixFreeCode

Kanban Column

Original · free

single kanban column with add-card

byWeb InnoventixReact + Tailwind
kanbancolumn
Open

Copy prompt gives Claude Code, Cursor or v0 a ready-to-paste prompt that recreates this exact component.

npx shadcn@latest add https://webinnoventix.com/r/kanban-column.json
kanban-column.tsx
"use client";

import {
  useEffect,
  useId,
  useRef,
  useState,
  type KeyboardEvent,
} from "react";
import { AnimatePresence, motion, useReducedMotion } from "motion/react";

type Priority = "high" | "medium" | "low";

type Card = {
  id: string;
  title: string;
  priority: Priority;
  assignee: string;
  tag: string;
  due: string;
  comments: number;
};

const WIP_LIMIT = 6;

const PRIORITY_LABEL: Record<Priority, string> = {
  high: "High",
  medium: "Medium",
  low: "Low",
};

const PRIORITY_CHIP: Record<Priority, string> = {
  high: "bg-rose-100 text-rose-700 ring-rose-600/20 dark:bg-rose-500/15 dark:text-rose-300 dark:ring-rose-400/25",
  medium:
    "bg-amber-100 text-amber-800 ring-amber-600/20 dark:bg-amber-500/15 dark:text-amber-300 dark:ring-amber-400/25",
  low: "bg-sky-100 text-sky-700 ring-sky-600/20 dark:bg-sky-500/15 dark:text-sky-300 dark:ring-sky-400/25",
};

const PRIORITY_DOT: Record<Priority, string> = {
  high: "bg-rose-500",
  medium: "bg-amber-500",
  low: "bg-sky-500",
};

const AVATAR_TONES = [
  "bg-indigo-500",
  "bg-violet-500",
  "bg-emerald-600",
  "bg-sky-600",
  "bg-amber-600",
  "bg-rose-500",
];

const INITIAL_CARDS: Card[] = [
  {
    id: "kbc-seed-1",
    title: "Migrate the checkout form to the new validation schema",
    priority: "high",
    assignee: "Rina Okafor",
    tag: "Payments",
    due: "Fri 24",
    comments: 4,
  },
  {
    id: "kbc-seed-2",
    title: "Cut LCP on the pricing page below 2.5s on 4G",
    priority: "high",
    assignee: "Dan Mercer",
    tag: "Performance",
    due: "Mon 27",
    comments: 2,
  },
  {
    id: "kbc-seed-3",
    title: "Write empty-state copy for the reports dashboard",
    priority: "low",
    assignee: "Aisha Verma",
    tag: "Content",
    due: "Wed 29",
    comments: 7,
  },
  {
    id: "kbc-seed-4",
    title: "Replace the legacy toast with the alert primitive",
    priority: "medium",
    assignee: "Tom Byrne",
    tag: "Design system",
    due: "Thu 30",
    comments: 1,
  },
];

function toneFor(name: string): string {
  let sum = 0;
  for (let i = 0; i < name.length; i += 1) sum += name.charCodeAt(i);
  return AVATAR_TONES[sum % AVATAR_TONES.length];
}

function initialsFor(name: string): string {
  return name
    .split(" ")
    .filter(Boolean)
    .map((part) => part[0])
    .join("")
    .slice(0, 2)
    .toUpperCase();
}

const FOCUS_RING =
  "outline-none focus-visible:ring-2 focus-visible:ring-indigo-500 focus-visible:ring-offset-2 focus-visible:ring-offset-slate-100 dark:focus-visible:ring-indigo-400 dark:focus-visible:ring-offset-slate-900";

function ChevronUpIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5">
      <path
        d="M4 10l4-4 4 4"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.75"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ChevronDownIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5">
      <path
        d="M4 6l4 4 4-4"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.75"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function TrashIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5">
      <path
        d="M3 4.5h10M6.5 4.5V3h3v1.5M4.5 4.5l.6 8a1 1 0 001 .9h3.8a1 1 0 001-.9l.6-8M6.8 7v4M9.2 7v4"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.5"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function PlusIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-4 w-4">
      <path
        d="M8 3.5v9M3.5 8h9"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.75"
        strokeLinecap="round"
      />
    </svg>
  );
}

function CommentIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5">
      <path
        d="M13.5 8.5a4.5 4.5 0 01-4.5 4.5H6l-3 2v-2.6A4.5 4.5 0 013 8.5v-1A4.5 4.5 0 017.5 3h1.5a4.5 4.5 0 014.5 4.5z"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinejoin="round"
      />
    </svg>
  );
}

function ClockIcon() {
  return (
    <svg viewBox="0 0 16 16" aria-hidden="true" className="h-3.5 w-3.5">
      <circle
        cx="8"
        cy="8"
        r="5.25"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.4"
      />
      <path
        d="M8 5.2V8l2 1.4"
        fill="none"
        stroke="currentColor"
        strokeWidth="1.4"
        strokeLinecap="round"
        strokeLinejoin="round"
      />
    </svg>
  );
}

export default function KanbanColumn() {
  const reduce = useReducedMotion();
  const baseId = useId();

  const [cards, setCards] = useState<Card[]>(INITIAL_CARDS);
  const [composerOpen, setComposerOpen] = useState(false);
  const [draft, setDraft] = useState("");
  const [draftPriority, setDraftPriority] = useState<Priority>("medium");
  const [status, setStatus] = useState("");
  const [lastAdded, setLastAdded] = useState<string | null>(null);

  const nextId = useRef(1);
  const pendingFocus = useRef<string | null>(null);
  const returnFocus = useRef(false);
  const btnRefs = useRef<Map<string, HTMLButtonElement>>(new Map());
  const addBtnRef = useRef<HTMLButtonElement | null>(null);
  const textareaRef = useRef<HTMLTextAreaElement | null>(null);
  const listEndRef = useRef<HTMLDivElement | null>(null);

  const count = cards.length;
  const over = count > WIP_LIMIT;
  const atLimit = count === WIP_LIMIT;
  const fill = Math.min(100, Math.round((count / WIP_LIMIT) * 100));

  useEffect(() => {
    if (composerOpen) {
      textareaRef.current?.focus();
      return;
    }
    if (returnFocus.current) {
      returnFocus.current = false;
      addBtnRef.current?.focus();
    }
  }, [composerOpen]);

  useEffect(() => {
    const key = pendingFocus.current;
    if (!key) return;
    pendingFocus.current = null;
    const el = btnRefs.current.get(key);
    if (el && !el.disabled) {
      el.focus();
      return;
    }
    const [id, which] = key.split(":");
    const fallback = btnRefs.current.get(
      `${id}:${which === "up" ? "down" : "up"}`,
    );
    if (fallback && !fallback.disabled) fallback.focus();
  }, [cards]);

  const move = (index: number, dir: -1 | 1) => {
    const target = index + dir;
    if (target < 0 || target >= cards.length) return;
    const next = cards.slice();
    const [item] = next.splice(index, 1);
    next.splice(target, 0, item);
    setCards(next);
    setStatus(
      `"${item.title}" moved to position ${target + 1} of ${next.length}.`,
    );
    pendingFocus.current = `${item.id}:${dir === -1 ? "up" : "down"}`;
  };

  const remove = (index: number) => {
    const card = cards[index];
    const next = cards.filter((_, i) => i !== index);
    setCards(next);
    setStatus(
      `"${card.title}" removed. ${next.length} ${next.length === 1 ? "card" : "cards"} left in this column.`,
    );
    if (next.length > 0) {
      const neighbour = next[Math.min(index, next.length - 1)];
      pendingFocus.current = `${neighbour.id}:del`;
      return;
    }
    if (composerOpen) textareaRef.current?.focus();
    else addBtnRef.current?.focus();
  };

  const submit = () => {
    const title = draft.trim();
    if (!title) return;
    const id = `kbc-new-${nextId.current}`;
    nextId.current += 1;
    const card: Card = {
      id,
      title,
      priority: draftPriority,
      assignee: "You",
      tag: "Untriaged",
      due: "No date",
      comments: 0,
    };
    setCards((prev) => [...prev, card]);
    setDraft("");
    setLastAdded(id);
    setStatus(
      `"${title}" added to In Progress as ${PRIORITY_LABEL[draftPriority].toLowerCase()} priority. ${count + 1} cards in this column.`,
    );
    listEndRef.current?.scrollIntoView({
      block: "nearest",
      behavior: reduce ? "auto" : "smooth",
    });
    textareaRef.current?.focus();
  };

  const closeComposer = () => {
    returnFocus.current = true;
    setComposerOpen(false);
    setDraft("");
  };

  const onDraftKeyDown = (e: KeyboardEvent<HTMLTextAreaElement>) => {
    if (e.key === "Enter" && !e.shiftKey) {
      e.preventDefault();
      submit();
      return;
    }
    if (e.key === "Escape") {
      e.preventDefault();
      closeComposer();
    }
  };

  const onCardKeyDown = (e: KeyboardEvent<HTMLLIElement>, index: number) => {
    if (!e.altKey) return;
    if (e.key === "ArrowUp") {
      e.preventDefault();
      move(index, -1);
    } else if (e.key === "ArrowDown") {
      e.preventDefault();
      move(index, 1);
    }
  };

  const setBtnRef = (key: string) => (el: HTMLButtonElement | null) => {
    if (el) btnRefs.current.set(key, el);
    else btnRefs.current.delete(key);
  };

  return (
    <section className="relative w-full bg-slate-100 px-4 py-20 sm:py-28 dark:bg-slate-950">
      <style>{`
        @keyframes kbcol-sheen {
          0% { transform: translateX(-110%); }
          100% { transform: translateX(210%); }
        }
        @keyframes kbcol-flash {
          0% { box-shadow: 0 0 0 0 rgba(99, 102, 241, 0.55); }
          100% { box-shadow: 0 0 0 9px rgba(99, 102, 241, 0); }
        }
        .kbcol-sheen { animation: kbcol-sheen 3.6s cubic-bezier(0.4, 0, 0.2, 1) infinite; }
        .kbcol-flash { animation: kbcol-flash 850ms ease-out 1; }
        @media (prefers-reduced-motion: reduce) {
          .kbcol-sheen, .kbcol-flash { animation: none !important; }
        }
      `}</style>

      <div className="mx-auto w-full max-w-md">
        <div className="flex flex-col rounded-2xl border border-slate-200 bg-slate-50 shadow-sm dark:border-slate-800 dark:bg-slate-900">
          <div className="px-5 pt-5">
            <div className="flex items-start justify-between gap-3">
              <div>
                <h2
                  id={`${baseId}-title`}
                  className="text-base font-semibold tracking-tight text-slate-900 dark:text-slate-50"
                >
                  In Progress
                </h2>
                <p className="mt-0.5 text-xs text-slate-500 dark:text-slate-400">
                  Sprint 24 &middot; Web platform
                </p>
              </div>
              <span
                className={`inline-flex h-6 min-w-[3.25rem] items-center justify-center rounded-full px-2 text-xs font-semibold tabular-nums ring-1 ${
                  over
                    ? "bg-rose-100 text-rose-700 ring-rose-600/20 dark:bg-rose-500/15 dark:text-rose-300 dark:ring-rose-400/25"
                    : "bg-slate-200 text-slate-700 ring-slate-900/5 dark:bg-slate-800 dark:text-slate-300 dark:ring-white/10"
                }`}
              >
                {count} / {WIP_LIMIT}
              </span>
            </div>

            <div
              className="relative mt-4 h-1 overflow-hidden rounded-full bg-slate-200 dark:bg-slate-800"
              role="progressbar"
              aria-valuenow={count}
              aria-valuemin={0}
              aria-valuemax={WIP_LIMIT}
              aria-label={`Work in progress limit: ${count} of ${WIP_LIMIT} cards used`}
            >
              <div
                className={`h-full rounded-full transition-[width] duration-300 ease-out ${
                  over
                    ? "bg-rose-500"
                    : atLimit
                      ? "bg-amber-500"
                      : "bg-indigo-500"
                }`}
                style={{ width: `${fill}%` }}
              />
              {!over && (
                <div className="pointer-events-none absolute inset-y-0 left-0 w-1/3">
                  <div className="kbcol-sheen h-full w-1/2 bg-gradient-to-r from-transparent via-white/70 to-transparent dark:via-white/30" />
                </div>
              )}
            </div>

            <p
              className={`mt-2 text-[11px] font-medium ${
                over
                  ? "text-rose-600 dark:text-rose-400"
                  : atLimit
                    ? "text-amber-700 dark:text-amber-400"
                    : "text-slate-500 dark:text-slate-400"
              }`}
            >
              {over
                ? `Over the WIP limit by ${count - WIP_LIMIT}. Ship something before pulling more.`
                : atLimit
                  ? "At the WIP limit. Finish a card before starting another."
                  : `${WIP_LIMIT - count} slot${WIP_LIMIT - count === 1 ? "" : "s"} left before the WIP limit.`}
            </p>
          </div>

          <ul
            aria-labelledby={`${baseId}-title`}
            className="mt-4 flex max-h-[26rem] flex-col gap-2.5 overflow-y-auto px-3 pb-1"
          >
            <AnimatePresence initial={false}>
              {cards.map((card, i) => (
                <motion.li
                  key={card.id}
                  layout={!reduce}
                  initial={reduce ? false : { opacity: 0, y: 10, scale: 0.98 }}
                  animate={{ opacity: 1, y: 0, scale: 1 }}
                  exit={
                    reduce
                      ? { opacity: 0 }
                      : { opacity: 0, x: -14, scale: 0.97 }
                  }
                  transition={
                    reduce
                      ? { duration: 0 }
                      : { type: "spring", stiffness: 460, damping: 36, mass: 0.7 }
                  }
                  onKeyDown={(e) => onCardKeyDown(e, i)}
                  className={`group relative rounded-xl border border-slate-200 bg-white p-3 shadow-sm transition-colors hover:border-slate-300 focus-within:border-indigo-400 dark:border-slate-700/70 dark:bg-slate-800 dark:hover:border-slate-600 dark:focus-within:border-indigo-500 ${
                    card.id === lastAdded ? "kbcol-flash" : ""
                  }`}
                >
                  <span
                    aria-hidden="true"
                    className={`absolute left-0 top-3 bottom-3 w-0.5 rounded-full ${PRIORITY_DOT[card.priority]}`}
                  />

                  <div className="flex items-start justify-between gap-2">
                    <div className="flex flex-wrap items-center gap-1.5">
                      <span
                        className={`inline-flex items-center rounded-md px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wide ring-1 ring-inset ${PRIORITY_CHIP[card.priority]}`}
                      >
                        {PRIORITY_LABEL[card.priority]}
                      </span>
                      <span className="inline-flex items-center rounded-md bg-slate-100 px-1.5 py-0.5 text-[10px] font-medium text-slate-600 ring-1 ring-inset ring-slate-900/5 dark:bg-slate-700/60 dark:text-slate-300 dark:ring-white/10">
                        {card.tag}
                      </span>
                    </div>

                    <div className="flex shrink-0 items-center gap-0.5 opacity-60 transition-opacity group-hover:opacity-100 group-focus-within:opacity-100">
                      <button
                        type="button"
                        ref={setBtnRef(`${card.id}:up`)}
                        onClick={() => move(i, -1)}
                        disabled={i === 0}
                        aria-label={`Move "${card.title}" up to position ${i}`}
                        className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:pointer-events-none disabled:opacity-30 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                      >
                        <ChevronUpIcon />
                      </button>
                      <button
                        type="button"
                        ref={setBtnRef(`${card.id}:down`)}
                        onClick={() => move(i, 1)}
                        disabled={i === cards.length - 1}
                        aria-label={`Move "${card.title}" down to position ${i + 2}`}
                        className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-slate-100 hover:text-slate-900 disabled:pointer-events-none disabled:opacity-30 dark:text-slate-400 dark:hover:bg-slate-700 dark:hover:text-slate-100 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                      >
                        <ChevronDownIcon />
                      </button>
                      <button
                        type="button"
                        ref={setBtnRef(`${card.id}:del`)}
                        onClick={() => remove(i)}
                        aria-label={`Delete "${card.title}"`}
                        className={`inline-flex h-6 w-6 items-center justify-center rounded-md text-slate-500 transition-colors hover:bg-rose-50 hover:text-rose-600 dark:text-slate-400 dark:hover:bg-rose-500/15 dark:hover:text-rose-400 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                      >
                        <TrashIcon />
                      </button>
                    </div>
                  </div>

                  <p className="mt-2 text-sm font-medium leading-snug text-slate-800 dark:text-slate-100">
                    {card.title}
                  </p>

                  <div className="mt-3 flex items-center justify-between gap-2">
                    <div className="flex items-center gap-2">
                      <span
                        aria-hidden="true"
                        className={`inline-flex h-6 w-6 items-center justify-center rounded-full text-[10px] font-bold text-white ${toneFor(card.assignee)}`}
                      >
                        {initialsFor(card.assignee)}
                      </span>
                      <span className="text-xs text-slate-500 dark:text-slate-400">
                        {card.assignee}
                      </span>
                    </div>
                    <div className="flex items-center gap-3 text-slate-400 dark:text-slate-500">
                      <span className="inline-flex items-center gap-1 text-[11px] tabular-nums">
                        <CommentIcon />
                        <span className="sr-only">Comments:</span>
                        {card.comments}
                      </span>
                      <span className="inline-flex items-center gap-1 text-[11px]">
                        <ClockIcon />
                        <span className="sr-only">Due:</span>
                        {card.due}
                      </span>
                    </div>
                  </div>
                </motion.li>
              ))}
            </AnimatePresence>

            {cards.length === 0 && (
              <li className="rounded-xl border border-dashed border-slate-300 px-4 py-8 text-center text-sm text-slate-500 dark:border-slate-700 dark:text-slate-400">
                Nothing in progress. Pull a card from Backlog or add one below.
              </li>
            )}
            <div ref={listEndRef} aria-hidden="true" />
          </ul>

          <div className="border-t border-slate-200 p-3 dark:border-slate-800">
            {composerOpen ? (
              <div className="rounded-xl border border-slate-200 bg-white p-3 shadow-sm dark:border-slate-700 dark:bg-slate-800">
                <label
                  htmlFor={`${baseId}-draft`}
                  className="block text-xs font-medium text-slate-600 dark:text-slate-300"
                >
                  New card title
                </label>
                <textarea
                  id={`${baseId}-draft`}
                  ref={textareaRef}
                  rows={2}
                  maxLength={90}
                  value={draft}
                  onChange={(e) => setDraft(e.target.value)}
                  onKeyDown={onDraftKeyDown}
                  placeholder="Audit the onboarding emails for broken links"
                  aria-describedby={`${baseId}-hint`}
                  className={`mt-1.5 w-full resize-none rounded-lg border border-slate-300 bg-white px-2.5 py-2 text-sm text-slate-900 placeholder:text-slate-400 dark:border-slate-600 dark:bg-slate-900 dark:text-slate-100 dark:placeholder:text-slate-500 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                />

                <fieldset className="mt-3">
                  <legend className="text-xs font-medium text-slate-600 dark:text-slate-300">
                    Priority
                  </legend>
                  <div className="mt-1.5 flex gap-1.5">
                    {(["high", "medium", "low"] as const).map((p) => (
                      <div key={p}>
                        <input
                          type="radio"
                          id={`${baseId}-p-${p}`}
                          name={`${baseId}-priority`}
                          value={p}
                          checked={draftPriority === p}
                          onChange={() => setDraftPriority(p)}
                          className="peer sr-only"
                        />
                        <label
                          htmlFor={`${baseId}-p-${p}`}
                          className="inline-flex cursor-pointer items-center gap-1.5 rounded-lg border border-slate-300 px-2.5 py-1 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-50 peer-checked:border-indigo-500 peer-checked:bg-indigo-50 peer-checked:text-indigo-700 peer-focus-visible:ring-2 peer-focus-visible:ring-indigo-500 peer-focus-visible:ring-offset-2 peer-focus-visible:ring-offset-white dark:border-slate-600 dark:text-slate-300 dark:hover:bg-slate-700/50 dark:peer-checked:border-indigo-400 dark:peer-checked:bg-indigo-500/15 dark:peer-checked:text-indigo-300 dark:peer-focus-visible:ring-indigo-400 dark:peer-focus-visible:ring-offset-slate-800"
                        >
                          <span
                            aria-hidden="true"
                            className={`h-1.5 w-1.5 rounded-full ${PRIORITY_DOT[p]}`}
                          />
                          {PRIORITY_LABEL[p]}
                        </label>
                      </div>
                    ))}
                  </div>
                </fieldset>

                <div className="mt-3 flex items-center justify-between gap-2">
                  <p
                    id={`${baseId}-hint`}
                    className="text-[11px] text-slate-400 dark:text-slate-500"
                  >
                    Enter to add &middot; Esc to cancel &middot;{" "}
                    <span className="tabular-nums">{draft.length}/90</span>
                  </p>
                  <div className="flex items-center gap-1.5">
                    <button
                      type="button"
                      onClick={closeComposer}
                      className={`rounded-lg px-2.5 py-1.5 text-xs font-medium text-slate-600 transition-colors hover:bg-slate-100 dark:text-slate-300 dark:hover:bg-slate-700 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                    >
                      Cancel
                    </button>
                    <button
                      type="button"
                      onClick={submit}
                      disabled={draft.trim().length === 0}
                      className={`rounded-lg bg-indigo-600 px-3 py-1.5 text-xs font-semibold text-white shadow-sm transition-colors hover:bg-indigo-500 disabled:cursor-not-allowed disabled:opacity-40 disabled:hover:bg-indigo-600 dark:bg-indigo-500 dark:hover:bg-indigo-400 ${FOCUS_RING} focus-visible:ring-offset-white dark:focus-visible:ring-offset-slate-800`}
                    >
                      Add card
                    </button>
                  </div>
                </div>
              </div>
            ) : (
              <button
                type="button"
                ref={addBtnRef}
                onClick={() => setComposerOpen(true)}
                className={`flex w-full items-center justify-center gap-1.5 rounded-xl border border-dashed border-slate-300 px-3 py-2.5 text-sm font-medium text-slate-600 transition-colors hover:border-indigo-400 hover:bg-white hover:text-indigo-700 dark:border-slate-700 dark:text-slate-300 dark:hover:border-indigo-500 dark:hover:bg-slate-800 dark:hover:text-indigo-300 ${FOCUS_RING}`}
              >
                <PlusIcon />
                Add a card
              </button>
            )}
          </div>
        </div>

        <p className="mt-4 text-center text-xs text-slate-500 dark:text-slate-400">
          Tip: focus a card and press{" "}
          <kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-700 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200">
            Alt
          </kbd>{" "}
          +{" "}
          <kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-700 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200">
            &uarr;
          </kbd>{" "}
          /{" "}
          <kbd className="rounded border border-slate-300 bg-white px-1 py-0.5 font-sans text-[10px] font-medium text-slate-700 dark:border-slate-600 dark:bg-slate-800 dark:text-slate-200">
            &darr;
          </kbd>{" "}
          to reorder it.
        </p>

        <p aria-live="polite" className="sr-only">
          {status}
        </p>
      </div>
    </section>
  );
}

Dependencies

motion

Licence

Built by Web Innoventix. Free for personal and commercial use, no attribution required.

Built by Web Innoventix

Need a custom interface, a full website, or SEO that gets you cited by AI? We design, build and rank it end to end.

Get a free quote

Similar components

Browse all →