Tips & Tricks (Mis à jour: 02/06/2026)

Implémenter le drag and drop avec Claude Code

Drag and drop avec Claude Code: React, clavier, accessibilité, risques, tests et CTA.

Implémenter le drag and drop avec Claude Code

Le drag and drop est une UI qui rate plus facilement qu’il n’y paraît.

Même si cela ressemble à « il suffit d’attraper une carte et de la déplacer », il faut en réalité penser à la souris, au tactile, au clavier, au lecteur d’écran, à la sauvegarde après réordonnancement, au dépôt dans une colonne vide et à l’annulation d’une fausse manœuvre. Si vous déléguez tout à Claude Code, un code qui « marche » sort vite, mais pour atteindre une qualité publiable, il est essentiel de cadrer les conditions dès le départ.

Dans cet article, nous construisons un tableau kanban en drag and drop avec React + TypeScript + dnd-kit. Pour les débutants, j’explique d’abord le sens de la source de glissement, de la cible de dépôt, des capteurs et de la mise à jour d’état, puis je rassemble les consignes à passer à Claude Code, le code d’implémentation, les vérifications d’accessibilité, les pièges et le tunnel de conversion.

À décider d’abord

Pour une implémentation de drag and drop, décidez en premier « ce qu’on déplace » et « où l’on sauvegarde ». Si ce point reste flou, Claude Code tend à générer du code qui réordonne par manipulation directe du DOM, ou des cartes non manipulables au clavier.

Pour un débutant, il suffit de retenir ces quatre mots.

TermeSens simpleOù le voir dans le code
Source de glissementL’élément qu’on attrape et déplaceLa carte de tâche
Cible de dépôtL’endroit où l’on peut poserColonne, avant ou après une carte
sensorLe rôle qui détecte le mode de saisieSouris, tactile, clavier
stateL’ordre correct affiché à l’écranLe useState de React

Comme schéma conceptuel, le flux est le suivant.

Action de l’utilisateur → le sensor détecte → DndContext la transforme en événement → mise à jour du state React → la colonne se redessine.

L’idée clé est de tenir le state pour vérité, plutôt que de réordonner directement le DOM. En React, plutôt que « faire bouger l’écran », pensez « l’écran change comme conséquence d’un changement d’ordre des données » : il y a moins de bugs.

Cas d’usage exploitables

Le drag and drop construit avec Claude Code ne doit pas s’arrêter à une simple démo : concevez jusqu’à son intégration dans des écrans métier. Voici les cas d’usage représentatifs.

Cas d’usageExemplePoint d’attention
Réordonnancement de tâchesToDo, liste d’apprentissage, ordre de publication d’articlesDécider du moment de sauvegarde après réordonnancement
Tableau kanbanAffaires commerciales, tickets de dev, candidats au recrutementNe pas oublier le dépôt dans une colonne vide
Téléversement de fichiersDépôt d’images, PDF, CSVValider la taille et le type MIME des fichiers
Édition de dashboardDisposition des widgets et graphiquesSur mobile, séparer le mode édition
Constructeur de formulaireRéordonnancement des champsEmpêcher la suppression d’un champ obligatoire

Le plus réutilisable est le tableau kanban. Comme il couvre le déplacement entre colonnes, le réordonnancement dans une même colonne, la colonne vide et le clavier, il s’applique aussi aux autres usages.

API native ou dnd-kit ?

Le navigateur dispose d’une fonctionnalité native décrite dans la HTML Drag and Drop API de MDN. Elle reste valable pour des usages comme déposer un fichier du bureau vers le navigateur. En revanche, pour le réordonnancement de listes React ou un kanban, le différentiel d’événements, le tactile et le clavier deviennent pénibles.

OptionSituations adaptéesPoints faibles
HTML Drag and Drop APIDépôt de fichiers, échange de données avec une app externeContrôle difficile sur tactile et pour le réordonnancement fin
dnd-kitDéplacement de cartes React, listes, kanban, opérations accessiblesNécessite de comprendre les concepts de la bibliothèque

L’exemple de cet article suit l’approche du Getting started de dnd-kit et du guide Accessibilité de dnd-kit, avec PointerSensor et KeyboardSensor. Réserver l’API native de MDN au téléversement de fichiers et dnd-kit au réordonnancement dans l’app est, en pratique, le partage le plus stable.

Les consignes à Claude Code

