├── .gitignore ├── LICENSE ├── README.md ├── craco.config.js ├── package-lock.json ├── package.json ├── public ├── favicon.ico ├── index.html ├── logo192.png ├── logo512.png ├── manifest.json └── robots.txt ├── src ├── App.js ├── App.test.js ├── Table.js ├── index.css ├── index.js ├── reportWebVitals.js ├── setupTests.js └── shared │ ├── Button.js │ ├── Icons.js │ └── Utils.js └── tailwind.config.js /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # dependencies 4 | /node_modules 5 | /.pnp 6 | .pnp.js 7 | 8 | # testing 9 | /coverage 10 | 11 | # production 12 | /build 13 | 14 | # misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The BSD Zero Clause License (0BSD) 2 | 3 | Copyright (c) 2021 Samuel Liedtke 4 | 5 | Permission to use, copy, modify, and/or distribute this software for any 6 | purpose with or without fee is hereby granted. 7 | 8 | THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH 9 | REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY 10 | AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT, 11 | INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM 12 | LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR 13 | OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR 14 | PERFORMANCE OF THIS SOFTWARE. 15 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # React Table + Tailwind CSS = ❤️ 2 | 3 | Source code for my tutorial on how to build customizable table component with React Table and Tailwind CSS. 4 | 5 | Both parts of the tutorial can be found on my [blog](https://www.samuelliedtke.com/): 6 | 7 | - [Part 1](https://www.samuelliedtke.com/blog/react-table-tutorial-part-1/): Build a fully featured table component step by step 8 | - [Part 2](https://www.samuelliedtke.com/blog/react-table-tutorial-part-2/): Style the table with Tailwind CSS 9 | 10 | Here is how the table component looks like: 11 | 12 | ![image](https://user-images.githubusercontent.com/45269373/122803208-36351600-d2be-11eb-97c5-ca5c3f15d7d1.png) 13 | 14 | ## 📖 Installation 15 | ```shell 16 | $ npm install 17 | $ npm start 18 | # Load the site at http://127.0.0.1:3000 19 | ``` 20 | 21 | ## 🤝 Contributing 22 | Contributions, issues and feature requests are welcome! 23 | 24 | ## ⭐️ Support 25 | Give a ⭐️ if this project helped you! 26 | -------------------------------------------------------------------------------- /craco.config.js: -------------------------------------------------------------------------------- 1 | // craco.config.js 2 | module.exports = { 3 | style: { 4 | postcss: { 5 | plugins: [ 6 | require('tailwindcss'), 7 | require('autoprefixer'), 8 | ], 9 | }, 10 | }, 11 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "react-tailwind-table", 3 | "version": "0.1.0", 4 | "private": true, 5 | "dependencies": { 6 | "@craco/craco": "^6.1.2", 7 | "@heroicons/react": "^1.0.1", 8 | "@tailwindcss/forms": "^0.3.3", 9 | "@testing-library/jest-dom": "^5.14.1", 10 | "@testing-library/react": "^11.2.7", 11 | "@testing-library/user-event": "^12.8.3", 12 | "react": "^17.0.2", 13 | "react-dom": "^17.0.2", 14 | "react-scripts": "4.0.3", 15 | "react-table": "^7.7.0", 16 | "web-vitals": "^1.1.2" 17 | }, 18 | "scripts": { 19 | "start": "craco start", 20 | "build": "craco build", 21 | "test": "craco test", 22 | "eject": "react-scripts eject" 23 | }, 24 | "eslintConfig": { 25 | "extends": [ 26 | "react-app", 27 | "react-app/jest" 28 | ] 29 | }, 30 | "browserslist": { 31 | "production": [ 32 | ">0.2%", 33 | "not dead", 34 | "not op_mini all" 35 | ], 36 | "development": [ 37 | "last 1 chrome version", 38 | "last 1 firefox version", 39 | "last 1 safari version" 40 | ] 41 | }, 42 | "devDependencies": { 43 | "autoprefixer": "^9.8.6", 44 | "postcss": "^7.0.36", 45 | "tailwindcss": "npm:@tailwindcss/postcss7-compat@^2.2.4" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmybutton/react-tailwind-table/b65ad5a97447094644390c88f97ddf2ade620610/public/favicon.ico -------------------------------------------------------------------------------- /public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 12 | 13 | 17 | 18 | 27 | React App 28 | 29 | 30 | 31 |
32 | 42 | 43 | 44 | -------------------------------------------------------------------------------- /public/logo192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmybutton/react-tailwind-table/b65ad5a97447094644390c88f97ddf2ade620610/public/logo192.png -------------------------------------------------------------------------------- /public/logo512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/jimmybutton/react-tailwind-table/b65ad5a97447094644390c88f97ddf2ade620610/public/logo512.png -------------------------------------------------------------------------------- /public/manifest.json: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "React App", 3 | "name": "Create React App Sample", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "logo192.png", 12 | "type": "image/png", 13 | "sizes": "192x192" 14 | }, 15 | { 16 | "src": "logo512.png", 17 | "type": "image/png", 18 | "sizes": "512x512" 19 | } 20 | ], 21 | "start_url": ".", 22 | "display": "standalone", 23 | "theme_color": "#000000", 24 | "background_color": "#ffffff" 25 | } 26 | -------------------------------------------------------------------------------- /public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import Table, { AvatarCell, SelectColumnFilter, StatusPill } from './Table' // new 3 | 4 | const getData = () => { 5 | const data = [ 6 | { 7 | name: 'Jane Cooper', 8 | email: 'jane.cooper@example.com', 9 | title: 'Regional Paradigm Technician', 10 | department: 'Optimization', 11 | status: 'Active', 12 | role: 'Admin', 13 | age: 27, 14 | imgUrl: 'https://images.unsplash.com/photo-1494790108377-be9c29b29330?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 15 | }, 16 | { 17 | name: 'Cody Fisher', 18 | email: 'cody.fisher@example.com', 19 | title: 'Product Directives Officer', 20 | department: 'Intranet', 21 | status: 'Inactive', 22 | role: 'Owner', 23 | age: 43, 24 | imgUrl: 'https://images.unsplash.com/photo-1570295999919-56ceb5ecca61?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 25 | }, 26 | { 27 | name: 'Esther Howard', 28 | email: 'esther.howard@example.com', 29 | title: 'Forward Response Developer', 30 | department: 'Directives', 31 | status: 'Active', 32 | role: 'Member', 33 | age: 32, 34 | imgUrl: 'https://images.unsplash.com/photo-1520813792240-56fc4a3765a7?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 35 | }, 36 | { 37 | name: 'Jenny Wilson', 38 | email: 'jenny.wilson@example.com', 39 | title: 'Central Security Manager', 40 | department: 'Program', 41 | status: 'Offline', 42 | role: 'Member', 43 | age: 29, 44 | imgUrl: 'https://images.unsplash.com/photo-1498551172505-8ee7ad69f235?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 45 | }, 46 | { 47 | name: 'Kristin Watson', 48 | email: 'kristin.watson@example.com', 49 | title: 'Lean Implementation Liaison', 50 | department: 'Mobility', 51 | status: 'Inactive', 52 | role: 'Admin', 53 | age: 36, 54 | imgUrl: 'https://images.unsplash.com/photo-1532417344469-368f9ae6d187?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 55 | }, 56 | { 57 | name: 'Cameron Williamson', 58 | email: 'cameron.williamson@example.com', 59 | title: 'Internal Applications Engineer', 60 | department: 'Security', 61 | status: 'Active', 62 | role: 'Member', 63 | age: 24, 64 | imgUrl: 'https://images.unsplash.com/photo-1566492031773-4f4e44671857?ixlib=rb-1.2.1&ixid=eyJhcHBfaWQiOjEyMDd9&auto=format&fit=facearea&facepad=4&w=256&h=256&q=60', 65 | }, 66 | ] 67 | return [...data, ...data, ...data] 68 | } 69 | 70 | function App() { 71 | 72 | const columns = React.useMemo(() => [ 73 | { 74 | Header: "Name", 75 | accessor: 'name', 76 | Cell: AvatarCell, 77 | imgAccessor: "imgUrl", 78 | emailAccessor: "email", 79 | }, 80 | { 81 | Header: "Title", 82 | accessor: 'title', 83 | }, 84 | { 85 | Header: "Status", 86 | accessor: 'status', 87 | Cell: StatusPill, 88 | }, 89 | { 90 | Header: "Age", 91 | accessor: 'age', 92 | }, 93 | { 94 | Header: "Role", 95 | accessor: 'role', 96 | Filter: SelectColumnFilter, // new 97 | filter: 'includes', 98 | }, 99 | ], []) 100 | 101 | const data = React.useMemo(() => getData(), []) 102 | 103 | return ( 104 |
105 |
106 |
107 |

