Picture this: You're building a data-heavy dashboard. Everything's going smooth—rows load fast, columns sort perfectly, filters feel snappy. Then comes the footer. Your designer hands you a mockup with custom pagination buttons, row counters with specific copy, and maybe a "rows per page" selector styled to match your brand.
You crack open your table library's docs. Option 1: A dozen boolean flags scattered across props—showPagination, paginationPosition, paginationStyle. You toggle them all, but the result? Close, but not quite right. The spacing's off, the icons don't match, and that "Showing X-Y of Z" text is hardcoded.
Option 2 (looking at you, TanStack): Rebuild the entire table UI from scratch, including headers, cells, and yes—footers. Sure, you get total control, but now you're maintaining layout logic, accessibility, and responsive behavior yourself. That "lightweight headless library" suddenly feels heavyweight.
There's a better way: custom footer renderers. One prop. Full control. Zero compromises.
The Problem: Footers Are Personal
Table footers aren't generic. They're where your brand shows through, where your users interact with data navigation, and where you communicate important information. Every app has different needs:
Design Requirements
Your pagination needs to match your design system—specific colors, spacing, icons, animations, and hover states that scream "this is our app."
Feature Combinations
Maybe you need row counts AND page numbers AND a "jump to page" input AND an export button. Good luck finding flags for that exact combo.
Custom Interactions
Want to track analytics when users change pages? Show loading states? Display contextual help? These scenarios fall outside the flag-based model entirely.
Accessibility Needs
Screen reader announcements, keyboard navigation patterns, ARIA labels—these require direct access to the DOM and behavior, not just styling flags.
Two Approaches to Footer Customization
When it comes to customizing table footers, libraries typically offer one of two paths. Let's break down the trade-offs:
Approach 1: The Feature Flag Maze
This is the "configure your way there" approach. The library provides dozens of boolean props and configuration options:
1<DataTable2 showPagination={true}3 paginationPosition="bottom"4 paginationAlign="right"5 showPageNumbers={true}6 showRowCount={true}7 rowCountFormat="Showing {start}-{end} of {total}"8 paginationSize="large"9 showFirstLastButtons={true}10 showPreviousNextButtons={true}11 pageNumbersToShow={5}12 // ... and 20 more pagination props13/>
The Good
- Quick starts: Toggle a few flags and you've got basic pagination working
- Consistent styling: The library handles the basic look and feel
- Less code: No need to write your own pagination logic
The Bad
- Limited combinations: Want that one specific layout? If it's not in the flags, you're stuck
- Style surgery: Overriding the default styles often requires !important wars and deep CSS selectors
- Prop explosion: More features = more flags = more complexity in your component
Approach 2: The TanStack Way (Build Everything)
TanStack Table takes a "headless" approach—it provides the logic and state management, but you build all the UI yourself, including the entire table structure:
1import { useReactTable, getCoreRowModel, getPaginationRowModel } from '@tanstack/react-table'23function MyTable() {4 const table = useReactTable({5 data,6 columns,7 getCoreRowModel: getCoreRowModel(),8 getPaginationRowModel: getPaginationRowModel(),9 })1011 return (12 <>13 {/* Build entire table structure */}14 <table>15 <thead>16 {table.getHeaderGroups().map(headerGroup => (17 <tr key={headerGroup.id}>18 {headerGroup.headers.map(header => (19 <th key={header.id}>20 {/* Custom header rendering */}21 </th>22 ))}23 </tr>24 ))}25 </thead>26 <tbody>27 {/* Custom body rendering */}28 </tbody>29 </table>3031 {/* Build your own footer from scratch */}32 <div className="pagination">33 <button34 onClick={() => table.previousPage()}35 disabled={!table.getCanPreviousPage()}36 >37 Previous38 </button>39 <span>40 Page {table.getState().pagination.pageIndex + 1} of{' '}41 {table.getPageCount()}42 </span>43 <button44 onClick={() => table.nextPage()}45 disabled={!table.getCanNextPage()}46 >47 Next48 </button>49 </div>50 </>51 )52}
The Good
- Ultimate control: Every pixel is yours to command
- Framework agnostic: Use any styling solution, any component library
- Zero style conflicts: No library CSS to override
The Bad
- Build EVERYTHING: Table structure, headers, rows, cells, footer—all on you
- Complexity creep: Simple tables turn into hundreds of lines of UI code
- Maintenance burden: You're now maintaining table layout, accessibility, responsive behavior, and more
The reality: TanStack is powerful, but overkill for most apps. You wanted to customize the footer, not rebuild the entire table from scratch.
The Solution: Custom Footer Renderers
Enter the footer renderer pattern—the sweet spot between flags and full control. Here's how it works in Simple Table:
1<SimpleTable2 defaultHeaders={headers}3 rows={data}4 rowIdAccessor="id"5 shouldPaginate={true}6 rowsPerPage={10}7 footerRenderer={({8 currentPage,9 totalPages,10 startRow,11 endRow,12 totalRows,13 hasPrevPage,14 hasNextPage,15 onPrevPage,16 onNextPage,17 onPageChange,18 }) => (19 <div className="custom-footer">20 {/* Build exactly the footer YOU want */}21 <div className="row-info">22 Showing {startRow}-{endRow} of {totalRows} items23 </div>2425 <div className="pagination-controls">26 <button27 onClick={onPrevPage}28 disabled={!hasPrevPage}29 className="nav-button"30 >31 ← Previous32 </button>3334 {/* Render custom page numbers */}35 {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (36 <button37 key={page}38 onClick={() => onPageChange(page)}39 className={currentPage === page ? 'active' : ''}40 >41 {page}42 </button>43 ))}4445 <button46 onClick={onNextPage}47 disabled={!hasNextPage}48 className="nav-button"49 >50 Next →51 </button>52 </div>53 </div>54 )}55/>
Why This Approach Wins
One Prop
No flag maze. Just pass a render function to footerRenderer and you're done.
Full Control
Build any footer design you want. Custom buttons, dropdowns, analytics—it's all just JSX.
Logic Handled
Simple Table manages pagination state. You just render the UI with the data it provides.
Style Freedom
Use CSS-in-JS, Tailwind, CSS Modules, or plain CSS. Your footer, your rules.
Easy Testing
Footer logic is just a React component. Test it like any other component.
Scope Focused
Unlike TanStack, you only customize what you need—the table itself still "just works."
Real-World Footer Scenarios
Let's look at some scenarios where custom footer renderers shine:
📊 Analytics Dashboard
Need: Show "Viewing X-Y of Z results" with an Export button and a "Rows per page" dropdown.
1footerRenderer={({ startRow, endRow, totalRows, ...props }) => (2 <div style={{ display: 'flex', justifyContent: 'space-between', padding: '16px' }}>3 <span>Viewing {startRow}-{endRow} of {totalRows} results</span>4 <div>5 <select onChange={handleRowsPerPageChange}>6 <option value="10">10 per page</option>7 <option value="25">25 per page</option>8 <option value="50">50 per page</option>9 </select>10 <button onClick={handleExport}>Export CSV</button>11 </div>12 </div>13)}
🛒 E-Commerce Admin
Need: Compact pagination with tooltips and accessibility labels.
1footerRenderer={({ currentPage, totalPages, onPageChange, ...props }) => (2 <nav aria-label="Product pagination">3 {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (4 <button5 key={page}6 onClick={() => onPageChange(page)}7 aria-current={currentPage === page ? 'page' : undefined}8 aria-label={`Go to page ${page}`}9 title={`Page ${page} of ${totalPages}`}10 >11 {page}12 </button>13 ))}14 </nav>15)}
📱 Mobile-First App
Need: Stacked layout on mobile, horizontal on desktop. Show "Load More" instead of page numbers.
1footerRenderer={({ hasNextPage, onNextPage, startRow, endRow, totalRows }) => (2 <div className="footer-mobile-responsive">3 <div className="row-count">4 {startRow}-{endRow} of {totalRows}5 </div>6 {hasNextPage && (7 <button onClick={onNextPage} className="load-more">8 Load More Results9 </button>10 )}11 </div>12)}1314// CSS15.footer-mobile-responsive {16 display: flex;17 flex-direction: column;18 gap: 12px;19}2021@media (min-width: 768px) {22 .footer-mobile-responsive {23 flex-direction: row;24 justify-content: space-between;25 }26}
The Verdict: Why Footer Renderers Win
Let's put all three approaches side-by-side:
| Criterion | Feature Flags | TanStack (Build All) | Footer Renderer | 
|---|---|---|---|
| Setup Time | ⚡ Fast | 🐌 Slow | ⚡ Fast | 
| Customization | ❌ Limited | ✅ Unlimited | ✅ Unlimited | 
| Code Complexity | 📄 Low | 📚 Very High | 📄 Low-Medium | 
| Maintenance | ⚠️ Fight with flags | ⚠️ Own everything | ✅ Just the footer | 
| Table Features | ✅ Built-in | ❌ Build yourself | ✅ Built-in | 
| Best For | Simple needs only | Total control freaks | Most real-world apps | 
Getting Started with Footer Renderers
Ready to try footer renderers? Here's a complete working example:
1import { SimpleTable } from "simple-table-core";2import "simple-table-core/styles.css";34const headers = [5 { accessor: "id", label: "ID", width: 60 },6 { accessor: "name", label: "Name", width: "1fr" },7 { accessor: "email", label: "Email", width: "1fr" },8];910const MyDataTable = ({ data }) => {11 return (12 <SimpleTable13 defaultHeaders={headers}14 rows={data}15 rowIdAccessor="id"16 shouldPaginate={true}17 rowsPerPage={10}18 footerRenderer={({19 currentPage,20 totalPages,21 startRow,22 endRow,23 totalRows,24 hasPrevPage,25 hasNextPage,26 onPrevPage,27 onNextPage,28 onPageChange,29 }) => (30 <div style={{31 display: 'flex',32 alignItems: 'center',33 justifyContent: 'space-between',34 padding: '16px',35 borderTop: '1px solid #e5e7eb',36 }}>37 {/* Row counter */}38 <span style={{ fontSize: '14px', color: '#6b7280' }}>39 Showing {startRow}-{endRow} of {totalRows} results40 </span>4142 {/* Pagination controls */}43 <div style={{ display: 'flex', gap: '8px' }}>44 <button45 onClick={onPrevPage}46 disabled={!hasPrevPage}47 style={{48 padding: '8px 16px',49 border: '1px solid #d1d5db',50 borderRadius: '6px',51 background: hasPrevPage ? 'white' : '#f3f4f6',52 cursor: hasPrevPage ? 'pointer' : 'not-allowed',53 }}54 >55 Previous56 </button>5758 {Array.from({ length: totalPages }, (_, i) => i + 1).map(page => (59 <button60 key={page}61 onClick={() => onPageChange(page)}62 style={{63 padding: '8px 12px',64 border: '1px solid #d1d5db',65 borderRadius: '6px',66 background: currentPage === page ? '#3b82f6' : 'white',67 color: currentPage === page ? 'white' : '#374151',68 cursor: 'pointer',69 fontWeight: currentPage === page ? '600' : '400',70 }}71 >72 {page}73 </button>74 ))}7576 <button77 onClick={onNextPage}78 disabled={!hasNextPage}79 style={{80 padding: '8px 16px',81 border: '1px solid #d1d5db',82 borderRadius: '6px',83 background: hasNextPage ? 'white' : '#f3f4f6',84 cursor: hasNextPage ? 'pointer' : 'not-allowed',85 }}86 >87 Next88 </button>89 </div>90 </div>91 )}92 />93 );94};
That's it! Simple Table handles all the pagination logic, state management, and data slicing. You just focus on rendering the UI you want.
The Bottom Line
Building tables doesn't have to be a compromise between "limited customization" and "rebuild everything from scratch." Custom footer renderers give you the best of both worlds:
- Full design freedom without fighting with flags or overriding styles
- Minimal code complexity compared to building tables from scratch
- All table features included (sorting, filtering, selection, etc.)
- Easy to maintain because you only own the footer, not the whole table
Footer UI is personal. It's where your brand shows, where your users navigate, and where design requirements get specific. Don't settle for close enough—take full control without the overhead.
Try it yourself: Simple Table's footer renderer gives you the flexibility you need with none of the complexity you don't. One prop. Total control. Zero compromises.