Free table components created using React.js and Tailwind.css.
A table in card layout with search, sorting and pagination. The component is created using different popular libraries to handle individual elements which can be replaced with the ones of your choice. Names of these libraries can be found under the requirements section below.
1import { faker } from "@faker-js/faker";2import {3 useTable,4 useGlobalFilter,5 useSortBy,6 usePagination,7} from "react-table";8import { useMemo, Fragment, useCallback } from "react";9import {10 FaSearch,11 FaChevronDown,12 FaCheck,13 FaChevronLeft,14 FaChevronRight,15 FaSortUp,16 FaSortDown,17} from "react-icons/fa";18import { Listbox, Transition } from "@headlessui/react";1920function Avatar({ src, alt = "avatar" }) {21 return (22 <img src={src} alt={alt} className="w-8 h-8 rounded-full object-cover" />23 );24}25const generateData = (numberOfRows = 25) =>26 [...Array(numberOfRows)].map(() => ({27 name: faker.name.fullName(),28 image: faker.image.avatar(),29 accountNumber: faker.finance.account(8),30 accountName: faker.finance.accountName(),31 amount: faker.finance.amount(500, 1e4, 2, "$"),32 }));33const getColumns = () => [34 {35 Header: "Name",36 accessor: "name",37 width: "300px",38 Cell: ({ row, value }) => {39 return (40 <div className="flex gap-2 items-center">41 <Avatar src={row.original.image} alt={`${value}'s Avatar`} />42 <div>{value}</div>43 </div>44 );45 },46 },47 {48 Header: "Account Number",49 accessor: "accountNumber",50 },51 {52 Header: "Account Name",53 accessor: "accountName",54 },55 {56 Header: "Amount",57 accessor: "amount",58 },59];6061function InputGroup7({62 label,63 name,64 value,65 onChange,66 type = "text",67 decoration,68 className = "",69 inputClassName = "",70 decorationClassName = "",71 disabled,72}) {73 return (74 <div75 className={`flex flex-row-reverse items-stretch w-full rounded-xl overflow-hidden bg-white shadow-[0_4px_10px_rgba(0,0,0,0.03)] ${className}`}76 >77 <input78 id={name}79 name={name}80 value={value}81 type={type}82 placeholder={label}83 aria-label={label}84 onChange={onChange}85 className={`peer block w-full p-3 text-gray-600 focus:outline-none focus:ring-0 appearance-none ${86 disabled ? "bg-gray-200" : ""87 } ${inputClassName}`}88 disabled={disabled}89 />90 <div91 className={`flex items-center pl-3 py-3 text-gray-600 ${92 disabled ? "bg-gray-200" : ""93 } ${decorationClassName}`}94 >95 {decoration}96 </div>97 </div>98 );99}100101function GlobalSearchFilter1({102 globalFilter,103 setGlobalFilter,104 className = "",105}) {106 return (107 <InputGroup7108 name="search"109 value={globalFilter || ""}110 onChange={(e) => setGlobalFilter(e.target.value)}111 label="Search"112 decoration={<FaSearch size="1rem" className="text-gray-400" />}113 className={className}114 />115 );116}117118function SelectMenu1({ value, setValue, options, className = "", disabled }) {119 const selectedOption = useMemo(120 () => options.find((o) => o.id === value),121 [options, value]122 );123 return (124 <Listbox value={value} onChange={setValue} disabled={disabled}>125 <div className={`relative w-full ${className}`}>126 <Listbox.Button127 className={`relative w-full rounded-xl py-3 pl-3 pr-10 text-base text-gray-700 text-left shadow-[0_4px_10px_rgba(0,0,0,0.03)] focus:outline-none128 ${129 disabled130 ? "bg-gray-200 cursor-not-allowed"131 : "bg-white cursor-default"132 }133134 `}135 >136 <span className="block truncate">{selectedOption.caption}</span>137 <span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">138 <FaChevronDown139 size="0.80rem"140 className="text-gray-400"141 aria-hidden="true"142 />143 </span>144 </Listbox.Button>145 <Transition146 as={Fragment}147 leave="transition ease-in duration-100"148 leaveFrom="opacity-100"149 leaveTo="opacity-0"150 >151 <Listbox.Options className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-xl bg-white text-base shadow-[0_4px_10px_rgba(0,0,0,0.03)] focus:outline-none">152 {options.map((option) => (153 <Listbox.Option154 key={option.id}155 className={({ active }) =>156 `relative cursor-default select-none py-3 pl-10 pr-4 ${157 active ? "bg-red-100" : ""158 }`159 }160 value={option.id}161 >162 {({ selected }) => (163 <>164 <span165 className={`block truncate ${166 selected ? "font-medium" : "font-normal"167 }`}168 >169 {option.caption}170 </span>171 {selected ? (172 <span className="absolute inset-y-0 left-0 flex items-center pl-3 text-red-400">173 <FaCheck size="0.5rem" aria-hidden="true" />174 </span>175 ) : null}176 </>177 )}178 </Listbox.Option>179 ))}180 </Listbox.Options>181 </Transition>182 </div>183 </Listbox>184 );185}186187function Button2({ content, onClick, active, disabled }) {188 return (189 <button190 className={`flex flex-col cursor-pointer items-center justify-center w-9 h-9 shadow-[0_4px_10px_rgba(0,0,0,0.03)] text-sm font-normal transition-colors rounded-lg191 ${active ? "bg-red-500 text-white" : "text-red-500"}192 ${193 !disabled194 ? "bg-white hover:bg-red-500 hover:text-white"195 : "text-red-300 bg-white cursor-not-allowed"196 }197 `}198 onClick={onClick}199 disabled={disabled}200 >201 {content}202 </button>203 );204}205206function PaginationNav1({207 gotoPage,208 canPreviousPage,209 canNextPage,210 pageCount,211 pageIndex,212}) {213 const renderPageLinks = useCallback(() => {214 if (pageCount === 0) return null;215 const visiblePageButtonCount = 3;216 let numberOfButtons =217 pageCount < visiblePageButtonCount ? pageCount : visiblePageButtonCount;218 const pageIndices = [pageIndex];219 numberOfButtons--;220 [...Array(numberOfButtons)].forEach((_item, itemIndex) => {221 const pageNumberBefore = pageIndices[0] - 1;222 const pageNumberAfter = pageIndices[pageIndices.length - 1] + 1;223 if (224 pageNumberBefore >= 0 &&225 (itemIndex < numberOfButtons / 2 || pageNumberAfter > pageCount - 1)226 ) {227 pageIndices.unshift(pageNumberBefore);228 } else {229 pageIndices.push(pageNumberAfter);230 }231 });232 return pageIndices.map((pageIndexToMap) => (233 <li key={pageIndexToMap}>234 <Button2235 content={pageIndexToMap + 1}236 onClick={() => gotoPage(pageIndexToMap)}237 active={pageIndex === pageIndexToMap}238 />239 </li>240 ));241 }, [pageCount, pageIndex]);242 return (243 <ul className="flex gap-2">244 <li>245 <Button2246 content={247 <div className="flex ml-1">248 <FaChevronLeft size="0.6rem" />249 <FaChevronLeft size="0.6rem" className="-translate-x-1/2" />250 </div>251 }252 onClick={() => gotoPage(0)}253 disabled={!canPreviousPage}254 />255 </li>256 {renderPageLinks()}257 <li>258 <Button2259 content={260 <div className="flex ml-1">261 <FaChevronRight size="0.6rem" />262 <FaChevronRight size="0.6rem" className="-translate-x-1/2" />263 </div>264 }265 onClick={() => gotoPage(pageCount - 1)}266 disabled={!canNextPage}267 />268 </li>269 </ul>270 );271}272273function TableComponent({274 getTableProps,275 headerGroups,276 getTableBodyProps,277 rows,278 prepareRow,279}) {280 return (281 <div className="w-full min-w-[30rem] p-4 bg-white rounded-xl shadow-[0_4px_10px_rgba(0,0,0,0.03)]">282 <table {...getTableProps()}>283 <thead>284 {headerGroups.map((headerGroup) => (285 <tr {...headerGroup.getHeaderGroupProps()}>286 {headerGroup.headers.map((column) => (287 <th288 {...column.getHeaderProps(column.getSortByToggleProps())}289 className="px-3 text-start text-xs font-light uppercase cursor-pointer"290 style={{ width: column.width }}291 >292 <div className="flex gap-2 items-center">293 <div className="text-gray-600">294 {column.render("Header")}295 </div>296 <div className="flex flex-col">297 <FaSortUp298 className={`text-sm translate-y-1/2 ${299 column.isSorted && !column.isSortedDesc300 ? "text-red-400"301 : "text-gray-300"302 }`}303 />304 <FaSortDown305 className={`text-sm -translate-y-1/2 ${306 column.isSortedDesc ? "text-red-400" : "text-gray-300"307 }`}308 />309 </div>310 </div>311 </th>312 ))}313 </tr>314 ))}315 </thead>316 <tbody {...getTableBodyProps()}>317 {rows.map((row, i) => {318 prepareRow(row);319 return (320 <tr {...row.getRowProps()} className="hover:bg-gray-100">321 {row.cells.map((cell) => {322 return (323 <td324 {...cell.getCellProps()}325 className="p-3 text-sm font-normal text-gray-700 first:rounded-l-lg last:rounded-r-lg"326 >327 {cell.render("Cell")}328 </td>329 );330 })}331 </tr>332 );333 })}334 </tbody>335 </table>336 </div>337 );338}339function Table1() {340 const data = useMemo(() => generateData(100), []);341 const columns = useMemo(getColumns, []);342 const {343 getTableProps,344 getTableBodyProps,345 headerGroups,346 prepareRow,347 state,348 setGlobalFilter,349 page: rows,350 canPreviousPage,351 canNextPage,352 pageCount,353 gotoPage,354 setPageSize,355 state: { pageIndex, pageSize },356 } = useTable(357 {358 columns,359 data,360 initialState: { pageSize: 5 },361 },362 useGlobalFilter,363 useSortBy,364 usePagination365 );366 return (367 <div className="flex flex-col gap-4">368 <div className="flex flex-col sm:flex-row justify-between gap-2">369 <GlobalSearchFilter1370 className="sm:w-64"371 globalFilter={state.globalFilter}372 setGlobalFilter={setGlobalFilter}373 />374 <SelectMenu1375 className="sm:w-44"376 value={pageSize}377 setValue={setPageSize}378 options={[379 { id: 5, caption: "5 items per page" },380 { id: 10, caption: "10 items per page" },381 { id: 20, caption: "20 items per page" },382 ]}383 />384 </div>385 <TableComponent386 getTableProps={getTableProps}387 headerGroups={headerGroups}388 getTableBodyProps={getTableBodyProps}389 rows={rows}390 prepareRow={prepareRow}391 />392 <div className="flex justify-center">393 <PaginationNav1394 gotoPage={gotoPage}395 canPreviousPage={canPreviousPage}396 canNextPage={canNextPage}397 pageCount={pageCount}398 pageIndex={pageIndex}399 />400 </div>401 </div>402 );403}404405function Table1Presentation() {406 return (407 <div className="flex flex-col overflow-auto py-4 sm:py-0">408 <Table1 />409 </div>410 );411}412413export { Table1Presentation };414