CardTable

A responsive table component with CSS Grid, React and Tailwind CSS.

Status
⚑ Live
Vehicle
Cost per Trip
$100
Rating
9.2
Surcharge
$5
Departure
08/01/2023
Arrival
12/06/2023
Count
5
Status
⚑ Live
Vehicle

Bicycle

ground

Cost per Trip
$5
Rating
8.7
Surcharge
$0
Departure
08/01/2023
Arrival
12/06/2027
Count
2
Status
πŸ”œ Soon
Vehicle

Teleporter

instant

Cost per Trip
$1,000,000
Rating
9.9
Surcharge
$1,005
Departure
08/01/2023
Arrival
08/01/2023
Count
0

CardTable is a table component that renders as an ordinary table on larger screensizes and as a collection of cards at smaller screensizes.

Basic Example

Id
1
Status
⚑ Live
Vehicle
Spaceship
Category
space
Cost per Trip
$100
Departure
08/01/2023
Capacity
5
Id
2
Status
⚑ Live
Vehicle
Bicycle
Category
ground
Cost per Trip
$5
Departure
08/01/2023
Capacity
2
Id
3
Status
πŸ”œ Soon
Vehicle
Teleporter
Category
instant
Cost per Trip
$1,000,000
Departure
08/01/3023
Capacity
1
import React, { useState } from "react"
import CardTable from "../CardTable"
const BasicExample = () => {
// Typically this would be data returned from an api
const data = [
{
id: 1,
status: "⚑ Live",
name: "Spaceship",
category: "space",
cost: "$100",
departure: "08/01/2023",
capacity: 5,
},
{
id: 2,
status: "⚑ Live",
name: "Bicycle",
category: "ground",
cost: "$5",
departure: "08/01/2023",
capacity: 2,
},
{
id: 3,
status: "πŸ”œ Soon",
name: "Teleporter",
category: "instant",
cost: "$1,000,000",
departure: "08/01/3023",
capacity: 1,
},
]
// These are column headers for each property key in the data
// It is required that:
// - the data is an array of objects
// - the objects have the same shape
// - the number of keys in the object equal the number of columns
const columns = [
"Id",
"Status",
"Vehicle",
"Category",
"Cost per Trip",
"Departure",
"Capacity",
]
return (
<CardTable
id="BasicExampleTable"
columns={columns}
data={data}
gridTemplateColumns="1fr 2fr 2fr 2fr 2fr 2fr 2fr"
/>
)
}
export default BasicExample

This basic example consumes data and formats it according to the array of column headers defined by the columns prop.

Take note of the gridTemplateColumns prop. That is where we define the styles for the grid that manages the alignment of the table at larger screen sizes. See the MDN Web Docs for more information.

Custom Columns Example

Status
⚑ Live
Vehicle
Cost per Trip
$100
Departure
08/01/2023
Count
5
Status
⚑ Live
Vehicle

Bicycle

ground

Cost per Trip
$5
Departure
08/01/2023
Count
2
Status
πŸ”œ Soon
Vehicle

Teleporter

instant

Cost per Trip
$1,000,000
Departure
08/01/3023
Count
0

Like the basic example. this example consumes data and formats it. However, this time we have a custom function for the columns. Let’s take a look!

In this example, we have state being changed by a dropdown component in the first column. We want to keep state for which dropdown is open and increment the count for each item in a row.

First up is getColumns() which is going to return an array of objects that control what gets rendered in each column on our table. Each column object will have two properties.

First, we have a label property, which is the string for the column head.

Second is an optional render property, which is a function that will render the column’s cell content for that row. If a render function does not exist for that cell, by default it will render the data as a string.

Let’s look at the Status column first.

{
label: "Status",
render: (status) => <div className="font-mono text-gray-700">{status}</div>,
}

In this case, we are using the render function to apply some Tailwind classes to style the text in the cell.

Next, let’s look at the Vehicle column.

{
label: "Vehicle",
render: ({ name, category, link, icon }) => (
<div className="flex flex-wrap">
<div className="text-3xl p-2 border md:ml-2 mr-2 md:mr-4">{icon}</div>
<div className="text-left pt-1">
<p>
<a href={link} className="text-blue-900 italic">
{name}
</a>
</p>
<p className="text-sm text-gray-600">{category}</p>
</div>
</div>
),
}

This time, our render function is accepting props that it is using to render a more complex table cell layout. This is how our data looks for one of these rows:

{
id: 1,
status: "⚑ Live",
data: {
name: "Spaceship",
category: "space",
link: "https://www.youtube.com/watch?v=fGATYOMajig",
icon: "πŸš€",
},
cost: "$100",
departure: "08/01/2023",
count: 5,
}

Last, let’s look at the top getColumns() and the first column which has a dropdown component with a button to increase the number in the count column.

