Make table rows clickable as links with tanstack-table and CSS
Tags: typescript, webdev, tutorial, a11y
Published: 2023-11-19 14:46:41

You want your entire table row to be a real clickable link? Here, I present you with a practical and accesible solution.

![gif of the result](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/2zi4fgq0m0zd0brlxh2o.gif)

Link to the codesandbox: [Check it out!](https://codesandbox.io/p/sandbox/busy-brattain-2t34fm?file=%2Fsrc%2Fcomponents%2FTable.tsx%3A1%2C1&layout=%257B%2522sidebarPanel%2522%253A%2522EXPLORER%2522%252C%2522rootPanelGroup%2522%253A%257B%2522direction%2522%253A%2522horizontal%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522id%2522%253A%2522ROOT_LAYOUT%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522UNKNOWN%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522clp4f2afl000a2067l6eod13o%2522%252C%2522sizes%2522%253A%255B68.08510638297872%252C31.914893617021278%255D%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522EDITOR%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522EDITOR%2522%252C%2522id%2522%253A%2522clp4f2afl0003206792wbne95%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522direction%2522%253A%2522horizontal%2522%252C%2522id%2522%253A%2522SHELLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522SHELLS%2522%252C%2522id%2522%253A%2522clp4f2afl00072067r47lzkym%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%257D%252C%257B%2522type%2522%253A%2522PANEL_GROUP%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522direction%2522%253A%2522vertical%2522%252C%2522id%2522%253A%2522DEVTOOLS%2522%252C%2522panels%2522%253A%255B%257B%2522type%2522%253A%2522PANEL%2522%252C%2522contentType%2522%253A%2522DEVTOOLS%2522%252C%2522id%2522%253A%2522clp4f2afl00092067t0osnxvi%2522%257D%255D%252C%2522sizes%2522%253A%255B100%255D%257D%255D%252C%2522sizes%2522%253A%255B56.71842353%252C43.28157647%255D%257D%252C%2522tabbedPanels%2522%253A%257B%2522clp4f2afl0003206792wbne95%2522%253A%257B%2522id%2522%253A%2522clp4f2afl0003206792wbne95%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clp4f2afk00022067x7e3xk4g%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252FREADME.md%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.tsx%2522%252C%2522id%2522%253A%2522clp4f3tyh00b0206737mjkyju%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252Fcomponents%252FTable.tsx%2522%252C%2522id%2522%253A%2522clp4f4sn700pv2067wuspt554%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252FApp.css%2522%252C%2522id%2522%253A%2522clp4fiuu40alm2067bigyq6b7%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%252C%257B%2522type%2522%253A%2522FILE%2522%252C%2522filepath%2522%253A%2522%252Fsrc%252Findex.css%2522%252C%2522id%2522%253A%2522clp5id3m803j42067daorh2rl%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522state%2522%253A%2522IDLE%2522%257D%255D%252C%2522activeTabId%2522%253A%2522clp4f4sn700pv2067wuspt554%2522%257D%252C%2522clp4f2afl00092067t0osnxvi%2522%253A%257B%2522id%2522%253A%2522clp4f2afl00092067t0osnxvi%2522%252C%2522activeTabId%2522%253A%2522clp4f2afl00082067wty6ob7a%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clp4f2afl00082067wty6ob7a%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_PORT%2522%252C%2522taskId%2522%253A%2522dev%2522%252C%2522port%2522%253A5173%252C%2522path%2522%253A%2522%252F%2522%257D%252C%257B%2522type%2522%253A%2522SANDBOX_INFO%2522%252C%2522id%2522%253A%2522clp5e8gtk004a2067tvhgd0xv%2522%252C%2522mode%2522%253A%2522permanent%2522%257D%255D%257D%252C%2522clp4f2afl00072067r47lzkym%2522%253A%257B%2522id%2522%253A%2522clp4f2afl00072067r47lzkym%2522%252C%2522activeTabId%2522%253A%2522clp4f2n6400792067u2wc73c0%2522%252C%2522tabs%2522%253A%255B%257B%2522id%2522%253A%2522clp4f2afl00042067ue0uvqet%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522dev%2522%257D%252C%257B%2522id%2522%253A%2522clp4f2afl000520679183s03n%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522build%2522%257D%252C%257B%2522id%2522%253A%2522clp4f2afl00062067qqmq8pbl%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TASK_LOG%2522%252C%2522taskId%2522%253A%2522preview%2522%257D%252C%257B%2522id%2522%253A%2522clp4f2n6400792067u2wc73c0%2522%252C%2522mode%2522%253A%2522permanent%2522%252C%2522type%2522%253A%2522TERMINAL%2522%252C%2522shellId%2522%253A%2522clp4f2n9h00cyefipg69y7hzf%2522%257D%255D%257D%257D%252C%2522showDevtools%2522%253Atrue%252C%2522showShells%2522%253Atrue%252C%2522showSidebar%2522%253Atrue%252C%2522sidebarPanelSize%2522%253A15%257D)

## Step 1: Create your table

Use whatever technologies you like for this: I went with React and tanstack-table, because that’s what I currently use at work.

```tsx
type Items = {
  ranking: number;
  url: string;
  name: string;
};

const data: Items[] = [
  {
    ranking: 1,
    url: "https://google.com",
    name: "Google",
  },
  {
    ranking: 2,
    url: "https://bing.com",
    name: "Bing",
  },
  {
    ranking: 3,
    url: "https://yahoo.com",
    name: "Yahoo",
  },
];
const columnHelper = createColumnHelper<Items>();
const columns = [
  columnHelper.accessor("ranking", {
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("name", {
    cell: (info) => info.getValue(),
  }),
  columnHelper.accessor("url", {
    cell: (info) => info.getValue(),
  }),
];
export function Table() {
  const table = useReactTable({
    data,
    columns,
    getCoreRowModel: getCoreRowModel(),
  });
  return (
    <div>
      <table>
        <thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {headerGroup.headers.map((header) => (
                <th key={header.id} className="table-head">
                  {header.isPlaceholder
                    ? null
                    : flexRender(
                        header.column.columnDef.header,
                        header.getContext(),
                      )}
                </th>
              ))}
            </tr>
          ))}
        </thead>
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id} className="data-rows">
              {row.getVisibleCells().map((cell) => (
                <td key={cell.id} className="table-cell">
                  {flexRender(cell.column.columnDef.cell, cell.getContext())}
                </td>
              ))}
            </tr>
          ))}
        </tbody>
      </table>
    </div>
  );
}
```
## Step 2: Add an additional column to your table

I added it on the left side, but that’s indifferent. Here, it’s filled with yellow, so you can see it.


![Table with one additional column](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/wwv6z9suscyjo4voe9go.png)


## Step 3: Put your link inside the empty column

In the small column, put the link that every row should lead to inside an anchor tag.

```tsx
...
	<thead>
          {table.getHeaderGroups().map((headerGroup) => (
            <tr key={headerGroup.id}>
              {/*⬇️ add an extra column here */}
              <th key="extra-column" aria-label="Search Engine Links"></th>
              {headerGroup.headers.map((header) => (
                <th key={header.id} className="table-head">
...
        <tbody>
          {table.getRowModel().rows.map((row) => (
            <tr key={row.id} className="data-rows">
              {/*⬇️ add a cell with an empty link for every row
              below the empty column header */}
              <td>
                <a
                  href={row.original.url}
                  aria-label={row.original.url}
                  className="row-link"
                ></a>
              </td>
              {row.getVisibleCells().map((cell) => (
...
```
## Step 4: Position the link so it spans over the whole row

Now, that the link is part of the table, let’s make it overlap over the whole row with CSS:

```css
tr {
  border: 1px solid white;
  position: relative;
  height: 3rem;
}

.row-link {
  position: absolute;
  top: 0;
  left: 0;
  content: "";
  width: 100%;
  height: 3rem;
}

.data-rows:hover {
  background-color: #a8a5a5;
}

```

![Whole clickable rows in yellow](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/rw91ibigg8twcsutdzb1.png)

I added a yellow background and borders to every element, so it’s easier to see. With the absolute positioning in relation to the table row, the anchor tag spans over the whole row and makes it clickable.

![Red outline for better accessibility](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/1hexg658umo8bat3eqvf.png)

By adding an adequate outline to the anchor tag, the focus is also including the whole row and making it therefore great to tab through. 🥳

## ⚠️Notes about Accessibility⚠️

- 🧑🏽‍🦯 The empty column header and the empty anchor tags can be confusing for people using screen readers, this is why I added an aria-attribute to them.
- ⤵️ I customized the outline for focus-visible, so the links are clearly marked as something clickable and accessible over the keyboard.
- 🎨 Mine is of course an example to show the concept - always check for sufficient contrast when choosing your colors.


I love this solution - it's so elegant! Hope this helped you!