Trello-Style Board Clone
Build multi-board workflows with lists, cards, labels, and URL-driven navigation state
Time to implement the project: ~ 4-6 weeks
- HTML
- CSS
- Advanced JavaScript
- Drag and Drop
- Client-Side Routing
- State Modeling
- URL-based State
This project takes the Kanban idea into a product-grade system. You will build a Trello-style clone with multiple boards, each board containing lists, and each list containing draggable cards. Users must create and rename boards and lists, add cards, reorder lists, and move cards across lists using drag-and-drop interactions that remain stable under fast usage.
Cards must support labels and user assignments. A card should display its label colors and assigned users at a glance, and a detail view (modal or dedicated route) should allow editing title, description, labels, and assignments. Navigation must be URL-driven: the selected board, open card, and filter states are represented in the URL so deep links work and refresh does not reset the app context. The result should feel coherent, responsive, and production-aligned.
What This Project Proves at an Advanced Level
This project is built to test advanced frontend architecture. You will design a data model that supports nested structures (boards → lists → cards), then implement UI operations that modify that structure without breaking references or losing order. Drag-and-drop adds complexity: you must update both the visible UI and the underlying state with precision, including list reordering, card movement, and cross-list insertion points.
URL-based state introduces a higher standard: views must be reconstructible from the address bar. That requirement forces clear separation between routing, state, and rendering, and it reflects how production applications are built for shareable navigation and consistent user experience.
Prerequisites and Expected Competency
This task expects confident JavaScript skills and experience structuring larger UI features. You should already write code in modules, maintain consistent state, and debug complex interactions without guessing.
- Strong DOM and event model knowledge, including delegation strategies
- Experience implementing drag-and-drop with stable ordering and drop targets
- Understanding of routing patterns (history API, URL params, deep linking)
- Ability to design normalized state for nested entities and relations
- Familiarity with accessibility expectations for interactive components
- Comfort profiling performance for large lists and frequent UI updates
- Clean code organization (modules, services, and predictable data flow)
System Requirements and Review Criteria
A credible Trello-style clone is judged by consistency, recoverability, and correctness under repeated actions. Reviewers look for predictable state transitions, stable drag-and-drop behavior, and URL-driven navigation that preserves context across refreshes. These requirements align with real take-home tests for advanced frontend roles.
| Requirement | Explanation |
| Multiple boards with switching via URL | Board selection tied to the URL enables deep links and proves routing drives view state. |
| Lists with ordering and list reordering | Reorderable lists require careful state updates and validate your nested data modeling. |
| Drag-and-drop cards between lists | Card movement tests event coordination, drop targeting, and correct insertion logic. |
| Card details view (modal or route) | Detail editing adds a second interaction layer and forces separation of concerns in code. |
| Labels system with visual indicators | Labels introduce metadata and filtering potential, reflecting real product UI complexity. |
| User assignments on cards | Assignments require relational state (users ↔ cards) and consistent rendering across views. |
| URL-based open-card state | Linkable card state proves the UI can be reconstructed from navigation alone. |
| Stable persistence strategy | Persisting board data prevents loss of work and supports repeat sessions and testing flows. |
| Robust edge-case handling | Empty lists, deleted cards, and invalid URLs must fail safely without breaking the app. |
Advanced Build Strategy and Execution Tips
Start by designing the data model and routes before writing UI code. A clean model treats boards, lists, cards, users, and labels as entities with IDs, and stores ordering as arrays of IDs rather than nested DOM-driven state. For routing, treat the URL as an input: parse it into a view state, render the correct board, and then apply optional states like “open card” or “filter by label.”
For drag-and-drop, isolate the reorder logic into pure functions that receive state and return updated state. This approach keeps bugs measurable and prevents UI-only fixes. Make interactions feel deliberate: drop previews, clear insertion markers, and guarded handlers that reject invalid drops. When state is deterministic, complex UI stops feeling fragile.
- Normalize entities (boards, lists, cards, users, labels) to prevent duplicated objects across views
- Represent ordering with arrays of IDs to support reordering without reshaping the entire structure
- Use the History API to push URL changes on board switch and card open, keeping refresh behavior correct
- Implement drag preview and insertion indicators so users always understand where a card will land
- Guard route parsing against invalid IDs and redirect to a safe default instead of rendering broken UI
- Persist state after every meaningful mutation to avoid partial updates and lost edits
- Throttle expensive renders when moving cards rapidly to keep the interface responsive
- Keep detail editing isolated so updating a card never resets the board scroll or list positions
Common Mistakes When Building a Trello-Style Board Clone
1. Storing cards only inside columns without stable IDs
A Trello-style board looks simple at first: you have columns, and each column contains cards. The mistake is treating cards as anonymous pieces of text inside an array. This becomes a serious problem when you add drag-and-drop, editing, deleting, comments, labels, due dates, or card detail modals. If a card does not have a stable ID, it is difficult to move it safely, update it later, or keep the UI consistent after reordering.
Problematic approach:
const board = {
columns: [
{
title: "To Do",
cards: ["Create layout", "Add drag and drop"]
},
{
title: "Done",
cards: ["Create project files"]
}
]
};
function moveCard(fromColumnIndex, toColumnIndex, cardIndex) {
const card = board.columns[fromColumnIndex].cards[cardIndex];
board.columns[fromColumnIndex].cards.splice(cardIndex, 1);
board.columns[toColumnIndex].cards.push(card);
}
This version works only while cards are plain strings. As soon as you add editing, descriptions, labels, comments, or modal views, the structure becomes too weak.
Better approach:
const board = {
columnsById: {
"column-1": {
id: "column-1",
title: "To Do",
cardIds: ["card-1", "card-2"]
},
"column-2": {
id: "column-2",
title: "Done",
cardIds: ["card-3"]
}
},
cardsById: {
"card-1": {
id: "card-1",
title: "Create layout",
description: "",
labels: []
},
"card-2": {
id: "card-2",
title: "Add drag and drop",
description: "",
labels: []
},
"card-3": {
id: "card-3",
title: "Create project files",
description: "",
labels: []
}
},
columnOrder: ["column-1", "column-2"]
};
Pay attention to: Use stable IDs for boards, columns, and cards. Store card data separately from column ordering. Columns should usually contain card IDs, not duplicated card objects. This makes drag-and-drop, editing, filtering, saving, and restoring the board much easier.
2. Mutating nested board state directly during drag-and-drop
Drag-and-drop logic often requires removing a card from one list and inserting it into another. Many beginners directly mutate arrays with splice() on the
existing state object. This can create hidden bugs, especially in React, because the reference to the parent object may not change. The UI may fail to re-render, or old
and new state may accidentally point to the same nested data.
Problematic code:
function onDragEnd(result) {
const { source, destination } = result;
if (!destination) return;
const sourceColumn = board.columns[source.droppableId];
const destinationColumn = board.columns[destination.droppableId];
const [movedCard] = sourceColumn.cards.splice(source.index, 1);
destinationColumn.cards.splice(destination.index, 0, movedCard);
setBoard(board);
}
The code changes the original board object. Even if it appears to work, it makes debugging harder and can break predictable state updates.
Recommended solution:
function onDragEnd(result) {
const { source, destination } = result;
if (!destination) return;
if (
source.droppableId === destination.droppableId &&
source.index === destination.index
) {
return;
}
setBoard((currentBoard) => {
const sourceColumn = currentBoard.columnsById[source.droppableId];
const destinationColumn = currentBoard.columnsById[destination.droppableId];
const sourceCardIds = [...sourceColumn.cardIds];
const destinationCardIds =
sourceColumn.id === destinationColumn.id
? sourceCardIds
: [...destinationColumn.cardIds];
const [movedCardId] = sourceCardIds.splice(source.index, 1);
destinationCardIds.splice(destination.index, 0, movedCardId);
return {
...currentBoard,
columnsById: {
...currentBoard.columnsById,
[sourceColumn.id]: {
...sourceColumn,
cardIds: sourceCardIds
},
[destinationColumn.id]: {
...destinationColumn,
cardIds: destinationCardIds
}
}
};
});
}
Pay attention to: Drag-and-drop should produce a new state object instead of modifying the old one. This keeps rendering predictable and makes it easier to add undo, autosave, optimistic updates, and server synchronization later.
3. Handling card movement but forgetting column reordering
Many Trello clone projects support moving cards between columns, but the columns themselves remain fixed. That makes the board feel incomplete because real boards often allow users to reorder lists such as Backlog, To Do, In Progress, Review, and Done. Even if your first version does not include full column drag-and-drop, your data model should be ready for it.
Limited structure:
const board = {
columnsById: {
todo: { id: "todo", title: "To Do", cardIds: [] },
progress: { id: "progress", title: "In Progress", cardIds: [] },
done: { id: "done", title: "Done", cardIds: [] }
}
};
const columns = Object.values(board.columnsById);
This approach depends on object iteration order. It is not clear where column ordering lives, and it is difficult to update when the user drags an entire list.
Improved structure:
const board = {
columnOrder: ["todo", "progress", "done"],
columnsById: {
todo: { id: "todo", title: "To Do", cardIds: [] },
progress: { id: "progress", title: "In Progress", cardIds: [] },
done: { id: "done", title: "Done", cardIds: [] }
}
};
const orderedColumns = board.columnOrder.map((columnId) => {
return board.columnsById[columnId];
});
Column reorder example:
function reorderColumns(startIndex, endIndex) {
setBoard((currentBoard) => {
const nextColumnOrder = [...currentBoard.columnOrder];
const [movedColumnId] = nextColumnOrder.splice(startIndex, 1);
nextColumnOrder.splice(endIndex, 0, movedColumnId);
return {
...currentBoard,
columnOrder: nextColumnOrder
};
});
}
Pay attention to: Store column order explicitly. Even if your first milestone focuses only on cards, a good board architecture should make column reordering possible without rewriting the whole state structure.
4. Saving board data too often or not saving it at all
A board clone should not lose all work after page refresh. At the same time, saving after every tiny state change without control can create performance issues, especially when the board grows. If you use browser storage, you need a clear save strategy. If you use an API, you need loading, error, and retry logic.
Problematic code:
function updateCardTitle(cardId, title) {
setBoard((board) => ({
...board,
cardsById: {
...board.cardsById,
[cardId]: {
...board.cardsById[cardId],
title
}
}
}));
localStorage.setItem("board", JSON.stringify(board));
}
This code may save the old board value because state updates are asynchronous. It also writes to storage immediately on every title change.
Better approach:
useEffect(() => {
const timeoutId = setTimeout(() => {
localStorage.setItem("trello-board", JSON.stringify(board));
}, 400);
return () => clearTimeout(timeoutId);
}, [board]);
Loading saved data:
function loadInitialBoard() {
const savedBoard = localStorage.getItem("trello-board");
if (!savedBoard) {
return createDefaultBoard();
}
try {
return JSON.parse(savedBoard);
} catch (error) {
return createDefaultBoard();
}
}
const [board, setBoard] = useState(loadInitialBoard);
Pay attention to: Autosave should be reliable, but not chaotic. Save from the latest state, debounce frequent changes, handle invalid saved data, and show users when changes are saved if you add a backend later.
5. Making drag-and-drop work only with a mouse
Drag-and-drop is the central feature of a Trello-style board, but it should not be the only way to move cards. Some users navigate with a keyboard, some use touch devices, and some drag-and-drop libraries need additional accessibility work. A common mistake is making the board visually draggable but ignoring focus states, button semantics, keyboard shortcuts, and alternative controls.
Less accessible interaction:
<div class="card" draggable="true" ondragstart="startDrag(event)">
Fix navbar spacing
</div>
This gives basic mouse dragging, but the card is not a proper interactive control. It has no keyboard behavior, no descriptive label, and no alternative movement action.
Improved structure:
<article class="card" tabindex="0" aria-label="Task: Fix navbar spacing">
<h3>Fix navbar spacing</h3>
<div class="card-actions" aria-label="Card actions">
<button type="button" onclick="moveCardLeft(cardId)">
Move left
</button>
<button type="button" onclick="moveCardRight(cardId)">
Move right
</button>
<button type="button" onclick="openCardModal(cardId)">
Open details
</button>
</div>
</article>
Useful keyboard handling:
function handleCardKeyDown(event, cardId) {
if (event.key === "Enter") {
openCardModal(cardId);
}
if (event.altKey && event.key === "ArrowRight") {
moveCardToNextColumn(cardId);
}
if (event.altKey && event.key === "ArrowLeft") {
moveCardToPreviousColumn(cardId);
}
}
Pay attention to: Drag-and-drop should feel good with a mouse, but the board should still be usable through buttons, keyboard focus, clear labels, and predictable modal behavior.
By completing this project, you'll gain advanced experience building a Trello-style workflow system with nested data structures, drag-and-drop reordering, metadata such as labels and assignments, and URL-based state that supports deep linking and refresh-safe navigation. You will sharpen your ability to design scalable frontend architecture, implement deterministic state transitions, and deliver complex interactions that remain stable under real usage. This foundation supports building production dashboards, admin tools, and collaborative systems that demand reliable navigation and high interaction quality.
Reference Implementations Worth Studying
Focused starter reference:
tberghuis - Trello Board Clone
This is the most focused reference for learners who want to understand the core board experience without getting distracted by a large backend or many product screens. It is a single-board Trello clone built with React and react-beautiful-dnd. The project includes draggable lists and cards, CRUD behavior for lists and cards, auto-sizing textareas, and persistence through browser storage.
Pay particular attention to:
- How the project keeps the scope limited to one board while still demonstrating the most important Trello interaction.
- The way drag-and-drop applies to both cards and lists, not only individual task cards.
- How browser storage is used to autosave and restore board data.
- The choice to represent application state through a single board data object.
- How small UI details such as auto-sizing textareas make the clone feel more realistic.
Use this repository as a practical baseline. It is especially useful if your goal is to build a clean portfolio project that proves you understand drag-and-drop state management without making the app unnecessarily complex.
More architecture-focused implementation:
Marcus20 - Trello Clone
This implementation is useful because it explores a Trello clone through React, Redux, and TypeScript. Even where the project reads more like an architectural experiment than a finished production app, it gives learners a different perspective: how a board can be structured when stronger typing and centralized state management are part of the technical direction.
When studying the code, focus on:
- How TypeScript can make board, list, and card data safer to work with.
- How Redux changes the way drag-and-drop updates are planned and dispatched.
- The separation between application state, presentational components, and future routes.
- The planned features such as boards route, home route, add list, add card, and drag-and-drop functionality.
- How tests and routing could be added as the project grows beyond a single board.
What makes this reference different is its emphasis on structure and scalability. It is a good comparison point if you want your Trello board clone to demonstrate stronger frontend architecture, not only visual similarity.
Alternative full-stack learning path:
ytolstyk - Trello
This repository offers a very different implementation style because it is built around Backbone, Bootstrap, jQuery UI, and a Rails API. Instead of focusing only on modern React-style component state, it introduces a more traditional MVC-style approach with boards, lists, cards, todo items, assignments, routing, server-backed data, ordering, and drag-and-drop behavior.
While reviewing this project, examine:
- How the project separates boards, lists, cards, todo items, and assignments as distinct models.
- How list and card ordering is handled with an
ordvalue instead of relying only on array position. - How routing is used for board index, board show, and board creation flows.
- How jQuery UI Sortable is used to support drag-and-drop behavior.
- How the Rails API changes the complexity of saving, loading, and updating board data.
This is a useful alternative if you want to understand the product logic behind Trello beyond one frontend framework. Compare it with React-based implementations to see which concepts are framework-specific and which concepts belong to the board architecture itself.