Components
DataTable
A wrapper around DataTables 3, the dependency free table library. Search, sorting, paging, export buttons, row selection, responsive child rows and more, styled with your theme.
Source code
Click to see the source code for this component on GitHub. Feel free to copy it and adjust it for your own use.
Installation
The component is built on DataTables 3. It is a client only component, so there is no server rendering step to configure.
What is included
The component file is self contained. It imports everything it needs:
- The DataTables core and its stylesheet.
- Buttons (copy, CSV, Excel, PDF, print and column visibility), Responsive, Select, SearchBuilder, FixedColumns, FixedHeader, ColReorder, RowGroup and RowReorder, each with its stylesheet.
- JSZip for Excel export, and pdfmake for PDF export. pdfmake is large, so it is loaded the first time someone clicks a PDF button and not before.
- Styles that map the DataTables CSS variables to your shadcn tokens, so light mode, dark mode and every
theme-*class work without extra setup.
Registering more plugins
Only the extensions above are registered. If you need another one, install it and import its JavaScript and its stylesheet in your copy of Datatable.client.vue, inside the plain <script lang="ts"> block next to the other imports.
DataTables 3 does not load CSS from JavaScript, so every extension needs both lines. For example, for KeyTable:
bun add datatables.net-keytable-dt
import "datatables.net-keytable-dt";
import "datatables.net-keytable-dt/css/keyTable.dataTables.css";
Then switch it on with the options prop, for example :options="{ keys: true }". The same two lines work for Scroller (datatables.net-scroller-dt/css/scroller.dataTables.css), AutoFill and the other -dt packages.
Remove the imports of extensions you never use to keep your bundle smaller.
DataTable.key().PDF export
PDF buttons work out of the box. The component registers a small stand-in for pdfmake and downloads the real library the first time a PDF is created, so it costs nothing until it is used. The download option of the pdf button is the default. The open option opens the PDF in a new tab, which some browsers block because the library loads asynchronously.
If you do not want PDF export, remove the pdfMake registration from your copy of the component and uninstall pdfmake.
Upgrading from DataTables 1 or 2
DataTables 3 keeps the same API, but a few things moved. If you copied an older version of this component, replace it with the new file and check the following:
domis replaced bylayout.domstill works, but it is the legacy way to describe the layout. Uselayoutinstead. For exampledom: "Bfrtip"becomeslayout: { topStart: "buttons", topEnd: "search", bottomStart: "info", bottomEnd: "paging" }.- CSS is loaded per package. The JavaScript no longer imports any CSS. Import
datatables.net-dt/css/dataTables.dataTables.cssfor the core and<extension>.dataTables.cssfor each extension. The component does this for you. - Styling uses CSS variables. Colors, borders and padding come from variables such as
--dt-body_border,--dt-paging-button_background-hoverand--dtb-button_background. Override them instead of writing selectors. - Class names changed. If you wrote custom CSS, rename the selectors.
Before After .dataTables_wrapper.dt-container.dataTables_length.dt-length.dataTables_filter.dt-search.dataTables_info.dt-info.dataTables_paginate.dt-paging.paginate_button.dt-paging-button.sorting,.sorting_asc,.sorting_desc.dt-orderable-asc,.dt-orderable-desc,.dt-ordering-asc,.dt-ordering-desc.dataTables_scroll,.dataTables_scrollBody.dt-scroll,.dt-scroll-body.dataTables_empty.dt-empty - Buttons 4 bundles its parts. Remove imports of
datatables.net-buttons/js/buttons.colVis.mjs,buttons.html5.mjsandbuttons.print.mjs. Register JSZip withDataTable.Buttons.jszip(JSZip)instead ofwindow.JSZip, and pdfmake withDataTable.Buttons.pdfMake(). - A type was renamed. The
ConfigColumnstype that you import fromdatatables.netis nowColumnOptions. - jQuery is optional. You can remove the
jquerypackage unless your own code uses it. Numbers and dates are right aligned automatically (since DataTables 2).
Usage
Layout
The layout option decides where the page length menu, search box, buttons, info text and paging controls go. By default pageLength is at the top start, search at the top end, info at the bottom start and paging at the bottom end.
Each position is top or bottom, an optional row number, and an optional Start or End. For example topStart, topEnd, top1 (an extra full width row above them) and bottom2End. Start and End flip for right to left languages. Set a position to null to remove it, and use an array to put several features in one position.
| Feature | What it shows |
|---|---|
pageLength | The "entries per page" select |
search | The search input |
info | "Showing 1 to 10 of 57 entries" |
paging | The paging buttons |
buttons | The export and column visibility buttons (Buttons) |
searchBuilder | The condition builder (SearchBuilder) |
The example below puts SearchBuilder above the table, the buttons and search box in the header, and the page length, info and paging in the footer. The table border comes from a small scoped style, because the table is not part of layout.
Custom component
Render any Vue component inside a cell with a slot. Set the column's render option to "#slotName" and add a matching <template #slotName>, or name the slot column-<index> or column-<name> and skip the render option. The slot receives cellData, rowData, rowIndex, colIndex and type.
Layout with SearchBuilder
The layout option accepts objects too. Here the paging controls get a className, so you can add any classes you like to a cell. Position names are described in the layout reference.
Simple
Total
$3,598.24
Image
No Horizontal
Total
$2,684.71
Striped
Total
$3,048.09
Vertical Lines
Total
$2,172.47
Dense
Row Selection
Total
$3,565.48
Card
Total
$4,099.61
Scroll with Sticky Header
Total
$16,486.88
Badge & Icons
Total
$18,837.15
Search & Sort
Column Search
DataTables 3 can search several columns with one term. columns().search() applies the search to every selected column, and column().search() targets a single one. Here two inputs drive the table API from outside the table.
One search applied across two columns.
Fixed Columns
Column ReOrder
Set colReorder: true to let people drag column headings into a new order. The ColReorder extension is already registered by the component. Use colReorder.enable() and colReorder.disable() on the table API to turn it on and off, as the buttons below do.
Column Reorder
You can reorder the columns by dragging and dropping the column header.
Total
$4,507.24
Row Group
The RowGroup extension adds a heading row for each group. Order the table by the grouping column so each group stays together, and hide the column if the heading already shows it.
Row Reorder
The RowReorder extension lets people drag rows into a new order. Set rowReorder.dataSrc to the property that stores each row's position, and order the table by that column. The row-reorder event tells you what moved. Dragging is pointer only, so add your own move buttons if you need a keyboard alternative.
Drag a row by its number to change the order.
Row reordering uses the pointer. Pair it with your own move up and move down buttons if you need a keyboard alternative.
Child Rows
Show extra detail under a row with row.child(). Give a column the dt-control class and the stylesheet draws the expand arrow. The click handler is a delegated listener registered from the ready event.
Export Buttons
Configure each button with extend. Use exportOptions to choose the columns that are exported, filename and title to name the file, and customize to change the PDF document. Give a column the no-export class to leave it out. PDF export loads pdfmake the first time it is used.
Ajax
The ajax option can be a URL, an object or a function. Turn on processing to show the loading overlay, and call ajax.reload() on the table API to fetch the data again. This loads everything once and lets the browser search, sort and page. For large data sets, see Server-side.
Data is loaded through the `ajax` option.
Server-side
When your data lives behind an API and is too large to load at once, set serverSide: true. DataTables then stops searching, sorting and paging in the browser. Every time the user searches, sorts, changes page or changes the page length, it calls your ajax function with a description of what they asked for, and you send back only the rows to show.
The request object has the fields you need:
| Field | Meaning |
|---|---|
draw | A counter. Send it back unchanged so DataTables can ignore out of date responses |
start and length | The first row and the number of rows to return |
search.value | What the user typed in the search box |
order | The sort. Each entry has a column index and a dir of asc or desc |
columns | Your column definitions. columns[order.column].data is the field to sort by |
Respond by calling callback with data (the rows), recordsTotal (rows in the whole table) and recordsFiltered (rows that match the search). They are used for the "Showing 1 to 10 of 194 entries" text and the paging buttons.
The example below loads products from DummyJSON. Search, sorting, paging and the page length menu are all sent to the API as query parameters, and the request that was just made is shown above the table. A few details worth copying:
searchDelaywaits for the user to stop typing, so you do not send a request per keystroke.- An
AbortControllercancels a request that has been replaced by a newer one. - Errors are handled by showing a message and returning an empty page.
dataon each column is the field name that is sent assortBy. Addorderable: falseto a column the API cannot sort by.
Last request: waiting for the first request...
Using your own API
Only the part that builds the query and reads the response is specific to an API. For example, a Strapi REST endpoint takes pagination[start], pagination[limit], sort and filters, and returns the total in meta.pagination.total:
const ajax = async (data: AjaxData, callback: (json: object) => void) => {
const order = data.order?.[0];
const term = data.search?.value.trim();
const res = await $fetch<{ data: any[]; meta: { pagination: { total: number } } }>(
"/api/articles",
{
baseURL: "https://your-strapi.example.com",
query: {
"pagination[start]": data.start,
"pagination[limit]": data.length,
// `title:asc`. Strapi accepts several sort fields too
...(order && { sort: `${data.columns?.[order.column]?.data}:${order.dir}` }),
// `$containsi` is a case insensitive "contains"
...(term && { "filters[title][$containsi]": term }),
},
}
);
callback({
draw: data.draw,
data: res.data,
recordsTotal: res.meta.pagination.total,
recordsFiltered: res.meta.pagination.total,
});
};
This is an adaptation of the example above and has not been run against a Strapi instance. Strapi 4 nests fields under attributes, so point your columns at attributes.title and so on. To search more than one field, use filters[$or][0][title][$containsi] and filters[$or][1][body][$containsi].
Server-side filters
Filters that live outside the table, like a category select, work the same way. Keep the value in a ref, read it inside your ajax function, and call ajax.reload() on the table API when it changes. Reloading goes back to the first page by default.
Last request: waiting for the first request...
Reactive Data
Pass a ref to the data prop. When it changes the component redraws the table, so you never have to call the API to add or remove rows.
4 people in the table
Custom Layout
A layout position can hold a feature name, an object with the feature's options, an array of features, or a function that returns an element. A number after top or bottom adds a row.