À Claude Code, ne dites pas « fais un drag and drop » : transmettez le mode de saisie, la sauvegarde, l’accessibilité et la méthode de vérification. Par exemple, en collant le JSON suivant comme description de tâche, on obtient un diff plus facile à relire.

{
  "goal": "Build an accessible React drag-and-drop kanban board with TypeScript.",
  "stack": ["React", "TypeScript", "@dnd-kit/core", "@dnd-kit/sortable"],
  "requirements": [
    "Support pointer and keyboard operation",
    "Move cards within a column and across columns",
    "Keep React state as the source of truth",
    "Show visible focus styles",
    "Do not persist data until drag end"
  ],
  "verification": [
    "Move a card with mouse or trackpad",
    "Move a card with keyboard",
    "Drop into an empty column",
    "Cancel a drag with Escape"
  ]
}

Pour les bases, lire aussi le guide de développement React, l’accessibilité avec Claude Code et, côté tests, le guide de stratégie de tests aide à mieux comprendre.

Mise en place

On ajoute dnd-kit au template Vite React + TypeScript. Pour une intégration dans une app existante, les dépendances sont identiques.

npm create vite@latest dnd-demo -- --template react-ts
cd dnd-demo
npm install @dnd-kit/core @dnd-kit/sortable @dnd-kit/utilities
npm run dev

Modèle de données

On crée src/taskModel.ts. Plutôt que de regrouper les tâches dans un seul tableau, tenir un tableau par colonne est plus facile à suivre pour un débutant.

export type ColumnId = "backlog" | "doing" | "done";

export type Task = {
  id: string;
  title: string;
  note: string;
};

export type BoardState = Record<ColumnId, Task[]>;

export const columnOrder: ColumnId[] = ["backlog", "doing", "done"];

export const columnLabels: Record<ColumnId, string> = {
  backlog: "Backlog",
  doing: "Doing",
  done: "Done",
};

export const initialBoard: BoardState = {
  backlog: [
    { id: "task-1", title: "Write acceptance criteria", note: "Clarify done state before coding." },
    { id: "task-2", title: "Create upload fallback", note: "Keep file input for keyboard users." },
  ],
  doing: [
    { id: "task-3", title: "Build sortable cards", note: "Use dnd-kit sensors and state updates." },
  ],
  done: [
    { id: "task-4", title: "Check MDN guidance", note: "Confirm native drag events and file behavior." },
  ],
};

Composant React fonctionnel

On remplace src/App.tsx par le contenu suivant. DndContext gère l’ensemble du drag and drop, SortableContext le réordonnancement dans une colonne, useDroppable les emplacements de dépôt (y compris la colonne vide) et useSortable le déplacement de chaque carte.

import { useRef, useState, type CSSProperties } from "react";
import {
  DndContext,
  KeyboardSensor,
  PointerSensor,
  closestCorners,
  useDroppable,
  useSensor,
  useSensors,
  type DragEndEvent,
  type DragOverEvent,
  type DragStartEvent,
} from "@dnd-kit/core";
import {
  SortableContext,
  arrayMove,
  sortableKeyboardCoordinates,
  useSortable,
  verticalListSortingStrategy,
} from "@dnd-kit/sortable";
import { CSS } from "@dnd-kit/utilities";
import "./App.css";
import {
  columnLabels,
  columnOrder,
  initialBoard,
  type BoardState,
  type ColumnId,
  type Task,
} from "./taskModel";

function isColumnId(value: string): value is ColumnId {
  return columnOrder.includes(value as ColumnId);
}

function findContainer(itemId: string, board: BoardState): ColumnId | null {
  if (isColumnId(itemId)) {
    return itemId;
  }

  return (
    columnOrder.find((columnId) =>
      board[columnId].some((task) => task.id === itemId),
    ) ?? null
  );
}