import ItemDropdown from "./ItemDropdown"
const getColumns = ({ activeItem, onToggleDropdown, onAddToCount }) => [
{
label: "",
render: (id) => (
<ItemDropdown
id={id}
onToggle={onToggleDropdown}
onAddToCount={onAddToCount}
activeItem={activeItem}
/>
),
},

Here we are passing some props to manage the state of which row’s dropdown element is active and a method to increment the count value for each row. To see how this works, let’s next look at how we are using CardTable for this.

import React, { useState } from "react"
import CardTable from "../CardTable"
import getColumns from "./getColumns" // custom column rendering function
import renderRows from "./renderRows" // custom row rendering function
import data from "./placeholderData" // typically this would come from an api
const CustomExample = () => {
const [itemData, setItemData] = useState(data)
const [activeItem, setActiveItem] = useState(null)
const onToggleDropdown = ({ id, isActive }) => {
setActiveItem(isActive ? null : id)
}
const onAddToCount = (id) => {
var newData = itemData.map((item) =>
item.id === id ? { ...item, count: item.count + 1 } : item
)
setItemData(newData)
}
return (
<CardTable
id="CustomExampleTable"
columns={getColumns({
onAddToCount,
activeItem,
onToggleDropdown,
})}
data={itemData}
gridTemplateColumns="1fr 2fr 4fr 3fr 3fr 2fr"
/>
)
}
export default CustomExample

Here we see that we are defining activeItem, onToggleDropdown, onAddToCount in a parent component, then use getColumns() to generate the columns array and pass to CardTable via its columns prop.

Custom Card Example

Status
⚑ Live
Vehicle
Cost per Trip
$100
Rating
9.2
Surcharge
$5
Departure
08/01/2023
Arrival
12/06/2023
Count
5
Status
⚑ Live
Vehicle

Bicycle

ground

Cost per Trip
$5
Rating
8.7
Surcharge
$0
Departure
08/01/2023
Arrival
12/06/2027
Count
2
Status
πŸ”œ Soon
Vehicle

Teleporter

instant

Cost per Trip
$1,000,000
Rating
9.9
Surcharge
$1,005
Departure
08/01/2023
Arrival
08/01/2023
Count
0

In this example, we will use a custom function for rendering the cells in each table row. By default, CardTable will use its own internal renderRows() function. Instead of using the default, we can create our own and pass it in as a prop.

const renderRows = ({ data, columns, gridTemplateColumns }) => (
<>
{data.map((row, rowIndex) => {
if (Object.keys(row).length !== columns.length) {
throw new ReferenceError(
"ReferenceError: The number of columns (" +
columns.length +
") and the number of keys (" +
Object.keys(row).length +
") in the row do not match, resulting in an undefined value."
)
}
return (
<tr
className={
"relative max-w-480 md:max-w-none mx-auto block w-full md:grid grid-flow-col py-3 md:py-0 border mb-6 md:mb-0 shadow-md md:shadow-none rounded md:rounded-none" +
(rowIndex > 0 ? " border-t" : "")
}
style={{ gridTemplateColumns }}
>
{Object.keys(row).map((key, colIndex) => {
let cellClassName =
"grid grid-flow-col grid-cols-2 items-center h-full md:grid-cols-1 p-2 grid-cols-2 md:border-l"
let labelClassName = "md:sr-only font-semibold text-left pl-16 pr-2"
let valueClassName = "pl-6 md:p-0 text-left md:text-center"
if (key === "id") {
cellClassName = "grid items-center h-full px-4 md:px-0 pt-2"
valueClassName = "pl-2 md:p-0 text-left md:text-center"
}
if (key === "status") {
cellClassName =
"grid grid-flow-col grid-cols-1 items-center h-full grid-cols-2 md:border-l"
labelClassName = "sr-only"
valueClassName =
"pt-5 pr-8 md:p-0 absolute top-0 right-0 md:static md:text-center font-bold md:font-normal text-xl md:text-base"
}
if (key === "data") {
cellClassName =
"-mt-3 mb-1 md:m-0 grid grid-flow-col grid-cols-1 items-center h-full p-2 grid-cols-2 md:border-l"
labelClassName = "sr-only"
valueClassName = ""
}
if (key === "count") {
cellClassName =
"grid grid-flow-col grid-cols-2 items-center h-full md:grid-cols-1 grid-cols-2 border-l pb-6"
}
return (
<td
className={cellClassName}
key={"row-" + rowIndex + "-col-" + colIndex}
title={typeof row[key] === "string" ? row[key] : ""}
>
<div className={labelClassName}>{columns[colIndex].label}</div>
<div className={valueClassName}>
{columns[colIndex].render
? columns[colIndex].render(row[key])
: row[key]}
</div>
</td>
)
})}
</tr>
)
})}
</>
)
export default renderRows

Using Tailwind utility classes for grid styles, breakpoints, accessibility, alignment and more, we can create a highly customized card layout for smaller screen devices while keeping the table layout for larger screens.

You can view all the code for these examples at github.com/johnpolacek/cardtable