React Table + Tailwind CSS = ❤

108 |
109 |
110 | 111 | 112 | 113 | 114 | ); 115 | } 116 | 117 | export default App; 118 | -------------------------------------------------------------------------------- /src/App.test.js: -------------------------------------------------------------------------------- 1 | import { render, screen } from '@testing-library/react'; 2 | import App from './App'; 3 | 4 | test('renders learn react link', () => { 5 | render(); 6 | const linkElement = screen.getByText(/learn react/i); 7 | expect(linkElement).toBeInTheDocument(); 8 | }); 9 | -------------------------------------------------------------------------------- /src/Table.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { useTable, useFilters, useGlobalFilter, useAsyncDebounce, useSortBy, usePagination } from 'react-table' 3 | import { ChevronDoubleLeftIcon, ChevronLeftIcon, ChevronRightIcon, ChevronDoubleRightIcon } from '@heroicons/react/solid' 4 | import { Button, PageButton } from './shared/Button' 5 | import { classNames } from './shared/Utils' 6 | import { SortIcon, SortUpIcon, SortDownIcon } from './shared/Icons' 7 | 8 | // Define a default UI for filtering 9 | function GlobalFilter({ 10 | preGlobalFilteredRows, 11 | globalFilter, 12 | setGlobalFilter, 13 | }) { 14 | const count = preGlobalFilteredRows.length 15 | const [value, setValue] = React.useState(globalFilter) 16 | const onChange = useAsyncDebounce(value => { 17 | setGlobalFilter(value || undefined) 18 | }, 200) 19 | 20 | return ( 21 | 34 | ) 35 | } 36 | 37 | // This is a custom filter UI for selecting 38 | // a unique option from a list 39 | export function SelectColumnFilter({ 40 | column: { filterValue, setFilter, preFilteredRows, id, render }, 41 | }) { 42 | // Calculate the options for filtering 43 | // using the preFilteredRows 44 | const options = React.useMemo(() => { 45 | const options = new Set() 46 | preFilteredRows.forEach(row => { 47 | options.add(row.values[id]) 48 | }) 49 | return [...options.values()] 50 | }, [id, preFilteredRows]) 51 | 52 | // Render a multi-select box 53 | return ( 54 | 73 | ) 74 | } 75 | 76 | export function StatusPill({ value }) { 77 | const status = value ? value.toLowerCase() : "unknown"; 78 | 79 | return ( 80 | 90 | {status} 91 | 92 | ); 93 | }; 94 | 95 | export function AvatarCell({ value, column, row }) { 96 | return ( 97 |
98 |
99 | 100 |
101 |
102 |
{value}
103 |
{row.original[column.emailAccessor]}
104 |
105 |
106 | ) 107 | } 108 | 109 | function Table({ columns, data }) { 110 | // Use the state and functions returned from useTable to build your UI 111 | const { 112 | getTableProps, 113 | getTableBodyProps, 114 | headerGroups, 115 | prepareRow, 116 | page, // Instead of using 'rows', we'll use page, 117 | // which has only the rows for the active page 118 | 119 | // The rest of these things are super handy, too ;) 120 | canPreviousPage, 121 | canNextPage, 122 | pageOptions, 123 | pageCount, 124 | gotoPage, 125 | nextPage, 126 | previousPage, 127 | setPageSize, 128 | 129 | state, 130 | preGlobalFilteredRows, 131 | setGlobalFilter, 132 | } = useTable({ 133 | columns, 134 | data, 135 | }, 136 | useFilters, // useFilters! 137 | useGlobalFilter, 138 | useSortBy, 139 | usePagination, // new 140 | ) 141 | 142 | // Render the UI for your table 143 | return ( 144 | <> 145 |
146 | 151 | {headerGroups.map((headerGroup) => 152 | headerGroup.headers.map((column) => 153 | column.Filter ? ( 154 |
155 | {column.render("Filter")} 156 |
157 | ) : null 158 | ) 159 | )} 160 |
161 | {/* table */} 162 |
163 |
164 |
165 |
166 |
167 | 168 | {headerGroups.map(headerGroup => ( 169 | 170 | {headerGroup.headers.map(column => ( 171 | // Add the sorting props to control sorting. For this example 172 | // we can add them into the header props 173 | 192 | ))} 193 | 194 | ))} 195 | 196 | 200 | {page.map((row, i) => { // new 201 | prepareRow(row) 202 | return ( 203 | 204 | {row.cells.map(cell => { 205 | return ( 206 | 216 | ) 217 | })} 218 | 219 | ) 220 | })} 221 | 222 |
178 |
179 | {column.render('Header')} 180 | {/* Add a sort direction indicator */} 181 | 182 | {column.isSorted 183 | ? column.isSortedDesc 184 | ? 185 | : 186 | : ( 187 | 188 | )} 189 | 190 |
191 |
211 | {cell.column.Cell.name === "defaultRenderer" 212 | ?
{cell.render('Cell')}
213 | : cell.render('Cell') 214 | } 215 |
223 |
224 |
225 | 226 | 227 | {/* Pagination */} 228 |
229 |
230 | 231 | 232 |
233 |
234 |
235 | 236 | Page {state.pageIndex + 1} of {pageOptions.length} 237 | 238 | 254 |
255 |
256 | 288 |
289 |
290 |
291 | 292 | ) 293 | } 294 | 295 | export default Table; -------------------------------------------------------------------------------- /src/index.css: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | @tailwind components; 3 | @tailwind utilities; 4 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import ReactDOM from 'react-dom'; 3 | import './index.css'; 4 | import App from './App'; 5 | import reportWebVitals from './reportWebVitals'; 6 | 7 | ReactDOM.render( 8 | 9 | 10 | , 11 | document.getElementById('root') 12 | ); 13 | 14 | // If you want to start measuring performance in your app, pass a function 15 | // to log results (for example: reportWebVitals(console.log)) 16 | // or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals 17 | reportWebVitals(); 18 | -------------------------------------------------------------------------------- /src/reportWebVitals.js: -------------------------------------------------------------------------------- 1 | const reportWebVitals = onPerfEntry => { 2 | if (onPerfEntry && onPerfEntry instanceof Function) { 3 | import('web-vitals').then(({ getCLS, getFID, getFCP, getLCP, getTTFB }) => { 4 | getCLS(onPerfEntry); 5 | getFID(onPerfEntry); 6 | getFCP(onPerfEntry); 7 | getLCP(onPerfEntry); 8 | getTTFB(onPerfEntry); 9 | }); 10 | } 11 | }; 12 | 13 | export default reportWebVitals; 14 | -------------------------------------------------------------------------------- /src/setupTests.js: -------------------------------------------------------------------------------- 1 | // jest-dom adds custom jest matchers for asserting on DOM nodes. 2 | // allows you to do things like: 3 | // expect(element).toHaveTextContent(/react/i) 4 | // learn more: https://github.com/testing-library/jest-dom 5 | import '@testing-library/jest-dom'; 6 | -------------------------------------------------------------------------------- /src/shared/Button.js: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { classNames } from './Utils' 3 | 4 | export function Button({ children, className, ...rest }) { 5 | return ( 6 | 17 | ) 18 | } 19 | 20 | export function PageButton({ children, className, ...rest }) { 21 | return ( 22 | 33 | ) 34 | } 35 | 36 | -------------------------------------------------------------------------------- /src/shared/Icons.js: -------------------------------------------------------------------------------- 1 | export function SortIcon({ className }) { 2 | return ( 3 | 4 | ) 5 | } 6 | 7 | export function SortUpIcon({ className }) { 8 | return ( 9 | 10 | ) 11 | } 12 | 13 | export function SortDownIcon({ className }) { 14 | return ( 15 | 16 | ) 17 | } -------------------------------------------------------------------------------- /src/shared/Utils.js: -------------------------------------------------------------------------------- 1 | export function classNames(...classes) { 2 | return classes.filter(Boolean).join(" "); 3 | } -------------------------------------------------------------------------------- /tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | purge: ['./src/**/*.{js,jsx,ts,tsx}', './public/index.html'], 3 | darkMode: false, // or 'media' or 'class' 4 | theme: { 5 | extend: {}, 6 | }, 7 | variants: { 8 | extend: {}, 9 | }, 10 | plugins: [ 11 | require('@tailwindcss/forms'), 12 | ], 13 | } 14 | --------------------------------------------------------------------------------