export default function App() {
  const [board, setBoard] = useState<BoardState>(initialBoard);
  const [activeId, setActiveId] = useState<string | null>(null);
  const sourceColumnRef = useRef<ColumnId | null>(null);

  const sensors = useSensors(
    useSensor(PointerSensor, {
      activationConstraint: { distance: 6 },
    }),
    useSensor(KeyboardSensor, {
      coordinateGetter: sortableKeyboardCoordinates,
    }),
  );

  function handleDragStart(event: DragStartEvent) {
    const taskId = String(event.active.id);
    setActiveId(taskId);
    sourceColumnRef.current = findContainer(taskId, board);
  }

  function handleDragOver(event: DragOverEvent) {
    const { active, over } = event;

    if (!over) {
      return;
    }

    const activeTaskId = String(active.id);
    const overId = String(over.id);

    setBoard((currentBoard) => {
      const activeColumn = findContainer(activeTaskId, currentBoard);
      const overColumn = findContainer(overId, currentBoard);

      if (!activeColumn || !overColumn || activeColumn === overColumn) {
        return currentBoard;
      }

      const activeTask = currentBoard[activeColumn].find(
        (task) => task.id === activeTaskId,
      );

      if (!activeTask) {
        return currentBoard;
      }

      const nextActiveItems = currentBoard[activeColumn].filter(
        (task) => task.id !== activeTaskId,
      );
      const overItems = currentBoard[overColumn];
      const overIndex = overItems.findIndex((task) => task.id === overId);
      const insertIndex = overIndex >= 0 ? overIndex : overItems.length;

      return {
        ...currentBoard,
        [activeColumn]: nextActiveItems,
        [overColumn]: [
          ...overItems.slice(0, insertIndex),
          activeTask,
          ...overItems.slice(insertIndex),
        ],
      };
    });
  }

  function handleDragEnd(event: DragEndEvent) {
    const startedIn = sourceColumnRef.current;
    sourceColumnRef.current = null;
    setActiveId(null);

    const { active, over } = event;

    if (!over || active.id === over.id) {
      return;
    }

    const activeTaskId = String(active.id);
    const overId = String(over.id);

    setBoard((currentBoard) => {
      const activeColumn = findContainer(activeTaskId, currentBoard);
      const overColumn = findContainer(overId, currentBoard);

      if (!activeColumn || !overColumn || activeColumn !== overColumn) {
        return currentBoard;
      }

      if (startedIn && startedIn !== activeColumn) {
        return currentBoard;
      }

      const columnTasks = currentBoard[activeColumn];
      const oldIndex = columnTasks.findIndex((task) => task.id === activeTaskId);
      const newIndex = columnTasks.findIndex((task) => task.id === overId);

      if (oldIndex < 0 || newIndex < 0) {
        return currentBoard;
      }

      return {
        ...currentBoard,
        [activeColumn]: arrayMove(columnTasks, oldIndex, newIndex),
      };
    });
  }

  function handleDragCancel() {
    sourceColumnRef.current = null;
    setActiveId(null);
  }

  return (
    <main className="appShell">
      <header className="appHeader">
        <div>
          <p className="eyebrow">Claude Code demo</p>
          <h1>Accessible drag-and-drop board</h1>
        </div>
        <p id="dnd-help" className="helpText">
          Use the handle on each card. Keyboard: Tab to a handle, Space or Enter to lift,
          arrow keys to move, Space or Enter to drop, Escape to cancel.
        </p>
      </header>

      <DndContext
        sensors={sensors}
        collisionDetection={closestCorners}
        onDragStart={handleDragStart}
        onDragOver={handleDragOver}
        onDragEnd={handleDragEnd}
        onDragCancel={handleDragCancel}
      >
        <div className="board" aria-describedby="dnd-help">
          {columnOrder.map((columnId) => (
            <KanbanColumn
              key={columnId}
              columnId={columnId}
              tasks={board[columnId]}
              activeId={activeId}
            />
          ))}
        </div>
      </DndContext>
    </main>
  );
}

function KanbanColumn({
  columnId,
  tasks,
  activeId,
}: {
  columnId: ColumnId;
  tasks: Task[];
  activeId: string | null;
}) {
  const { setNodeRef, isOver } = useDroppable({ id: columnId });

  return (
    <section
      ref={setNodeRef}
      className={`column ${isOver ? "columnOver" : ""}`}
      aria-labelledby={`heading-${columnId}`}
    >
      <h2 id={`heading-${columnId}`}>{columnLabels[columnId]}</h2>
      <SortableContext
        items={tasks.map((task) => task.id)}
        strategy={verticalListSortingStrategy}
      >
        <div className="taskList">
          {tasks.map((task) => (
            <SortableTask key={task.id} task={task} isActive={activeId === task.id} />
          ))}
          {tasks.length === 0 ? <p className="emptyState">Drop a task here</p> : null}
        </div>
      </SortableContext>
    </section>
  );
}

