CC
0 XP
0

Chapter 2: Build Core Todo

Empty States

build3 min

Empty States

An empty todo list with no message feels broken. A good empty state tells the user what happened and what to do next. Let's add friendly messages for every scenario.

The prompt

"Create an EmptyState component at src/components/EmptyState.tsx. It should display different messages depending on why the list is empty: no todos at all (encourage adding one), no matches for search (suggest clearing the search), no matches for category filter (suggest trying a different category). Accept a type prop to determine which message to show. Include a relevant icon or illustration for each state."

What Claude Code creates

'use client';
 
interface EmptyStateProps {
  type: 'no-todos' | 'no-search-results' | 'no-filter-results';
}
 
const messages = {
  'no-todos': {
    title: 'No todos yet',
    description: 'Add your first todo above to get started!',
    icon: (
      <svg className="w-12 h-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
          d="M9 5H7a2 2 0 00-2 2v12a2 2 0 002 2h10a2 2 0 002-2V7a2 2 0 00-2-2h-2M9 5a2 2 0 002 2h2a2 2 0 002-2M9 5a2 2 0 012-2h2a2 2 0 012 2" />
      </svg>
    ),
  },
  'no-search-results': {
    title: 'No results found',
    description: 'Try a different search term or clear the search.',
    icon: (
      <svg className="w-12 h-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
          d="M21 21l-6-6m2-5a7 7 0 11-14 0 7 7 0 0114 0z" />
      </svg>
    ),
  },
  'no-filter-results': {
    title: 'No todos in this category',
    description: 'Try selecting a different category or add a new todo.',
    icon: (
      <svg className="w-12 h-12 text-gray-300" fill="none" viewBox="0 0 24 24" stroke="currentColor">
        <path strokeLinecap="round" strokeLinejoin="round" strokeWidth={1.5}
          d="M3 4a1 1 0 011-1h16a1 1 0 011 1v2.586a1 1 0 01-.293.707l-6.414 6.414a1 1 0 00-.293.707V17l-4 4v-6.586a1 1 0 00-.293-.707L3.293 7.293A1 1 0 013 6.586V4z" />
      </svg>
    ),
  },
};
 
export default function EmptyState({ type }: EmptyStateProps) {
  const { title, description, icon } = messages[type];
 
  return (
    <div className="bg-white rounded-lg shadow-sm border border-gray-200
                    p-12 text-center">
      <div className="flex justify-center mb-4">{icon}</div>
      <h3 className="text-lg font-medium text-gray-900 mb-1">{title}</h3>
      <p className="text-gray-500 text-sm">{description}</p>
    </div>
  );
}
💡Info

The three empty states cover the most common scenarios. Each gives the user a clear next action -- this is much better than a blank screen or a generic "Nothing to show" message. Using SVG icons instead of emoji keeps the design consistent and professional.

Wire it into the page

"Update page.tsx to show the appropriate EmptyState when the filtered todo list is empty. Determine which type to show based on whether there's a search query, a category filter, or no todos at all."

// Helper function to determine which empty state to show:
const getEmptyStateType = (): EmptyStateProps['type'] => {
  if (todos.length === 0) return 'no-todos';
  if (searchQuery) return 'no-search-results';
  if (filterCategory) return 'no-filter-results';
  return 'no-todos';
};
 
// In the JSX, replace the TodoList with a conditional:
{sortedTodos.length > 0 ? (
  <TodoList
    todos={sortedTodos}
    onToggle={toggleTodo}
    onDelete={deleteTodo}
    onEdit={editTodo}
  />
) : (
  <EmptyState type={getEmptyStateType()} />
)}
Tip

The order of checks in getEmptyStateType matters. We check searchQuery before filterCategory because if someone is actively searching, clearing the search is the most actionable thing they can do. This kind of UX thinking is easy to miss but makes the app feel intuitive.

Add a todo count status bar

While we are polishing, let's add a status line that shows how many todos are visible:

"Add a small status line between the filters and the list showing the count of todos. Something like '5 todos' or '2 of 5 todos' when filtered. Also show how many are completed."

<p className="text-sm text-gray-500">
  {filteredTodos.length === todos.length
    ? `${todos.length} todo${todos.length !== 1 ? 's' : ''}`
    : `${filteredTodos.length} of ${todos.length} todos`}
  {' -- '}
  {todos.filter((t) => t.completed).length} completed
</p>

Test empty states

  1. Delete all todos -- you should see "No todos yet" with a clipboard icon and a prompt to add one
  2. Add some todos, then search for something that does not exist -- "No results found" with a search icon
  3. Filter by a category that has no todos -- "No todos in this category" with a filter icon
  4. Clear the search or filter -- the list returns to normal
  5. Check that the status bar updates correctly as you add, complete, and filter todos
⚠️Warning

If you are using localStorage and want to test the "no todos" state, either clear localStorage from DevTools (Application > Local Storage > right-click > Clear) or ask Claude Code: "Add a temporary 'Clear all' button to delete all todos for testing."

Why empty states matter

Empty states are one of the most impactful small features you can add to any application. They turn a confusing blank screen into a helpful guide. Professional apps spend significant design time on empty states because they represent a critical moment -- the user either gets unstuck or leaves.

With Claude Code, adding thoughtful empty states takes about 3 minutes. That is an incredible return on effort.

The finished component layout

File StructureYour structure may look different — that's OK

src/ components/ AddTodo.tsx TodoList.tsx CategoryFilter.tsx SearchBar.tsx SortSelect.tsx EmptyState.tsx hooks/ useLocalStorage.ts types/ todo.ts data/ sample-todos.ts

That wraps up the feature build. Let's review everything we have accomplished in the chapter review.