Overview
The Akitable component, is designed to manage and display complex datasets in
React applications efficiently. It integrates with Ant Design for robust UI
elements and extends functionality with custom theming, pagination, and row
selection capabilities.
Key Features
- Dynamic Data Rendering: Easily render data arrays with customizable columns.
- Integrated Pagination: Managed through the
paginationprop for smooth navigation. - Flexible Row Selection: Pass
rowSelectiondirectly to enable checkbox/radio selection independently ofactions. - Customizable Styling: Adapt the table to different UI themes, with options for background removal and scroll behavior.
- Extensible Design: Works seamlessly with
header,footer, andactionsprops.
Example Usage
import type {
AkitableAction,
AkitableColumn,
AkitablePaginatedData,
AkitablePaginationProps,
SorterChangeEvent
} from '@akinon/akitable';
import { Akitable } from '@akinon/akitable';
const results = [
{
key: '1',
name: 'Mike',
age: 32
},
{
key: '2',
name: 'John',
age: 42
}
];
const MyComponent = () => {
const data: AkitablePaginatedData = {
count: results.length,
results
};
const columns: AkitableColumn[] = [
{
title: 'Name',
dataIndex: 'name',
key: 'name'
},
{
title: 'Age',
dataIndex: 'age',
key: 'age'
}
];
const actions: AkitableAction[] = [
{
label: 'Delete',
onSelect: (selectedRowKeys: React.Key[]) => {
console.log('selectedRowKeys: ', selectedRowKeys);
}
}
// Additional actions...
];
const pagination: AkitablePaginationProps = {
page: 1,
size: 20
};
const handleSorterChange: SorterChangeEvent = (sorter) => {
console.log('Sorter changed: ', sorter);
};
return (
<Akitable
isLoading={false}
data={data}
columns={columns}
actions={actions}
pagination={pagination}
rowKey="key"
onChangeSorter={handleSorterChange}
// Additional Akitable props...
/>
);
};
export default MyComponent;Akitable Props
Akitable props provide various options for configuring the table component:
| Property | Description | Type |
|---|---|---|
isLoading | Indicates if the data is currently loading. | boolean |
data | Array of data objects to display. | AkitableData[] | AkitablePaginatedData |
columns | Configuration for the table columns. | AkitableColumn[] |
actions | Array of actions that can be performed on selected rows. | AkitableAction[] |
header | Manages the display of the table’s header. | AkitableHeaderProps | undefined |
footer | Manages the display of the table’s footer. | AkitableFooterProps | undefined |
emptyState | Custom empty state configuration for the table. | AkitableEmptyStateProps | undefined |
rowKey | Row’s unique key. | string |
pagination | Pagination settings to control the page navigation. Optionally takes pageSizeOptions to replace the “items per page” list. | AkitablePaginationProps | undefined |
onRowClick | Configuration for row click features. | RowClickEvent | undefined |
onRowEdit | Configuration for row edit features. | RowEditCallback | undefined |
actionColumn | Action column appended as the last column, replacing the default edit column. | AkitableActionColumn | undefined |
onChangeSorter | Called when table sorting is changed. | SorterChangeEvent | undefined |
rowSelection | Row selection configuration. Enables checkbox/radio selection regardless of whether actions are defined. When actions are also present, the selection is managed internally unless selectedRowKeys is passed: then it is controlled, onChange is called on every change and the actions apply the controlled keys. Without selectedRowKeys, onChange is not called while actions are present. A controlled selection must be updated from onChange, with or without actions: passing selectedRowKeys without onChange makes the selection read-only, so clicking a checkbox does not change it. All other rowSelection fields are merged. | AkitableRowSelection | undefined |
onPaginationChanged | Called when the page number or page size is changed. | PaginationChangeEvent | undefined |
paginationShowTotal | Custom render function for the pagination total label (e.g. “1-20 of 100 items”). | (total: number, range: [number, number]) => ReactNode | undefined |
toolbar | Layout of the bars above and below the table (actions select, Apply, selected count, pagination, header buttons). See Fitting a Narrow Container. | AkitableToolbarProps | undefined |
className | Class name added to the table’s root element. | string | undefined |
style | Inline style for the table’s root element. | React.CSSProperties | undefined |
Types
interface AkitableData {
[key: string]: any;
}
interface AkitablePaginatedData {
count: number;
next?: string | null;
previous?: string | null;
results: AkitableData[];
}import type { TableColumnType } from 'antd';
interface AkitableColumn extends TableColumnType<AkitableData> {
copyable?: boolean;
editable?: boolean;
}You can learn about TableColumnType type
here .
interface AkitableAction {
label: string;
onSelect: (selectedRowKeys: React.Key[]) => void;
}interface AkitableActionColumnRenderParams {
record: AkitableData;
isEditing: boolean;
isEditLoading: boolean;
hasEditFormError: boolean;
handleStartEdit: (e?: React.MouseEvent) => void;
handleSaveEditingItem: () => Promise<void>;
handleCancelEditingItem: () => void;
}
interface AkitableActionColumn {
title?: ReactNode;
width?: number | string;
fixed?: 'left' | 'right' | boolean;
className?: string;
align?: 'left' | 'right' | 'center';
render?: (params: AkitableActionColumnRenderParams) => ReactNode;
items?: AkitableActionItem[];
onNavigate?: (
href: string,
record: AkitableData,
event: React.MouseEvent
) => void;
}type AkitableActionItemValue<T> = T | ((record: AkitableData) => T);
// Every other `@akinon/ui-button` prop (`icon`, `iconSize`, `type`, `danger`,
// `tooltip`, `loading`, `ghost`, `block`, `className`, `data-*`, …) is accepted
// and forwarded to the rendered button. `size` is fixed to `small` by the
// action column, so it is not accepted here.
interface AkitableActionItem
extends Omit<
IButtonProps,
'children' | 'disabled' | 'hidden' | 'href' | 'onClick' | 'size'
> {
key?: string;
label?: ReactNode;
href?: AkitableActionItemValue<string>;
onClick?: (record: AkitableData, event: React.MouseEvent) => void;
startEdit?: boolean;
disabled?: AkitableActionItemValue<boolean>;
hidden?: AkitableActionItemValue<boolean>;
}type AkitableHeaderLayout = 'auto' | 'stacked';
interface AkitableHeaderProps {
title?: string | ReactElement;
extra?: ReactNode;
downloadActions?: ReactNode;
downloadActionsSlotWidth?: number;
reserveDownloadActionsSpace?: boolean;
trailingExtra?: ReactNode;
// Layout of the header side. See "Header Layout" below.
layout?: AkitableHeaderLayout;
}interface AkitableFooterProps {
extra?: ReactNode;
}interface AkitableEmptyStateProps {
icon?: ReactNode;
description?: ReactNode;
}// The default sizes stay as literals for editor suggestions, but any number is
// accepted so a custom `pageSizeOptions` list is not limited to them.
type AkitablePageSizes = 20 | 50 | 100 | 250 | (number & NonNullable<unknown>);
interface AkitablePaginationProps {
page: number;
size: AkitablePageSizes;
pageSizeOptions?: readonly AkitablePageSizes[];
}Customizing the page size options
The “items per page” dropdown offers [20, 50, 100, 250] by default. Pass
pageSizeOptions to replace that list — useful when the backend does not
support every size. Omitting it (or passing an empty array) keeps the defaults.
Repeated sizes are dropped from the list, but the given order is preserved — the dropdown presents the sizes exactly as they are passed.
size accepts any number, but it must be a positive integer: a 0, negative or
fractional size would leave the pager with a nonsensical page count, so the
pagination renders the first offered option instead — and reports that size back
through onPaginationChanged on the next interaction.
import {
AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS,
type AkitablePaginationProps
} from '@akinon/akitable';
// Only the sizes the backend supports.
const pagination: AkitablePaginationProps = {
page: 1,
size: 20,
pageSizeOptions: [20, 50, 100]
};
// Or derive the list from the defaults.
const derived: AkitablePaginationProps = {
page: 1,
size: 20,
pageSizeOptions: AKITABLE_DEFAULT_PAGE_SIZE_OPTIONS.filter(
size => size !== 250
)
};type RowClickEvent = (
record: AkitableData,
event?: React.MouseEvent<HTMLElement>,
rowIndex?: number
) => void;type RowEditCallback = (
modifiedRecord: AkitableData,
payload: AkitableData
) => void | Promise<void>;type PaginationChangeEvent = (page: number, size: number) => void;type SortOrder = 'descend' | 'ascend' | null;
interface SorterResult {
column?: AkitableColumn;
order?: SortOrder;
field?: string | readonly string[];
columnKey?: string;
}
type TableOnChangeSorter = SorterResult | SorterResult[];
type SorterChangeEvent = (sorter: TableOnChangeSorter) => void;enum AkitableRowStatus {
PENDING = 'pending',
ERROR = 'error'
}type AkitableRowSelectionType = 'checkbox' | 'radio';
type AkitableRowSelectMethod = 'all' | 'none' | 'invert' | 'single' | 'multiple';
interface AkitableRowSelection {
align?: 'left' | 'center' | 'right';
checkStrictly?: boolean;
columnTitle?: ReactNode | ((originalNode: ReactNode) => ReactNode);
columnWidth?: string | number;
fixed?: boolean | 'left' | 'right';
getCheckboxProps?: (record: AkitableData) => Record<string, unknown>;
getTitleCheckboxProps?: () => Record<string, unknown> & React.AriaAttributes;
hideSelectAll?: boolean;
preserveSelectedRowKeys?: boolean;
renderCell?: (checked: boolean, record: AkitableData, index: number, originNode: ReactNode) => ReactNode;
selectedRowKeys?: React.Key[];
selections?: object[] | boolean;
type?: AkitableRowSelectionType;
onChange?: (selectedRowKeys: React.Key[], selectedRows: AkitableData[], info: { type: AkitableRowSelectMethod }) => void;
onSelect?: (record: AkitableData, selected: boolean, selectedRows: AkitableData[], nativeEvent: Event) => void;
}Header Layout (header.layout)
The right-hand side of the header holds, in order, header.extra,
header.downloadActions, the pagination and header.trailingExtra. By default
(layout: 'auto') they share a single row, and move to two rows only while bulk
actions are given: extra and downloadActions above, the pagination and
trailingExtra below.
Set layout: 'stacked' to get the two-row layout on a table without actions
as well, for example to show XLS/CSV export buttons above the pagination bar on
a read-only list.
| Value | Layout of the header side |
|---|---|
'auto' | A single row; two rows while actions are given. The default. |
'stacked' | Always two rows: extra and downloadActions above the pagination and trailingExtra. |
import { Akitable } from '@akinon/akitable';
import { Button } from '@akinon/ui-button';
<Akitable
columns={columns}
data={data}
rowKey="pk"
pagination={pagination}
onPaginationChanged={handlePaginationChange}
header={{
title: 'Attribute Values',
layout: 'stacked',
downloadActions: (
<>
<Button size="small" type="primary" icon="download">
XLS
</Button>
<Button size="small" type="primary" icon="download">
CSV
</Button>
</>
)
}}
/>;In the single-row layout, reserveDownloadActionsSpace (default true) keeps a
downloadActionsSlotWidth-wide slot next to the pagination while no
downloadActions are given, so the pagination does not shift between tables
with and without download buttons. The stacked layout gives the downloads their
own row and renders no such placeholder, so both settings only apply to the
single-row layout.
Fitting a Narrow Container (toolbar)
In a narrow container the bars above and below the table (the actions select
and its Apply button, the “N record(s) selected” text, the pagination with its
“1-20 of 50 items” label, and the header buttons) take a lot of room and wrap
awkwardly. toolbar makes them smaller, tightens the space between them and
hides the texts you do not need. Leaving it out keeps the default layout.
import { Akitable } from '@akinon/akitable';
<Akitable
{...tableProps}
toolbar={{ size: 'small', showSelectedCount: false, showTotal: false }}
/>;type AkitableCssLength = number | string; // a pixel number or any CSS length
interface AkitableToolbarProps {
size?: 'default' | 'small';
showSelectedCount?: boolean;
showTotal?: boolean;
gap?: AkitableCssLength;
controlHeight?: AkitableCssLength;
fontSize?: AkitableCssLength;
controlPaddingInline?: AkitableCssLength;
downloadIconSize?: AkitableCssLength;
downloadSize?: AkitableCssLength;
downloadFontSize?: AkitableCssLength;
}| Property | Description | Default |
|---|---|---|
size | 'small' is a preset for controlHeight: 32, fontSize: '0.75rem' and gap: '0.5rem'. Any of those set explicitly wins. | 'default' |
showSelectedCount | Shows the “N record(s) selected” text next to Apply. | true |
showTotal | Shows the pagination total label in the header and the footer. When false, a custom paginationShowTotal is hidden as well (it is not called). | true |
gap | Space between toolbar items: the header groups, the actions select and Apply, the header buttons and the parts of the pagination. | - |
controlHeight | Height of the actions select, Apply, the pagination buttons, page input and page-size select, and the buttons passed through header.downloadActions, header.extra, header.trailingExtra and footer.extra. | - |
fontSize | Font size of those controls, the selected-rows count and the pagination total label. | - |
controlPaddingInline | Horizontal padding inside the actions select and Apply. Both shrink or grow together, so a narrow select keeps a matching Apply. | 0.75rem with controlHeight, the stylesheet’s padding otherwise |
downloadIconSize | Size of the icons inside the header.downloadActions buttons. Set it when those buttons get smaller, so the icon and its label still fit. | the button’s own icon size |
downloadSize | Width and height of the square header.downloadActions buttons. | controlHeight + 0.5rem with controlHeight, the stylesheet’s size otherwise |
downloadFontSize | Font size of the labels inside the header.downloadActions buttons. Wins over fontSize for those buttons. | fontSize, or the stylesheet’s size |
Each setting can be used on its own, for example toolbar={{ showTotal: false }}
only hides the total label and changes nothing else. The width of the actions
select is still set with actionsSelectProps.
For finer control, set the CSS variables below through className or style
on the table. They take effect while the matching toolbar setting is active.
| Variable | Set by | Description |
|---|---|---|
--akitable-toolbar-gap | gap | Space between toolbar items. |
--akitable-toolbar-control-height | controlHeight | Height of the toolbar controls. |
--akitable-toolbar-control-padding-inline | controlPaddingInline | Horizontal padding of the actions select and Apply. Defaults to 0.75rem while controlHeight is set. |
--akitable-toolbar-font-size | fontSize | Font size of the toolbar text. |
--akitable-toolbar-download-size | downloadSize | Size of the square downloadActions buttons. Defaults to controlHeight + 0.5rem, so their icon and label keep some padding. |
--akitable-toolbar-download-icon-size | downloadIconSize | Size of the icons inside the downloadActions buttons. |
--akitable-toolbar-download-font-size | downloadFontSize | Font size of the labels inside the downloadActions buttons. |
<Akitable
{...tableProps}
toolbar={{ size: 'small' }}
style={
{ '--akitable-toolbar-control-padding-inline': '0.5rem' } as React.CSSProperties
}
/>Navigating Actions (Open in a New Tab)
Actions that navigate somewhere should be declared through actionColumn.items rather than a
custom render. Akitable renders every item that has an href as a real <a href> element,
so the browser’s own gestures work on it:
| Gesture | Handled by | Result |
|---|---|---|
| Left click | Akitable → onNavigate | Client-side navigation, no page reload |
| Middle click | The browser | Opens in a new tab |
Cmd/Ctrl/Shift + click | The browser | Opens in a new tab or window |
| Right click → open in a new tab | The browser | Opens in a new tab |
A plain left click is intercepted (preventDefault) and delegated to the item’s own onClick
if it has one, otherwise to the column’s onNavigate — which keeps SPA routing intact. When
neither is defined the click is left alone and the browser follows the href as a regular
link. No target is set on the anchor, because that would make a plain left click open a new
tab as well.
Non-navigating actions (delete, toggles, …) use onClick instead of href and render as
plain buttons, so they are unaffected.
An item that is disabled or loading renders as a plain button as well, even when it has an
href: antd ignores a Button’s click handler in both states, so an anchor left in place would
be followed on a middle click while a plain left click did nothing. Clicking an action never
triggers the table’s onRowClick, in any of these states.
<Akitable
data={data}
columns={columns}
rowKey="id"
actionColumn={{
title: 'Actions',
fixed: 'right',
onNavigate: (href) => navigate(href),
items: [
{
key: 'edit',
icon: 'edit',
tooltip: 'Edit',
type: 'link',
href: (record) => `/products/${record.id}/edit`
},
{
key: 'delete',
icon: 'bin',
tooltip: 'Delete',
type: 'text',
danger: true,
onClick: (record) => remove(record.id)
}
]
}}
/>href, disabled and hidden accept either a static value or a function of the row, so
each row can resolve its own target and state.
Since the href becomes a real anchor attribute, it must be a URL the browser can resolve on
its own — for example /products/1, or #/products/1 when a hash router is used.
Because an href is typically derived from record data, it is validated rather than trusted: a
real anchor is followed natively on middle click and Cmd/Ctrl + click, where no click
handler runs to intercept it. Only relative URLs (/products/1, products/1, #/products/1,
?page=2) and the http, https, mailto and tel schemes are accepted. Everything else is
rejected — javascript: above all, but also a protocol-relative URL (//evil.com, plus the
\\evil.com / /\evil.com shapes browsers parse the same way), an empty or whitespace-only
value, and an internal routing key shaped like catalog:detail, which parses as the scheme
catalog:.
A rejected value is dropped silently: nothing is logged and the item renders as a plain
button. Check the values your href builder can produce rather than waiting for a console
message — record => record.detailUrl ?? '' is an easy slip that turns a link into a button.
Precedence and Backwards Compatibility
render takes precedence over items when both are provided, and a warning is logged. This
keeps every existing actionColumn.render usage working unchanged.
Note that anything returned from render is opaque to Akitable, so a <button onClick>
rendered there will not gain new-tab support. Either migrate the navigating actions to
items, or render your own real <a href> elements inside render.
To put the row into edit mode from an item, set startEdit: true instead of an onClick.
This requires the table’s onRowEdit to be defined, since that is what saves the row. While a
row is being edited, Akitable renders its built-in save and cancel actions in place of the
items.