function SortableTask({ task, isActive }: { task: Task; isActive: boolean }) {
  const {
    attributes,
    listeners,
    setActivatorNodeRef,
    setNodeRef,
    transform,
    transition,
    isDragging,
  } = useSortable({ id: task.id });

  const style: CSSProperties = {
    transform: CSS.Transform.toString(transform),
    transition,
  };

  return (
    <article
      ref={setNodeRef}
      style={style}
      className={`taskCard ${isDragging || isActive ? "taskCardDragging" : ""}`}
    >
      <button
        ref={setActivatorNodeRef}
        type="button"
        className="dragHandle"
        aria-label={`Move ${task.title}`}
        {...attributes}
        {...listeners}
      >
        <span aria-hidden="true">↕</span>
      </button>
      <div>
        <h3>{task.title}</h3>
        <p>{task.note}</p>
      </div>
    </article>
  );
}

CSS

On remplace aussi src/App.css. En particulier, focus-visible et prefers-reduced-motion ne touchent pas qu’à l’apparence : ce sont des points examinés en revue d’accessibilité.

:root {
  color: #172033;
  background: #f6f7fb;
  font-family:
    Inter, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI",
    sans-serif;
}

body {
  margin: 0;
}

button {
  font: inherit;
}

.appShell {
  min-height: 100vh;
  padding: 32px;
}

.appHeader {
  display: grid;
  grid-template-columns: minmax(0, 1fr) minmax(280px, 460px);
  gap: 24px;
  align-items: end;
  margin-bottom: 24px;
}

.eyebrow {
  margin: 0 0 8px;
  color: #4f46e5;
  font-size: 0.78rem;
  font-weight: 700;
  text-transform: uppercase;
}

h1,
h2,
h3,
p {
  margin-top: 0;
}

h1 {
  margin-bottom: 0;
  font-size: clamp(2rem, 4vw, 3.5rem);
}

.helpText {
  margin-bottom: 0;
  color: #526070;
  line-height: 1.6;
}

.board {
  display: grid;
  grid-template-columns: repeat(3, minmax(220px, 1fr));
  gap: 16px;
}

.column {
  min-height: 420px;
  padding: 16px;
  border: 1px solid #d9deea;
  border-radius: 8px;
  background: #ffffff;
}

.columnOver {
  outline: 3px solid #93c5fd;
  outline-offset: 2px;
}

.taskList {
  display: grid;
  gap: 12px;
  min-height: 120px;
}

.taskCard {
  display: grid;
  grid-template-columns: 44px minmax(0, 1fr);
  gap: 12px;
  align-items: start;
  padding: 14px;
  border: 1px solid #d9deea;
  border-radius: 8px;
  background: #ffffff;
  box-shadow: 0 8px 24px rgba(23, 32, 51, 0.08);
}

.taskCardDragging {
  opacity: 0.58;
}

.taskCard h3 {
  margin-bottom: 4px;
  font-size: 1rem;
}

.taskCard p {
  margin-bottom: 0;
  color: #526070;
  line-height: 1.5;
}

.dragHandle {
  display: grid;
  width: 36px;
  height: 36px;
  place-items: center;
  border: 1px solid #c8d0df;
  border-radius: 8px;
  background: #f8fafc;
  color: #263244;
  cursor: grab;
}

.dragHandle:active {
  cursor: grabbing;
}

.dragHandle:focus-visible {
  outline: 3px solid #2563eb;
  outline-offset: 2px;
}

.emptyState {
  display: grid;
  min-height: 88px;
  place-items: center;
  border: 1px dashed #b9c2d3;
  border-radius: 8px;
  color: #6b7280;
}

@media (max-width: 760px) {
  .appShell {
    padding: 20px;
  }

  .appHeader,
  .board {
    grid-template-columns: 1fr;
  }
}

@media (prefers-reduced-motion: reduce) {
  .taskCard {
    transition: none;
  }
}

Vérifier clavier et accessibilité

Avec le drag and drop, « ça marche à la souris » ne suffit pas. Avant publication, contrôlez les points suivants.

  • On peut atteindre la poignée de chaque carte avec Tab.
  • Space ou Enter permet de soulever la carte.
  • Les flèches déplacent, Space ou Enter dépose.
  • Escape annule l’opération.
  • L’anneau de focus est visible.
  • Le texte d’aide est relié à la zone d’opération par aria-describedby.
  • On prend en compte prefers-reduced-motion pour les utilisateurs sensibles au mouvement.

