ReactHustle

How to Create React Table with Sticky Columns using TailwindCSS

Jasser Mark Arioste

Jasser Mark Arioste

How to Create React Table with Sticky Columns using TailwindCSS

Hello, hustlers! In this tutorial, you'll learn how to create a react table with sticky columns using TailwindCSS.

Tutorial Objectives #

Here are our tutorial objectives for today.  We're going from simple and work our way up to complicated tables.

  1. Create a table with 1 sticky column with no logic, just pure JSX and TailwindCSS classes.
  2. Create a table with 2 sticky columns with no logic, just pure JSX and TailwindCSS classes.
  3. Create a table where you can control which columns will be stickied to the left. Here, we'll be using @tanstack/react-table package.

Final Output #

By the end of this tutorial, you'll be able to create an amazing dynamic table with sticky columns.

React Table Dynamic Sticky Columns - Final Output

Prerequisites #

In this tutorial, I assume you have a basic knowledge of React and Tailwind CSS. TailwindCSS should also be installed in your project. I won't cover its installation in this tutorial. 

Easy - Table with 1 Sticky Column #

To create a sticky column, you have to remember these three tips. These are the basic things you have to remember when creating sticky table columns. It can be applied to advanced implementations.

  1. use classes sticky and `left-*` on <th/> and <td/> elements. sticky doesn't work on <thead/> or <tr/> elements.
  2.  It also helps to use table-fixed on the <table> element to easily set the width for each column.
  3. Set the background-color for the stickied column. Otherwise, it will overlap with other columns because the default background is transparent.

Here's an example:

import React from "react";

