Skip to main content
Back to Blog
Tutorials
3 min read
November 20, 2024

How to Build a Mobile-Responsive Data Table in React

Create a data table that works on all screen sizes. Card layout on mobile, full table on desktop, with sorting and filtering.

Ryel Banfield

Founder & Lead Developer

Data tables are difficult on small screens. The solution: show a full table on desktop and switch to a card layout on mobile. No horizontal scrolling required.

Step 1: Define the Table Data

type Invoice = {
  id: string;
  client: string;
  amount: number;
  status: "paid" | "pending" | "overdue";
  date: string;
};

const invoices: Invoice[] = [
  { id: "INV-001", client: "Acme Corp", amount: 2500, status: "paid", date: "2026-01-15" },
  { id: "INV-002", client: "TechStart", amount: 4800, status: "pending", date: "2026-01-20" },
  { id: "INV-003", client: "GrowthCo", amount: 1200, status: "overdue", date: "2026-01-05" },
  { id: "INV-004", client: "DesignHub", amount: 3600, status: "paid", date: "2026-01-22" },
  { id: "INV-005", client: "DataFlow", amount: 7500, status: "pending", date: "2026-01-25" },
];

Step 2: Build the Desktop Table

function DesktopTable({ data, sortField, sortDirection, onSort }: {
  data: Invoice[];
  sortField: string;
  sortDirection: "asc" | "desc";
  onSort: (field: string) => void;
}) {
  return (
    <div className="hidden md:block">
      <table className="w-full text-left">
        <thead>
          <tr className="border-b text-sm text-gray-500 dark:border-gray-700">
            {["id", "client", "amount", "status", "date"].map((field) => (
              <th key={field} className="px-4 py-3">
                <button
                  onClick={() => onSort(field)}
                  className="flex items-center gap-1 font-medium hover:text-gray-900 dark:hover:text-white"
                >
                  {field.charAt(0).toUpperCase() + field.slice(1)}
                  {sortField === field && (
                    <span>{sortDirection === "asc" ? "↑" : "↓"}</span>
                  )}
                </button>
              </th>
            ))}
          </tr>
        </thead>
        <tbody>
          {data.map((invoice) => (
            <tr
              key={invoice.id}
              className="border-b transition-colors hover:bg-gray-50 dark:border-gray-700 dark:hover:bg-gray-800"
            >
              <td className="px-4 py-3 text-sm font-medium">{invoice.id}</td>
              <td className="px-4 py-3 text-sm">{invoice.client}</td>
              <td className="px-4 py-3 text-sm">
                ${invoice.amount.toLocaleString()}
              </td>
              <td className="px-4 py-3">
                <StatusBadge status={invoice.status} />
              </td>
              <td className="px-4 py-3 text-sm text-gray-500">
                {new Date(invoice.date).toLocaleDateString()}
              </td>
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}

Step 3: Build the Mobile Card Layout

function MobileCards({ data }: { data: Invoice[] }) {
  return (
    <div className="space-y-4 md:hidden">
      {data.map((invoice) => (
        <div
          key={invoice.id}
          className="rounded-lg border p-4 dark:border-gray-700"
        >
          <div className="flex items-center justify-between">
            <span className="text-sm font-medium">{invoice.id}</span>
            <StatusBadge status={invoice.status} />
          </div>
          <p className="mt-2 font-semibold">{invoice.client}</p>
          <div className="mt-3 flex items-center justify-between text-sm text-gray-500">
            <span>${invoice.amount.toLocaleString()}</span>
            <span>{new Date(invoice.date).toLocaleDateString()}</span>
          </div>
        </div>
      ))}
    </div>
  );
}

Step 4: Status Badge Component

function StatusBadge({ status }: { status: Invoice["status"] }) {
  const styles = {
    paid: "bg-green-100 text-green-700 dark:bg-green-900/30 dark:text-green-400",
    pending: "bg-yellow-100 text-yellow-700 dark:bg-yellow-900/30 dark:text-yellow-400",
    overdue: "bg-red-100 text-red-700 dark:bg-red-900/30 dark:text-red-400",
  };

  return (
    <span className={`rounded-full px-2.5 py-0.5 text-xs font-medium ${styles[status]}`}>
      {status.charAt(0).toUpperCase() + status.slice(1)}
    </span>
  );
}

Step 5: Add Sorting and Filtering

"use client";

import { useState, useMemo } from "react";

export function DataTable() {
  const [sortField, setSortField] = useState("date");
  const [sortDirection, setSortDirection] = useState<"asc" | "desc">("desc");
  const [filter, setFilter] = useState("");
  const [statusFilter, setStatusFilter] = useState<string>("all");

  function handleSort(field: string) {
    if (sortField === field) {
      setSortDirection((prev) => (prev === "asc" ? "desc" : "asc"));
    } else {
      setSortField(field);
      setSortDirection("asc");
    }
  }

  const filteredData = useMemo(() => {
    let data = [...invoices];

    // Text filter
    if (filter) {
      const query = filter.toLowerCase();
      data = data.filter(
        (inv) =>
          inv.id.toLowerCase().includes(query) ||
          inv.client.toLowerCase().includes(query)
      );
    }

    // Status filter
    if (statusFilter !== "all") {
      data = data.filter((inv) => inv.status === statusFilter);
    }

    // Sort
    data.sort((a, b) => {
      const aVal = a[sortField as keyof Invoice];
      const bVal = b[sortField as keyof Invoice];
      const modifier = sortDirection === "asc" ? 1 : -1;

      if (typeof aVal === "number" && typeof bVal === "number") {
        return (aVal - bVal) * modifier;
      }
      return String(aVal).localeCompare(String(bVal)) * modifier;
    });

    return data;
  }, [filter, statusFilter, sortField, sortDirection]);

  return (
    <div>
      {/* Filters */}
      <div className="flex flex-col gap-3 sm:flex-row sm:items-center sm:justify-between">
        <input
          type="text"
          placeholder="Search invoices..."
          value={filter}
          onChange={(e) => setFilter(e.target.value)}
          className="w-full rounded-lg border px-3 py-2 text-sm sm:w-64 dark:border-gray-700 dark:bg-gray-800"
        />
        <select
          value={statusFilter}
          onChange={(e) => setStatusFilter(e.target.value)}
          className="rounded-lg border px-3 py-2 text-sm dark:border-gray-700 dark:bg-gray-800"
        >
          <option value="all">All statuses</option>
          <option value="paid">Paid</option>
          <option value="pending">Pending</option>
          <option value="overdue">Overdue</option>
        </select>
      </div>

      {/* Table / Cards */}
      <div className="mt-4">
        <DesktopTable
          data={filteredData}
          sortField={sortField}
          sortDirection={sortDirection}
          onSort={handleSort}
        />
        <MobileCards data={filteredData} />
      </div>

      {/* Empty state */}
      {filteredData.length === 0 && (
        <p className="py-8 text-center text-sm text-gray-500">
          No invoices found.
        </p>
      )}
    </div>
  );
}

Alternative: Horizontal Scroll

For simpler cases, a scrollable table is acceptable:

<div className="overflow-x-auto">
  <table className="min-w-[600px] w-full">
    {/* table content */}
  </table>
</div>

This works but is less user-friendly on mobile than the card approach.

Need Custom Dashboard Components?

We build data-rich dashboards and admin interfaces with responsive tables, charts, and real-time data. Contact us for a consultation.

Reactdata tableresponsiveTailwind CSStutorial

Ready to Start Your Project?

RCB Software builds world-class websites and applications for businesses worldwide.

Get in Touch

Related Articles