Pour le téléversement de fichiers, ne dépendez pas du seul glissement. Conservez toujours un <input type="file"> pour permettre la sélection au clavier. Pour les détails, voir l’implémentation du téléversement de fichiers et la HTML Drag and Drop API de MDN.

Pièges fréquents

Le premier est de sauvegarder côté serveur à chaque dragover ou onDragOver. Comme l’événement se déclenche de nombreuses fois pendant le glissement, la sauvegarde se fait en principe après confirmation du dépôt. Gérer l’intermédiaire avec le seul state React est plus stable.

Le deuxième est d’oublier de faire d’une colonne vide une cible de dépôt. Si l’on ne peut rien poser dans une colonne sans aucune carte, un kanban réel se bloque vite. L’exemple ajoute useDroppable({ id: columnId }) à la colonne pour éviter ce problème.

Le troisième est de faire une UI réservée à la souris. Sur tactile, un conflit avec le défilement apparaît, et l’utilisateur clavier ne peut pas opérer. Mettre à la fois PointerSensor et KeyboardSensor de dnd-kit et faire de la poignée un button est la solution réaliste.

Le quatrième est de lire l’ordre depuis le DOM. En React, c’est le state, et non le DOM, qui fait foi. Pour sauvegarder, concevez l’envoi du contenu de board à l’API.

Le cinquième est de donner des consignes trop larges à Claude Code. Plutôt que « rends l’UI sympa », passer des contraintes comme « utilise dnd-kit », « gère le clavier », « gère la colonne vide », « sauvegarde après drag end » stabilise la qualité du code généré. Pour la conception de prompts, le recueil de techniques de prompt est aussi utile.

L’intégrer au tunnel de conversion

Un article sur le drag and drop ne montre pas qu’une implémentation d’UI : c’est un sujet facile à relier à l’accompagnement Claude Code ou à la vente de templates. Le lecteur a une demande concrète : « je veux mettre cette UI dans mon app métier ».

Chez ClaudeCodeLab, nous proposons la formation et le conseil Claude Code pour les équipes, ainsi que des templates et supports pour standardiser les règles d’implémentation. Pour intégrer une UI comme un kanban, un téléversement de fichiers ou l’édition de dashboard dans un produit interne, écrire au préalable dans CLAUDE.md « accessibilité », « moment de sauvegarde » et « manipulation directe du DOM interdite » rend la sortie de Claude Code plus facile à relire. Pour cadrer CLAUDE.md, voir aussi les bonnes pratiques Claude.md.

Résultats obtenus en pratique

Note de vérification de Masa : avec ce montage, décider d’abord le BoardState par colonne avant de solliciter Claude Code a nettement réduit les corrections ultérieures. En particulier, inscrire dans une checklist le dépôt en colonne vide, le clavier et l’annulation par Escape permet de repérer tôt le code qui « marche visuellement mais n’est pas de qualité publiable ». Pour l’appliquer au dépôt de fichiers ou au réordonnancement d’images, décider d’abord la forme du state, le moment de sauvegarde et l’alternative clavier reste ce qui fonctionne le mieux.

Conclusion

Pour construire un drag and drop avec Claude Code, décidez avant la bibliothèque d’implémentation la structure de données, le mode de saisie, l’accessibilité et le moment de sauvegarde. Pour une UI de réordonnancement React, prenez dnd-kit comme repère ; pour la réception de fichiers, la HTML Drag and Drop API de MDN.

À partir de cet exemple, vous pouvez décliner vers la gestion de tâches, l’édition de l’ordre d’affichage des articles, la galerie d’images, le kanban commercial et l’édition de dashboard. Avant publication, vérifiez la souris, le clavier, la colonne vide, la largeur mobile et la façon de revenir en arrière en cas d’échec de sauvegarde.

#Claude Code #drag and drop #React #UI #interaction
Gratuit

PDF gratuit: cheatsheet Claude Code

Saisissez votre email et téléchargez une page avec commandes, habitudes de review et workflow sûr.

Nous protégeons vos données et n'envoyons pas de spam.

Masa

À propos de l'auteur

Masa

Ingénieur spécialisé dans les workflows pratiques avec Claude Code.