CardTable
A responsive table component with CSS Grid, React and Tailwind CSS.
Status | Vehicle | Cost per Trip | Rating | Surcharge | Departure | Arrival | Count | |
---|---|---|---|---|---|---|---|---|
Status β‘ Live | Vehicle space | Cost per Trip $100 | Rating 9.2 | Surcharge $5 | Departure 08/01/2023 | Arrival 12/06/2023 | Count 5 | |
Status β‘ Live | Vehicle ground | Cost per Trip $5 | Rating 8.7 | Surcharge $0 | Departure 08/01/2023 | Arrival 12/06/2027 | Count 2 | |
Status π Soon | Vehicle 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 | Status | Vehicle | Category | Cost per Trip | Departure | Capacity |
---|---|---|---|---|---|---|
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 apiconst 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 columnsconst columns = ["Id","Status","Vehicle","Category","Cost per Trip","Departure","Capacity",]return (<CardTableid="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 | Vehicle | Cost per Trip | Departure | Count | |
---|---|---|---|---|---|
Status β‘ Live | Vehicle space | Cost per Trip $100 | Departure 08/01/2023 | Count 5 | |
Status β‘ Live | Vehicle ground | Cost per Trip $5 | Departure 08/01/2023 | Count 2 | |
Status π Soon | Vehicle 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) => (<ItemDropdownid={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 functionimport renderRows from "./renderRows" // custom row rendering functionimport data from "./placeholderData" // typically this would come from an apiconst 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 (<CardTableid="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 | Vehicle | Cost per Trip | Rating | Surcharge | Departure | Arrival | Count | |
---|---|---|---|---|---|---|---|---|
Status β‘ Live | Vehicle space | Cost per Trip $100 | Rating 9.2 | Surcharge $5 | Departure 08/01/2023 | Arrival 12/06/2023 | Count 5 | |
Status β‘ Live | Vehicle ground | Cost per Trip $5 | Rating 8.7 | Surcharge $0 | Departure 08/01/2023 | Arrival 12/06/2027 | Count 2 | |
Status π Soon | Vehicle 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 (<trclassName={"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 (<tdclassName={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