Fliplet.UI.Table
Fliplet.UI.Table is a powerful and flexible table component that provides features like sorting, searching, pagination, row selection, expandable rows, and custom rendering capabilities.
Install
Add the fliplet-table dependency to your screen or app libraries.
Basic Usage
const table = new Fliplet.UI.Table({
target: '#table-container',
columns: [
{ name: 'ID', field: 'id' },
{ name: 'Name', field: 'name' }
],
data: [
{ id: 1, name: 'Item 1' },
{ id: 2, name: 'Item 2' }
]
});
Configuration Options
Main Options
| Option | Type | Default | Description |
|---|---|---|---|
target |
String | Element | Required | CSS selector or DOM element where the table will be rendered |
className |
String | '' |
Additional CSS class name(s) to add to the table |
columns |
Array | Required | Array of column definitions |
data |
Array | [] |
Array of data objects to display in the table |
searchable |
Boolean | false |
Enable global search functionality |
pagination |
Object | Boolean | false |
Enable and configure pagination. Set to false to disable pagination entirely |
selection |
Object | undefined |
Enable and configure row selection |
expandable |
Object | undefined |
Enable and configure expandable rows |
Column Definition
Each column in the columns array can have the following properties:
| Property | Type | Default | Description |
|---|---|---|---|
name |
String | Required | Display name of the column |
field |
String | Required | Field name in the data object |
sortable |
Boolean | false |
Enable sorting for this column |
searchable |
Boolean | false |
Include this column in global search |
render |
Function | undefined |
Custom render function for cell content |
sortFn |
Function | undefined |
Custom sort function for this column |
width |
String | undefined |
Fixed column width (e.g., '100px', '60px'). Columns without a width use flex to fill available space |
resizable |
Boolean | true |
Enable column resizing by dragging the right edge of the header. Set to false to prevent resizing |
isExpandTrigger |
Boolean | false |
Make this column trigger row expansion when clicked |
Row Data Properties
The library recognizes special properties in row data objects that control their behavior and appearance:
| Property | Type | Description |
|---|---|---|
_selected |
Boolean | When set to true, the row will be automatically selected when the table is initialized |
_partiallySelected |
Boolean | When set to true, the row will be marked as partially selected (shows a blue minus-square icon instead of a checkbox) |
Example
const data = [
{
id: 1,
name: 'Documents',
type: 'folder',
_selected: true // This row will be selected initially
},
{
id: 2,
name: 'Projects',
type: 'folder',
_partiallySelected: true // This row will show as partially selected
},
{
id: 3,
name: 'file.txt',
type: 'file'
// This row will be unselected
}
];
const table = new Fliplet.UI.Table({
target: '#table',
selection: { enabled: true, multiple: true },
columns: [
{ name: 'Name', field: 'name' },
{ name: 'Type', field: 'type' }
],
data: data
});
Note: These properties work alongside the initialSelection and initialPartialSelection configuration options. You can use either approach or combine both for maximum flexibility.
Selection Options
{
selection: {
enabled: true, // Enable row selection
multiple: true, // Allow multiple row selection
rowClickEnabled: true, // Enable row click selection
initialSelection: [], // Array of row IDs or row objects to select initially
initialPartialSelection: [], // Array of row IDs or row objects to mark as partially selected initially
// Optional validation before selection
onBeforeSelect: function(rowData) {
// Return true/false or a Promise that resolves to true/false
return true; // or return Promise.resolve(true);
}
}
}
Selection Configuration
| Property | Type | Default | Description |
|---|---|---|---|
enabled |
Boolean | false |
Enable row selection functionality |
multiple |
Boolean | false |
Allow multiple row selection |
rowClickEnabled |
Boolean | false |
Enable row click to toggle selection (in addition to checkbox) |
initialSelection |
Array | [] |
Array of row IDs or row objects to select initially |
initialPartialSelection |
Array | [] |
Array of row IDs or row objects to mark as partially selected initially |
onBeforeSelect |
Function | undefined |
Optional validation function called before selection. Return true/false or a Promise that resolves to true/false |
Pagination Options
{
pagination: {
pageSize: 10 // Number of rows per page
}
}
Pagination Configuration
| Property | Type | Default | Description |
|---|---|---|---|
pageSize |
Number | 10 |
Number of rows to display per page |
Note: Set pagination: false to disable pagination entirely.
Expandable Rows Options
{
expandable: {
enabled: true, // Enable expandable rows
onBeforeExpand: function(rowData) { // Optional validation before expansion
// Return true/false or a Promise that resolves to true/false
return true; // or return Promise.resolve(true);
},
onExpand: function(rowData) { // Content provider function
// Return HTML string, DOM element, or Promise that resolves to either
return '<div>Details for ' + rowData.name + '</div>';
}
}
}
Expandable Row Configuration
| Property | Type | Description |
|---|---|---|
enabled |
Boolean | Enable expandable rows functionality |
onBeforeExpand |
Function | Optional validation function called before row expansion. Return true/false or a Promise that resolves to true/false |
onExpand |
Function | Required content provider function. Return HTML string, DOM element, or Promise that resolves to either |
Expansion Triggers
There are multiple ways to trigger row expansion:
- Column-level triggers: Set
isExpandTrigger: trueon any column to make the entire column clickable for expansion - Custom element triggers: Add
data-expandattribute to any element within a cell’s custom render function to make that specific element trigger expansion
// Example: Custom expand triggers using data-expand attribute
{
name: 'Actions',
render: function(rowData) {
return '<button data-expand class="btn btn-primary">View Details</button>';
}
}
Note: Elements with the data-expand attribute will automatically trigger row expansion when clicked, regardless of which column they appear in.
Events
Fliplet.UI.Table emits various events that you can listen to:
| Event | Detail | Description |
|---|---|---|
selection:change |
{ selected: Array, deselected: Array, source: String } |
Fired when row selection changes. Source can be ‘row-click’, ‘checkbox’, or ‘api’ |
row:click |
{ data: Object } |
Fired when a row is clicked |
sort:change |
{ field: String, direction: String } |
Fired when sort column/direction changes |
column:resize |
{ column: Object, width: String } |
Fired when a column is resized by dragging |
search |
{ query: String, data: Array } |
Fired when search query changes |
page:change |
{ page: Number } |
Fired when current page changes |
expand:start |
{ row: Object, rowEl: Element } |
Fired when row expansion starts (before content is loaded) |
expand:complete |
{ row: Object, rowEl: Element, contentEl: Element } |
Fired when row expansion completes successfully |
expand:error |
{ row: Object, rowEl: Element, error: Error } |
Fired when row expansion fails |
collapse:complete |
{ row: Object, rowEl: Element } |
Fired when row is collapsed |
cell:interaction |
{ row: Object, column: Object, event: Event, target: Element, action: String } |
Fired when a custom expand trigger is clicked |
Event Handling
table.on('selection:change', function(detail) {
console.log('Selected rows:', detail.selected);
console.log('Deselected rows:', detail.deselected);
console.log('Selection source:', detail.source);
});
API Methods
Selection Methods
| Method | Parameters | Description |
|---|---|---|
getSelectedRows() |
None | Returns array of selected row data |
selectRow(rowData) |
Row data object | Selects a specific row. Can be a complete row data object or a partial object for matching. If multiple rows match, the first one is selected. |
deselectRow(rowData) |
Row data object | Deselects a specific row. Can be a complete row data object or a partial object for matching. |
selectAll() |
None | Selects all rows |
deselectAll([options]) |
options.silent: Boolean |
Deselects all rows. If silent is true, no event is fired |
selectCurrentPage() |
None | Selects all rows on the current page (when pagination is enabled) |
deselectCurrentPage() |
None | Deselects all rows on the current page (when pagination is enabled) |
setRowPartialSelection(rowData, isPartial) |
Row data object, Boolean | Sets or removes partial selection state for a specific row |
isRowPartiallySelected(rowData) |
Row data object | Returns true if the row is in partial selection state |
clearAllPartialSelection() |
None | Removes partial selection state from all rows |
Expandable Row Methods
| Method | Parameters | Description |
|---|---|---|
expandRow(rowData) |
Row data object | Expands a specific row |
collapseRow(rowData) |
Row data object | Collapses a specific row |
isRowExpanded(rowData) |
Row data object | Returns true if the row is currently expanded |
isRowExpanding(rowData) |
Row data object | Returns true if the row is currently being expanded (async operation in progress) |
Example of selecting a row with partial data
// This will find the first row with id === 1 and select it
table.selectRow({ id: 1 });
// You can also use multiple properties
table.selectRow({ name: 'Apple', type: 'Fruit' });
Data Methods
| Method | Parameters | Description |
|---|---|---|
getData() |
None | Returns current table data |
destroy() |
None | Destroys the table instance and cleanup |
Event Methods
| Method | Parameters | Description |
|---|---|---|
on(eventName, handler) |
eventName: String, handler: Function |
Adds an event listener |
off(eventName, handler) |
eventName: String, handler: Function |
Removes an event listener |
Custom Rendering
You can customize cell rendering using the render function in column definition:
{
name: 'Status',
field: 'status',
render: function(data) {
return '<span class="badge ' + data.status + '">' + data.status + '</span>';
}
}
Custom Sorting
Define custom sort logic using the sortFn in column definition:
{
name: 'Name',
field: 'name',
sortable: true,
sortFn: function(a, b, direction) {
var aLen = a.name.length;
var bLen = b.name.length;
return direction === 'asc' ? aLen - bLen : bLen - aLen;
}
}
Complete Example
const table = new Fliplet.UI.Table({
target: '#table-container',
className: 'my-table',
searchable: true,
pagination: {
pageSize: 4
},
selection: {
enabled: true,
multiple: true,
rowClickEnabled: true,
initialSelection: [1, 2],
onBeforeSelect: function(rowData) {
// Example validation
return rowData.isSelectable;
}
},
columns: [
{
name: '',
field: 'expand',
isExpandTrigger: true,
width: '30px'
},
{
name: 'Name',
field: 'name',
sortable: true,
searchable: true
},
{
name: 'Type',
field: 'type',
sortable: true,
render: function(data) {
return '<span class="type-badge">' + data.type + '</span>';
}
},
{
name: 'Price',
field: 'price',
sortable: true,
render: function(data) {
return '$' + data.price.toFixed(2);
}
}
],
expandable: {
enabled: true,
onExpand: function(rowData) {
return '<div style="padding: 10px; background: #f0f8ff;">' +
'<h4>Details for ' + rowData.name + '</h4>' +
'<p>Type: ' + rowData.type + '</p>' +
'<p>Price: $' + rowData.price + '</p>' +
'</div>';
}
},
data: [
{ id: 1, name: 'Item 1', type: 'Type A', price: 10.99, isSelectable: true },
{ id: 2, name: 'Item 2', type: 'Type B', price: 20.50, isSelectable: false }
]
});
// Event handling
table.on('selection:change', function(detail) {
console.log('Selection changed:', detail.selected);
console.log('Deselected:', detail.deselected);
console.log('Source:', detail.source);
});
table.on('sort:change', function(detail) {
console.log('Sort changed:', detail.field, detail.direction);
});
table.on('search', function(detail) {
console.log('Search query:', detail.query);
console.log('Filtered data:', detail.data);
});
table.on('expand:complete', function(detail) {
console.log('Row expanded:', detail.row.name);
console.log('Expanded content element:', detail.contentEl);
});
Expandable Rows
Fliplet.UI.Table supports expandable rows that can display additional content when expansion triggers are clicked. This feature supports both synchronous and asynchronous content loading, with built-in race condition prevention for rapid clicking scenarios.
Basic Expandable Rows
Fliplet.UI.Table supports two approaches for expandable rows:
1. Dedicated Trigger Column
const table = new Fliplet.UI.Table({
target: '#table',
columns: [
{ name: '', field: 'expand', isExpandTrigger: true, width: '40px' },
{ name: 'Name', field: 'name' },
{ name: 'Email', field: 'email' }
],
data: [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
],
expandable: {
enabled: true,
onExpand: function(rowData) {
return '<div class="user-details">' +
'<h4>Details for ' + rowData.name + '</h4>' +
'<p>Email: ' + rowData.email + '</p>' +
'<p>ID: ' + rowData.id + '</p>' +
'</div>';
}
}
});
2. Custom Triggers Within Any Cell
const table = new Fliplet.UI.Table({
target: '#table',
columns: [
{
name: 'Name',
field: 'name',
render: function(rowData) {
// Any element with data-expand attribute becomes a trigger
return rowData.name + ' <span data-expand style="cursor: pointer; color: #007bff;">▶️</span>';
}
},
{ name: 'Email', field: 'email' },
{
name: 'Actions',
render: function(rowData) {
return '<button data-expand class="details-btn">View Details</button>';
}
}
],
data: [
{ id: 1, name: 'John Doe', email: 'john@example.com' },
{ id: 2, name: 'Jane Smith', email: 'jane@example.com' }
],
expandable: {
enabled: true,
onExpand: function(rowData) {
return '<div class="user-details">' +
'<h4>Details for ' + rowData.name + '</h4>' +
'<p>Email: ' + rowData.email + '</p>' +
'<p>ID: ' + rowData.id + '</p>' +
'</div>';
}
}
});
Async Content Loading
const table = new Fliplet.UI.Table({
// ... other config
expandable: {
enabled: true,
onExpand: function(rowData) {
return new Promise((resolve, reject) => {
// Simulate API call
setTimeout(() => {
if (rowData.id === 999) {
reject(new Error('User details not found'));
} else {
const details = '<div>Async loaded details for ' + rowData.name + '</div>';
resolve(details);
}
}, 1000);
});
}
}
});
Preventing Expansion
const table = new Fliplet.UI.Table({
// ... other config
expandable: {
enabled: true,
onBeforeExpand: function(rowData) {
// Prevent expansion for inactive users
return rowData.status === 'active';
},
onExpand: function(rowData) {
return generateUserDetails(rowData);
}
}
});
Expandable Row Events
// Listen for expansion lifecycle events
table.on('expand:start', function(detail) {
console.log('Started expanding:', detail.row.name);
// You can show loading indicators here
});
table.on('expand:complete', function(detail) {
console.log('Expansion completed:', detail.row.name);
console.log('Content element:', detail.contentEl);
// Initialize any interactive elements in the expanded content
});
table.on('expand:error', function(detail) {
console.log('Expansion failed:', detail.row.name, detail.error.message);
// Handle error state
});
table.on('collapse:complete', function(detail) {
console.log('Row collapsed:', detail.row.name);
});
// Listen for custom trigger interactions
table.on('cell:interaction', function(detail) {
console.log('Custom trigger clicked:', detail.target);
console.log('Row data:', detail.row);
console.log('Column:', detail.column.name);
console.log('Action:', detail.action); // 'expand'
});
Programmatic Control
// Expand a specific row
table.expandRow(rowData);
// Collapse a specific row
table.collapseRow(rowData);
// Check if a row is expanded
if (table.isRowExpanded(rowData)) {
console.log('Row is currently expanded');
}
// Check if a row is being expanded (useful for showing loading states)
if (table.isRowExpanding(rowData)) {
console.log('Row is currently being expanded');
}
Nested Tables
Fliplet.UI.Table supports creating tables within expanded rows, perfect for hierarchical data displays with multi-level selection:
const departmentTable = new Fliplet.UI.Table({
target: '#departments',
selection: { enabled: true, multiple: true }, // Enable department selection
columns: [
{ name: 'Department', field: 'name' },
{ name: 'Manager', field: 'manager' },
{ name: 'Employee Count', field: 'count' }
],
expandable: {
enabled: true,
onExpand: function(dept) {
// Create a container for the nested table
const container = document.createElement('div');
container.innerHTML = '<div id="employees-' + dept.id + '"></div>';
// Load employee data asynchronously
loadEmployees(dept.id).then(employees => {
// Create nested table with its own selection
const employeeTable = new Fliplet.UI.Table({
target: '#employees-' + dept.id,
selection: { enabled: true, multiple: true },
columns: [
{ name: 'Name', field: 'name', sortable: true },
{ name: 'Position', field: 'position', sortable: true },
{ name: 'Email', field: 'email' }
],
data: employees
});
});
return container;
}
},
data: departments
});
// Get selections at both levels
const selectedDepartments = departmentTable.getSelectedRows();
// Track nested table instances to get their selections
Key features:
- Multi-level selection: Select items at parent and child levels independently
- Async data loading: Load nested data on-demand
- Independent state: Each table maintains its own selection, sort, and pagination state
Partial Selection UI
Fliplet.UI.Table provides a sophisticated partial selection UI for multiple row selection scenarios. Instead of using traditional HTML checkboxes, the component uses FontAwesome icons to display three distinct states in the header select-all checkbox.
Selection States
The select-all checkbox in the header shows three different visual states:
- Empty (fa-square-o): No rows are selected - displayed in gray
- Partial (fa-minus-square): Some but not all visible rows are selected - displayed in blue
- Full (fa-check-square): All visible rows are selected - displayed in blue
Visual Indicators
/* CSS classes for different states */
.fl-table-select-all-checkbox {
font-size: 16px;
color: #007bff;
transition: color 0.2s ease;
}
.fl-table-header-checkbox-partial {
color: #007bff !important; /* Blue for partial state */
}
How It Works
The partial selection UI automatically updates based on the current selection state:
- With Pagination: The header checkbox reflects only the selection state of rows visible on the current page
- With Search: The header checkbox reflects only the selection state of filtered/visible rows
- Dynamic Updates: The state changes immediately when rows are selected or deselected
Example Implementation
const table = new Fliplet.UI.Table({
target: '#table',
selection: {
enabled: true,
multiple: true
},
pagination: {
pageSize: 10
},
columns: [
{ name: 'Name', field: 'name' },
{ name: 'Department', field: 'department' }
],
data: userData
});
// Listen for selection changes to handle custom UI updates
table.on('selection:change', function(detail) {
console.log('Selection changed:', detail.selected.length, 'rows selected');
});
Behavior with Pagination and Search
- Pagination: When navigating between pages, the header checkbox shows the state for the current page only
- Search: When filtering data, the header checkbox shows the state for visible/filtered rows only
- Cross-page Selection: You can select rows across different pages, and the header checkbox will reflect the current page state
Custom Partial Selection for Individual Rows
For file manager scenarios where a file or folder might have some of its content selected (but not all), Fliplet.UI.Table supports setting individual rows to a partial selection state. This is particularly useful for hierarchical data where a folder might contain both selected and unselected items.
API Methods for Custom Partial Selection
// Set a row to partial selection state
table.setRowPartialSelection(rowData, true);
// Remove partial selection state from a row
table.setRowPartialSelection(rowData, false);
// Check if a row is in partial selection state
const isPartial = table.isRowPartiallySelected(rowData);
// Clear all partial selection states
table.clearAllPartialSelection();
Example Usage
const table = new Fliplet.UI.Table({
target: '#file-manager-table',
selection: { enabled: true, multiple: true },
columns: [
{ name: 'Name', field: 'name' },
{ name: 'Type', field: 'type' },
{ name: 'Size', field: 'size' }
],
data: fileData
});
// Mark a folder as partially selected when some of its contents are selected
table.setRowPartialSelection({ name: 'Documents', type: 'folder' }, true);
// Listen for selection changes
table.on('selection:change', function(detail) {
// Update parent folders' partial selection state based on child selection
updateFolderStates();
});
Visual Behavior
- Partial rows: Display a blue minus-square icon (fa-minus-square) instead of a checkbox
- Header checkbox: Shows partial state (blue minus-square) when any partial selections exist on the current page
- Click behavior: Follows standard UI conventions:
- Individual row: Partial → Selected → Unselected → Selected (cycles)
- Select-all: Empty → Partial (when some selected) → All Selected → Empty (cycles)
Initializing Selection States
There are three convenient ways to initialize selection states when creating a table:
1. Configuration-based Initialization
const table = new Fliplet.UI.Table({
target: '#table',
selection: {
enabled: true,
multiple: true,
// Pre-select specific rows
initialSelection: [
{ id: 1 }, // Select by partial object match
{ name: 'Documents', type: 'folder' }, // Select by multiple fields
5 // Select by ID (assumes row has id: 5)
],
// Mark specific rows as partially selected
initialPartialSelection: [
{ name: 'Projects', type: 'folder' },
{ id: 3 }
]
},
columns: [...],
data: fileData
});
2. Data-driven Initialization
const fileData = [
{
id: 1,
name: 'Documents',
type: 'folder',
_selected: true // This row will be selected
},
{
id: 2,
name: 'Projects',
type: 'folder',
_partiallySelected: true // This row will show as partially selected
},
{
id: 3,
name: 'file.txt',
type: 'file'
// This row will be unselected
}
];
const table = new Fliplet.UI.Table({
target: '#table',
selection: { enabled: true, multiple: true },
columns: [...],
data: fileData
});
3. Hybrid Approach
// Combine both approaches for maximum flexibility
const table = new Fliplet.UI.Table({
target: '#table',
selection: {
enabled: true,
multiple: true,
initialSelection: [{ id: 1 }], // Select Documents folder
initialPartialSelection: [{ id: 2 }] // Mark Projects as partial
},
columns: [...],
data: [
{ id: 1, name: 'Documents', type: 'folder' },
{ id: 2, name: 'Projects', type: 'folder' },
{ id: 3, name: 'file.txt', type: 'file', _selected: true }, // Also selected
{ id: 4, name: 'image.jpg', type: 'file', _partiallySelected: true } // Also partial
]
});
File Manager Example
Perfect for file/folder hierarchies where selection states need to be preserved:
const fileManagerData = [
{
id: 'folder-1',
name: 'Documents',
type: 'folder',
size: '1.2 GB',
_partiallySelected: true // Some files inside are selected
},
{
id: 'folder-2',
name: 'Images',
type: 'folder',
size: '850 MB',
_selected: true // All files inside are selected
},
{
id: 'file-1',
name: 'report.pdf',
type: 'file',
size: '2.3 MB'
// Not selected
}
];
const fileManager = new Fliplet.UI.Table({
target: '#file-manager',
selection: { enabled: true, multiple: true },
columns: [
{ name: 'Name', field: 'name' },
{ name: 'Type', field: 'type' },
{ name: 'Size', field: 'size' }
],
data: fileManagerData
});
// All selection states are automatically applied on initialization!
Column Resizing
Columns are resizable by default. Users can drag the right edge of any column header to adjust its width. On the first resize interaction, all column widths are frozen to fixed pixel values to prevent other columns from collapsing.
Disabling Resize for Specific Columns
{
name: 'ID',
field: 'id',
width: '60px',
resizable: false // This column cannot be resized
}
Listening for Resize Events
table.on('column:resize', function(detail) {
console.log('Resized column:', detail.column.name);
console.log('New width:', detail.width); // e.g., '250px'
});
Visual Behavior
- Hover on header row: Faint resize borders appear between all resizable columns
- Hover on a specific border: The border highlights in blue
- During drag: Only the active border stays highlighted
CSS Customization
Fliplet.UI.Table provides several CSS classes for styling:
| Class | Description |
|---|---|
.fl-table |
Main table container |
.fl-table-header |
Table header row |
.fl-table-row |
Table data row |
.fl-table-selected |
Selected row |
.fl-table-sortable |
Sortable column header |
.fl-table-sorted-asc |
Column sorted in ascending order |
.fl-table-sorted-desc |
Column sorted in descending order |
.fl-table-search |
Search input container |
.fl-table-pagination |
Pagination container |
.fl-table-cell |
Table cell |
.fl-table-checkbox |
Checkbox cell for selection |
.fl-table-select-all-checkbox |
Header select-all checkbox (FontAwesome icon) |
.fl-table-header-checkbox-partial |
Partial selection state styling for header checkbox |
.fl-table-header-checkbox-selected |
Selected state styling for header checkbox |
.fl-table-row-checkbox-partial |
Partial selection state styling for individual row checkboxes |
.fl-table-expand-trigger |
Cell that triggers row expansion |
.fl-table-row-expanded |
Container for expanded row content |
.fl-table-scroll-container |
Scrollable container for header and body (enables horizontal scroll on resize) |
.fl-table-cell-content |
Text content wrapper within sortable header cells |
.fl-table-sort-icon |
Sort direction icon container (▲▼) in sortable headers |
.fl-table-sort-arrow |
Individual sort arrow element |
.fl-table-sort-active |
Active sort direction arrow (dark) |
.fl-table-sort-inactive |
Inactive sort direction arrow (light gray) |
.fl-table-resize-handle |
Draggable resize handle on the right edge of header cells |
.fl-table-resize-active |
Applied to the resize handle currently being dragged |
.fl-table-resizing |
Applied to the table container during a resize drag |
Default Styling
The component comes with a default theme that includes:
- Clean, modern appearance
- Hover effects on rows
- Light blue background for selected rows
- Subtle borders and spacing
- Responsive layout with flexbox
- Sort direction indicators (▲▼ arrows in column headers)
- Column resize handles with hover highlighting
- Styled checkboxes for selection
- Pagination controls
You can override these styles by targeting the CSS classes in your own stylesheet.