const Table = () => {
  // array to populate rows
  const arr = new Array(10).fill("x");
  return (
    <div className="overflow-auto">
      <table className="table-fixed w-full">
        <thead>
          <tr className="text-left">
            <th className="w-10 p-2 sticky left-0 bg-indigo-900 text-white">
              ID
            </th>
            <th className="w-40 p-2 bg-indigo-500">Column 2</th>
            <th className="w-96 p-2 bg-indigo-500">Column 3</th>
            <th className="w-96 p-2 bg-indigo-500">Column 4</th>
          </tr>
        </thead>
        <tbody>
          {arr.map((item, index) => {
            return (
              <tr key={index} className="text-left">
                <td className="w-10 p-2 sticky left-0 bg-indigo-200">
                  {index}
                </td>
                <td className="w-40 p-2">Data</td>
                <td className="w-96 p-2">Data</td>
                <td className="w-96 p-2">Data</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
1234567891011121314151617181920212223242526272829303132333435363738

Here's the output. We stickied the first column to the left by using sticky to the first <th> and <td> elements:

React table sticky column example 1.

Medium - Table with 2 or more Sticky Columns #

Having two or more sticky columns gets a bit tricky because you have to accurately position the left property of the second column to the width of the first column.

Here's an updated example:

import React from "react";

const Table = () => {
  // array to populate rows
  const arr = new Array(10).fill("x");
  return (
    <div className="overflow-auto">
      <table className="table-fixed w-full">
        <thead>
          <tr className="text-left">
            <th className="w-10 p-2 sticky left-0 bg-indigo-900 text-white">
              ID
            </th>
            {/* notice how we use left-[40px] because `w-10` equals 40px */}
            <th className="w-40 p-2 sticky left-[40px] bg-indigo-900 text-white">
              Column 2
            </th>
            <th className="w-96 p-2 bg-indigo-500">Column 3</th>
            <th className="w-96 p-2 bg-indigo-500">Column 4</th>
          </tr>
        </thead>
        <tbody>
          {arr.map((item, index) => {
            return (
              <tr key={index} className="text-left">
                <td className="w-10 p-2 sticky left-0 bg-indigo-200">
                  {index}
                </td>
                {/* notice how we use left-[40px] because `w-10` equals 40px */}
                <td className="w-96 p-2 left-[40px] bg-indigo-200">Data</td>
                <td className="w-96 p-2">Data</td>
                <td className="w-96 p-2">Data</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
123456789101112131415161718192021222324252627282930313233343536373839404142

Here's the output:

React Table Sticky Columns using Tailwind CSS

Note that for multiple sticky columns to work, the ordering of the columns has to be correct. All the stickied columns must be ordered correctly.

For example, the code below does NOT work:

import React from "react";

const Table = () => {
  // array to populate rows
  const arr = new Array(10).fill("x");
  return (
    <div className="overflow-auto">
      <table className="table-fixed w-full">
        <thead>
          <tr className="text-left">
            <th className="w-10 p-2 sticky left-0 bg-indigo-900 text-white">
              ID
            </th>
            <th className="w-40 p-2 bg-indigo-500">Column 2</th>
            <th className="w-96 p-2 bg-indigo-500">Column 3</th>
            <th className="w-96 p-2 bg-indigo-500 sticky left-[40px]">
              Column 4
            </th>
          </tr>
        </thead>
        <tbody>
          {arr.map((item, index) => {
            return (
              <tr key={index} className="text-left">
                <td className="w-10 p-2 sticky left-0 bg-indigo-200">
                  {index}
                </td>
                <td className="w-40 p-2 ">Data</td>
                <td className="w-96 p-2">Data</td>
                <td className="w-96 p-2 sticky left-[40px]">Data</td>
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default Table;
12345678910111213141516171819202122232425262728293031323334353637383940

Here, only the first column will be stickied. We use sticky on the fourth column, but the columns before it has no sticky class.

Hard - Creating Dynamic Sticky Columns #

Now that we know how to create sticky columns using Tailwind CSS, let's increase the difficulty by making dynamic sticky columns. What if there's a requirement to select any column to be stickied on runtime? How should you implement it?

For this example, I'm using pure react since we don't need too much functionality. However, I also recommend using @tanstack/react-table if you have many features in your table or complex scenarios.

Step 1 - Defining the Types #

First, let's define the types that we'll be using in our table since this also connects to our column definition.  Create the file components/DynamicTable.tsx:

// components/DynamicTable.tsx

type User = {
  id: number;
  email: string;
  last_visited_at: string;
  created_at: string;
  ip_address: string;
  gender: string;
};

type ColumnDef<T> = {
  header: string; //header Text
  accessorKey: keyof T; //key for how to get the value
  width: number; // column width
  isPinned?: boolean; //column pinned state
};
1234567891011121314151617

Step 2 - Defining the Columns #

Next, let's define the columns of our table:

// components/DynamicTable.tsx
const defaultColumns: ColumnDef<User>[] = [
  {
    header: "id",
    accessorKey: "id",
    width: 60,
  },
  {
    header: "Email",
    accessorKey: "email",
    width: 250,
  },
  {
    header: "Last Visited At",
    accessorKey: "last_visited_at",
    width: 200,
  },
  {
    header: "Created At",
    accessorKey: "created_at",
    width: 200,
  },
  {
    header: "IP Address",
    accessorKey: "ip_address",
    width: 200,
  },
  {
    header: "Gender",
    accessorKey: "gender",
    width: 200,
  },
];
123456789101112131415161718192021222324252627282930313233

Step 3 - Generating the Data #

Next, we'll use fakerjs to generate the data that we'll be using. Install faker by using the following command:

# npm
npm i @faker-js/faker
# yarn
yarn add @faker-js/faker

Now add the following to your code:

// components/DynamicTable.tsx
// generate random data
const data = new Array(10).fill("x").map<User>((item, index) => {
  return {
    id: index,
    email: faker.internet.email(),
    gender: faker.name.gender(true),
    ip_address: faker.internet.ipv4(),
    created_at: faker.date.past().toDateString(),
    last_visited_at: faker.date.recent().toDateString(),
  };
});
123456789101112

Now that we have the column definitions and randomly generated data, all that's left is to implement the table.

Step 4 - Implementing a Dynamic Table #

Define a component called DynamicTable and copy the following code below. I've added comments so make sure you don't skip reading them.

// components/DynamicTable.tsx
import React, { useState } from "react";
import classNames from "classnames";

// ... omitted for brevity

const DynamicTable = () => {
  const [columns, setColumns] = useState([...defaultColumns]);

  // logic to pin a specific column
  // 1. modify the isPinned property
  // 2. sort the columns so that isPinned is first
  // 3. use setColumns to re-render the component
  const onPinColumn = (accessorKey: string, isPinned: boolean = false) => {
    const newCols = columns.map((col) => {
      if (col.accessorKey === accessorKey) {
        return {
          ...col,
          isPinned,
        };
      }
      return col;
    });

    newCols.sort((a, b) => {
      const aPinned = a.isPinned ? 1 : 0;
      const bPinned = b.isPinned ? 1 : 0;
      return bPinned - aPinned;
    });
    return setColumns([...newCols]);
  };

  // logic to get sticky position for columns
  // 1. find all the previous columns
  // 2. add the total width of all the previous columns.
  const getLeftStickyPos = (index: number) => {
    if (!index) return 0;

    const prevColumnsTotalWidth = columns
      .slice(0, index)
      .reduce((curr, column) => {
        return curr + column.width;
      }, 0);
    return prevColumnsTotalWidth;
  };
  return (
    <div className="overflow-auto max-h-[400px]">
      <table className="table-fixed w-full">
        <thead>
          <tr>
            {columns.map((col, i) => {
              return (
                <th
                  className={classNames({
                    "p-2 text-left whitespace-nowrap": true,
                    "bg-indigo-500": !col.isPinned,
                    "sticky bg-indigo-900 text-indigo-50": col.isPinned,
                  })}
                  style={{
                    left: getLeftStickyPos(i),
                    width: col.width,
                  }}
                  key={col.header}
                >
                  {col.header}
                  <button
                    onClick={() => onPinColumn(col.accessorKey, !col.isPinned)}
                    className="mx-2 text-xs text-white"
                  >
                    {col.isPinned ? "x" : "pin"}
                  </button>
                </th>
              );
            })}
          </tr>
        </thead>
      </table>
    </div>
  );
};

export default DynamicTable;
12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182

Here, we're only rendering the headers but  it has the following output:

React Table Headers with Dynamic Sticky Columns

Next, let's render the data along with it.

Step 4 - Rendering the Data #

To render the data, all we have to do is to map through the data and map through each column. We'll use the accessorKey property to get the value for the data. For example:

// components/DynamicTable.tsx
const DynamicTable = () => {
  const [columns, setColumns] = useState([...defaultColumns]);

  // ...
  return (
    <div className="overflow-auto max-h-[400px]">
      <table className="table-fixed w-full">
        <head>
          {/* ... */}
        </thead>
        <tbody>
          {data.map((item, index) => {
            return (
              <tr key={index} className="text-left">
                {columns.map((col, i) => {
                  const accessorKey = col.accessorKey;
                  const value = item[accessorKey];
                  return (
                    <td
                      className={classNames({
                        "p-2": true,
                        "sticky bg-indigo-200": col.isPinned,
                      })}
                      style={{
                        left: getLeftStickyPos(i),

                        width: col.width,
                      }}
                      key={col.header}
                    >
                      {value}
                    </td>
                  );
                })}
              </tr>
            );
          })}
        </tbody>
      </table>
    </div>
  );
};

export default DynamicTable;
123456789101112131415161718192021222324252627282930313233343536373839404142434445

After this step, you should have the same output as our final output above.

Code and Demo #

The full code is available at my GitHub: jmarioste/react-table-sticky-column-tailwind-tutorial.

The demo is available on StackBlitz: React Table Sticky Column Tailwind Tutorial

Conclusion #

You learned how to create a react table with dynamic sticky columns from simple to a bit of an advanced example. 

If you like this tutorial, please leave a like or share this article. For future tutorials like this, please subscribe to our newsletter or follow me on GitHub.

Resources #

To learn more about sticky columns, I suggest you read the tutorial below as well.

Credits: Image by Mrexentric from Pixabay

Share this post!

Related Posts

Disclaimer

This content may contain links to products, software and services. Please assume all such links are affiliate links which may result in my earning commissions and fees.
As an Amazon Associate, I earn from qualifying purchases. This means that whenever you buy a product on Amazon from a link on our site, we receive a small percentage of its price at no extra cost to you. This helps us continue to provide valuable content and reviews to you. Thank you for your support!
Donate to ReactHustle