QTable is a component that allows you to display data in a tabular manner. It’s generally called a datatable. It packs the following main features:
- Filtering
- Sorting
- Single / Multiple rows selection with custom selection actions
- Pagination (including server-side if required)
- Grid mode (you can use for example QCards to display data in a non-tabular manner)
- Total customization of rows and cells through scoped slots
- Ability to add additional row(s) at top or bottom of data rows
- Column picker (through QTableColumns component described in one of the sections)
- Custom top and/or bottom Table controls
- Responsive design
If you don’t need pagination, sorting, filtering, and all other features of QTable, then you may want to check out QMarkupTable component instead.
Defining the columns
Let’s take an example of configuring the columns property. We are going to tell QTable that row-key is ‘name’, which must be unique. If this was data fetched from a database we would likely use the row id.
columns: [
// array of Objects
// column Object definition
{
// unique id
// identifies column
// (used by pagination.sortBy, "body-cell-[name]" slot, ...)
name: 'desc',
// label for header
label: 'Dessert (100g serving)',
// row Object property to determine value for this column
field: 'name',
// OR field: row => row.some.nested.prop,
// (optional) if we use visible-columns, this col will always be visible
required: true,
// (optional) alignment
align: 'left',
// (optional) tell QTable you want this column sortable
sortable: true,
// (optional) compare function if you have
// some custom data or want a specific way to compare two rows
// --> note that rows with null/undefined as value will get auto sorted
// without calling this method (if you want to handle those as well, use "rawSort" instead)
sort: (a, b, rowA, rowB) => parseInt(a, 10) - parseInt(b, 10),
// function return value:
// * is less than 0 then sort a to an index lower than b, i.e. a comes first
// * is 0 then leave a and b unchanged with respect to each other, but sorted with respect to all different elements
// * is greater than 0 then sort b to an index lower than a, i.e. b comes first
// (optional) requires Quasar v2.13+
// compare function if you have
// some custom data or want a specific way to compare two rows
// --> note that there is an alternative "sort" method (above) if you don't
// want to handle (by yourself) rows with null/undefined as value
rawSort: (a, b, rowA, rowB) => parseInt(a, 10) - parseInt(b, 10),
// has the same return value as the alternative "sort" method above
// (optional) override 'column-sort-order' prop;
// sets column sort order: 'ad' (ascending-descending) or 'da' (descending-ascending)
sortOrder: 'ad', // or 'da'
// (optional) you can format the data with a function
format: (val, row) => `${val}%`,
// one more format example:
// format: val => val
// ? /* Unicode checkmark checked */ "\u2611"
// : /* Unicode checkmark unchecked */ "\u2610",
// body td:
style: 'width: 500px',
// or as Function --> style: row => ... (return String/Array/Object)
classes: 'my-special-class',
// or as Function --> classes: row => ... (return String)
// header th:
headerStyle: 'width: 500px',
headerClasses: 'my-special-class'
},
{ name: 'calories', label: 'Calories', field: 'calories', sortable: true },
{ name: 'fat', label: 'Fat (g)', field: 'fat', sortable: true },
{ name: 'carbs', label: 'Carbs (g)', field: 'carbs' },
{ name: 'protein', label: 'Protein (g)', field: 'protein' },
{ name: 'sodium', label: 'Sodium (mg)', field: 'sodium' },
{
name: 'calcium',
label: 'Calcium (%)',
field: 'calcium',
sortable: true,
sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
},
{
name: 'iron',
label: 'Iron (%)',
field: 'iron',
sortable: true,
sort: (a, b) => parseInt(a, 10) - parseInt(b, 10)
}
]Basic usage
You can use the dense prop along with $q.screen to create a responsive behavior. Example: :dense="$q.screen.lt.md". More info: Screen Plugin.
Omitting columns definition
You can omit specifying the columns. QTable will infer the columns from the properties of the first row of the data. Note that labels are uppercased and sorting is enabled:
Sticky header/column
Sticky headers and columns are achieved through CSS with position: sticky. This is NOT supported on all browsers. Check caniuse.com before using this technique.
Pay attention to the code in the “style” section in the following examples, especially around position: sticky.
The browser paints each sticky cell independently and table cells can have fractional sizes, so on some display scalings or browser zoom levels (Windows 125% scaling, for example) hairline gaps can appear between sticky cells, revealing the content scrolled behind them. The 1px box-shadow in the same color as the cell background that you will see in the examples below covers such gaps.
The footer slot (rendered as a real <tfoot> element) can be made sticky the same way. A good use case for it is a totals row:
Separators
Styling
For all the styling component properties, please check the API card at the top of the page.
Virtual scrolling
Notice that when enabling virtual scroll you will need to specify the table-style (with a max-height) prop. In the example below, we are also forcing QTable to display all rows at once (note the use of pagination and rows-per-page-options props).
You can dynamically load new rows when scroll reaches the end:
You can have both virtual scroll and pagination:
The example below shows how virtual scroll can be used along with a sticky header. Notice the virtual-scroll-sticky-start prop which is set to the header height.
Sticky columns work with virtual scroll too, with one precaution: scope any positional td selector (like td:first-child) to the data rows, as in the example below. Virtual scroll emulates the full height of the list through two hidden filler rows (q-virtual-scroll__padding), and their cells match such selectors as well; making them sticky breaks scrolling (dragging the scrollbar would run away to the end of the table).
There are 2 utility CSS classes that control VirtualScroll size calculation:
- Use
q-virtual-scroll--with-prevclass on an element rendered by the VirtualScroll to indicate that the element should be grouped with the previous one (main use case is for multiple table rows generated from the same row of data). - Use
q-virtual-scroll--skipclass on an element rendered by the VirtualScroll to indicate that the element’s size should be ignored in size calculations.
When rendering more than one QTr for the same row of data (through the body slot), give each QTr a distinct key and add the q-virtual-scroll--with-prev class to every extra QTr after the first one. This tells VirtualScroll to group their sizes together with the previous element, so the total height of the data row is measured correctly. If a row should not be measured at all (for example, a separator), use q-virtual-scroll--skip instead. This is especially important when also using a sticky header or row expansion, otherwise you may notice an incorrect scroll height or a jumping scroll position.
Selection
The property row-key must be set in order for selection to work properly.
Visible columns, custom top, fullscreen
Please note that columns marked as required (in the column definition) cannot be toggled and are always visible.
Popup editing
Below is an example with the user being able to edit “in place” with the help of QPopupEdit component. Please note that we are using the body scoped slot. QPopupEdit won’t work with cell scoped slots.
QPopupEdit edits one cell at a time. To edit a whole row at once, or to offer per row actions (like Edit and Remove), render your own buttons in a body-cell scoped slot and open a QDialog holding a form for that row.
Editing with an input
Grid style
You can use the grid prop along with $q.screen to create a responsive behavior. Example: :grid="$q.screen.lt.md". More info: Screen Plugin.
In the example below, we let QTable deal with displaying the grid mode (not using the specific slot):
However, if you want to fully customize the content, check the example below, where:
- We are using a Vue scoped slot called
itemto define how each record (the equivalent of a row in non-grid mode) should look. This allows you total freedom. - We are using multiple selection.
Expanding rows
Add unique (distinct) key on QTr if you generate more than one QTr from a row in data.
An external expansion model can also be used:
If you are using virtual scroll with QTable, you should know that there are 2 utility CSS classes that control VirtualScroll size calculation:
- Use
q-virtual-scroll--with-prevclass on an element rendered by the VirtualScroll to indicate that the element should be grouped with the previous one (main use case is for multiple table rows generated from the same row of data). - Use
q-virtual-scroll--skipclass on an element rendered by the VirtualScroll to indicate that the element’s size should be ignored in size calculations.
An expanded row is just another QTr rendered for the same row of data, so the same rules from “Virtual scrolling” apply: give it its own unique key and the q-virtual-scroll--with-prev class, so its height is added to the main row when VirtualScroll calculates sizes — even while it’s hidden with v-show.
Before/after slots
Note the difference between bottom-row and footer: the former renders extra rows inside the table body, while the latter renders a real <tfoot> element (which, for example, can be made sticky through CSS; see the “Sticky footer” example above).
Pagination
When pagination has a property named rowsNumber, then this means that you’ll be configuring Table for server-side pagination (& sorting & filtering). See “Server side pagination, filter and sorting” section.
Below are two examples of handling the pagination (and sorting and rows per page).
The first example highlights how to configure the initial pagination:
The second example uses the “v-model:pagination” directive because we want to access its current value at any time. A use-case for the technique below can be to control the pagination from outside of QTable.
Pagination slot
For learning purposes, we will customize the pagination controls with the default controls in order to help you get started with your own.
Loading state
Custom top
Body slots
The example below shows how you can use a slot to customize the entire row:
When used in the body slot, each QTd needs to know which column it belongs to. Set its col-name prop (v2.27+) to the column’s name from the columns definition, as in the example above. The Vue key attribute is also supported for backwards compatibility, but since key is a reserved attribute Vue does not pass it along through $attrs, so it cannot be used when wrapping QTd inside a custom component. The same applies to QTh in the header slot.
Below, we use a slot which gets applied to each body cell:
You can also customize a particular column. The syntax for this slot is body-cell-[name], where [name] is the column’s name from the columns definition.
Header slots
The example below shows how you can use a slot to customize the entire header row:
Below, we use a slot which gets applied to each header cell:
You can also customize a particular header cell. The syntax for this slot is header-cell-[name], where [name] is the column’s name from the columns definition.
No data
There is also a “no-data” scoped slot (see below) that you can also to customize the messages for both when a filter doesn’t returns any results or the table has no data to display. Also type something into the “Search” input.
Handling bottom layer
There are a few properties that you can use to hide the bottom layer or specific parts of it. You can play with it below:
Custom sorting
Responsive tables
In order to create responsive tables, we have two tools at our disposal: dense and grid properties. We can connect these with $q.screen. More info: Screen Plugin.
First example below uses $q.screen.lt.md (for enabling dense mode) and the second examples uses $q.screen.xs to enable grid mode, so play with browser width to see them in action.
Server side pagination, filter and sorting
When your database contains a big number of rows for a Table, obviously it’s not feasible to load them all for multiple reasons (memory, UI rendering performance, …). Instead, you can load only a Table page. Whenever the user wants to navigate to another Table page, or wants to sort by a column or wants to filter the Table, a request is sent to the server to fetch the partially paged data.
First step to enable this behavior is to specify
paginationprop, which MUST containrowsNumber. QTable needs to know the total number of rows available in order to correctly render the pagination links. Should filtering cause therowsNumberto change then it must be modified dynamically.Second step is to listen for
@requestevent on QTable. This event is triggered when data needs to be fetched from the server because either page number or sorting or filtering changed.It’s best that you also specify the
loadingprop in order to notify the user that a background process is in progress.
In the example below, steps have been taken to emulate an ajax call to a server. While the concepts are similar, if you use this code you will need to make the appropriate changes to connect to your own data source.
Exporting data
Below is an example of a naive csv encoding and then exporting table data by using the exportFile Quasar util. The browser should trigger a file download. For a more professional approach in regards to encoding we do recommend using csv-parse and csv-stringify packages.
You could also make use of the filteredSortedRows internal computed property of QTable should you want to export the user filtered + sorted data.
Accessibility v2.25+
QTable renders a native <table> with thead/tbody, so the tabular semantics come for free. Sortable column headers are focusable, sort on Enter or Space and expose aria-sort; this behavior lives in QTh, so custom header/header-cell slots should render QTh rather than a plain th to keep it. The horizontally scrolling body is a Tab stop, so a wide table can be scrolled with the arrow keys. The selection checkboxes (and their otherwise empty column header), the pagination controls, the rows-per-page select and the loading progress bar carry localized accessible names from the Quasar Language Pack.
A few aspects remain in your hands:
- Clickable rows (
@row-click& co.) are pointer-only: rows receive no focus and no key handling. Offer row actions as real buttons inside a cell, or add keyboard handling yourself — the Keyboard navigation example above shows the technique. gridmode trades the table semantics for plain cards.- The
titleprop renders a visual heading, not a<caption>— associate a name with the table througharia-label/aria-labelledbyif it needs one.
Keyboard navigation
Below is an example of keyboard navigation in the table using selected row. Use Arrow Up, Arrow Down, Page Up, Page Down, Home and End keys to navigate.