Vue3 DataTable

Vue3 DataTable

·

26 min read

Vue DataTable displays data in tabular format. It supports Vue 3 with PrimeVue 3 and Vue 2 with PrimeVue 2.

Setup

Refer to PrimeVue setup documentation for download and installation steps for your environment such as Vue CLI, Vite or browser.

Import

import DataTable from 'primevue/datatable';
import Column from 'primevue/column';
import ColumnGroup from 'primevue/columngroup';     //optional for column grouping

Getting Started

DataTable requires a value as an array of objects and columns defined with Column component. Throughout the samples, a car interface having vin, brand, year and color properties is used to define an object to be displayed by the datatable. Cars are loaded by a CarService that connects to a server to fetch the cars with a axios. Note that this is only for demo purposes, DataTable does not have any restrictions on how the data is provided.

import axios from 'axios'

export default class CarService {

    getCarsSmall() {
        return axios.get('demo/data/cars-small.json').then(res => res.data.data);
    }

    getCarsMedium() {
        return axios.get('demo/data/cars-medium.json').then(res => res.data.data);
    }

    getCarsLarge() {
        return axios.get('demo/data/cars-large.json').then(res => res.data.data);
    }
}

Example response;

{
    "data": [
        {"brand": "Volkswagen", "year": 2012, "color": "Orange", "vin": "dsad231ff"},
        {"brand": "Audi", "year": 2011, "color": "Black", "vin": "gwregre345"},
        {"brand": "Renault", "year": 2005, "color": "Gray", "vin": "h354htr"},
        {"brand": "BMW", "year": 2003, "color": "Blue", "vin": "j6w54qgh"},
        {"brand": "Mercedes", "year": 1995, "color": "Orange", "vin": "hrtwy34"},
        {"brand": "Volvo", "year": 2005, "color": "Black", "vin": "jejtyj"},
        {"brand": "Honda", "year": 2012, "color": "Yellow", "vin": "g43gr"},
        {"brand": "Jaguar", "year": 2013, "color": "Orange", "vin": "greg34"},
        {"brand": "Ford", "year": 2000, "color": "Black", "vin": "h54hw5"},
        {"brand": "Fiat", "year": 2013, "color": "Red", "vin": "245t2s"}
    ]
}

Following sample datatable has 4 columns and retrieves the data from a service on mount.

<DataTable :value="cars">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    }
}

Dynamic Columns

Column components can be dynamically generated using a v-for as well.

<DataTable :value="cars">
    <Column v-for="col of columns" :field="col.field" :header="col.header" :key="col.field"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            columns: null,
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();

        this.columns = [
            {field: 'vin', header: 'Vin'},
            {field: 'year', header: 'Year'},
            {field: 'brand', header: 'Brand'},
            {field: 'color', header: 'Color'}
        ];
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    }
}

Auto Layout

Default table-layout is fixed meaning the cell widths do not depend on their content. If you require cells to scale based on their contents set autoLayout property to true. Note that Scrollable and/or Resizable tables do not support auto layout due to technical limitations.

Templating

Field data of a corresponding row is displayed as the cell content by default, this can be customized using a body template where current row data and column properties are passed via the slot props. On the other hand, header and footer sections of a column can either be defined with the properties or the templates. Similarly DataTable itself also provides header and footer properties along with the templates for the main header and footer of the table.

<DataTable :value="cars">
    <template #header>
        <div>
            <Button icon="pi pi-refresh" style="float: left"/>
            List of Cars
        </div>
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand">
        <template #body="slotProps">
            <img :src="'demo/images/car/' + slotProps.data.brand + '.png'" :alt="slotProps.data.brand"  width="48px"/>
        </template>
    </Column>
    <Column field="color" header="Color"></Column>
    <Column headerStyle="width: 8em" bodyStyle="text-align: center">
        <template #header>
            <Button type="button" icon="pi pi-cog"></Button>
        </template>
        <template #body="slotProps">
            <Button type="button" icon="pi pi-search" class="p-button-success" style="margin-right: .5em"></Button>
            <Button type="button" icon="pi pi-pencil" class="p-button-warning"></Button>
        </template>
    </Column>
    <template #footer>
        In total there are {{cars ? cars.length : 0 }} cars.
    </template>
</DataTable>

Sizes

In addition to the regular table, a smal and a large version are available with different paddings. For a table with smaller paddings use p-datatable-sm class and for a larger one use p-datatable-lg.

<DataTable :value="cars" class="p-datatable-sm">
    <template #header>
        Small Table
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

<DataTable :value="cars">
    <template #header>
        Normal Table
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

<DataTable :value="cars" class="p-datatable-lg">
    <template #header>
        Large Table
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Column Grouping

Columns can be grouped at header and footer sections by defining a ColumnGroup with nested rows and columns.

<DataTable :value="sales">
    <ColumnGroup type="header">
        <Row>
            <Column header="Brand" :rowspan="3" />
            <Column header="Sale Rate" :colspan="4" />
        </Row>
        <Row>
            <Column header="Sales" :colspan="2" />
            <Column header="Profits" :colspan="2" />
        </Row>
        <Row>
            <Column header="Last Year" />
            <Column header="This Year" />
            <Column header="Last Year" />
            <Column header="This Year" />
        </Row>
    </ColumnGroup>
    <Column field="brand" />
    <Column field="lastYearSale" />
    <Column field="thisYearSale" />
    <Column field="lastYearProfit" />
    <Column field="thisYearProfit" />
    <ColumnGroup type="footer">
        <Row>
            <Column footer="Totals:" :colspan="3" />
            <Column footer="$506,202" />
            <Column footer="$531,020" />
        </Row>
    </ColumnGroup>
</DataTable>

Pagination

Pagination is enabled by setting paginator property to true and defining the rows property defines the number of rows per page. See the Paginator for the available customization options such as paginator templates, page links, rows per page options and more which can be passed through the DataTable.

<DataTable :value="cars" :paginator="true" :rows="10">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

paginatorLeft and paginatorLeft templates are available to specify custom content at the left and right side.

<DataTable :value="cars" :paginator="true" :rows="10">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
    <template #paginatorLeft>
        <Button type="button" icon="pi pi-refresh" />
    </template>
    <template #paginatorRight>
        <Button type="button" icon="pi pi-cloud" />
    </template>
</DataTable>

Paginator can also be programmed programmatically using a binding to the first property that defines the index of the first element to display. For example setting first to zero will reset the paginator to the very first page. This property also supports v-model in case you'd like your binding to be updated whenever the user changes the page.

<DataTable :value="cars" :paginator="true" :rows="10" :first="firstRecordIndex">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Sorting

Enabling sortable property at column component would be enough to make a column sortable. The property to use when sorting is the field by default and can be customized using the sortField.

<DataTable :value="cars">
    <Column field="vin" header="Vin" :sortable="true"></Column>
    <Column field="year" header="Year" :sortable="true"></Column>
    <Column field="brand" header="Brand" :sortable="true"></Column>
    <Column field="color" header="Color" :sortable="true"></Column>
</DataTable>

By default sorting is executed on the clicked column only. To enable multiple field sorting, set sortMode property to "multiple" and use metakey when clicking on another column.

<DataTable :value="cars" sortMode="multiple">
    <Column field="vin" header="Vin" :sortable="true"></Column>
    <Column field="year" header="Year" :sortable="true"></Column>
    <Column field="brand" header="Brand" :sortable="true"></Column>
    <Column field="color" header="Color" :sortable="true"></Column>
</DataTable>

In case you'd like to display the table as sorted per a single column by default on mount or programmatically apply sort, use sortField and sortOrder properties. These two properties also support the v-model directive to get updated when the user applies sort a column.

<DataTable :value="cars" sortField="year" :sortOrder="1">
    <Column field="vin" header="Vin" :sortable="true"></Column>
    <Column field="year" header="Year" :sortable="true"></Column>
    <Column field="brand" header="Brand" :sortable="true"></Column>
    <Column field="color" header="Color" :sortable="true"></Column>
</DataTable>


<DataTable :value="cars" sortField="dynamicSortField" :sortOrder="dynamicSortOrder">
    <Column field="vin" header="Vin" :sortable="true"></Column>
    <Column field="year" header="Year" :sortable="true"></Column>
    <Column field="brand" header="Brand" :sortable="true"></Column>
    <Column field="color" header="Color" :sortable="true"></Column>
</DataTable>

In multiple mode, use the multiSortMeta property and bind an array of SortMeta objects instead.

<DataTable :value="cars" sortMode="multiple" :multiSortMeta="multiSortMeta">
    <Column field="vin" header="Vin" :sortable="true"></Column>
    <Column field="year" header="Year" :sortable="true"></Column>
    <Column field="brand" header="Brand" :sortable="true"></Column>
    <Column field="color" header="Color" :sortable="true"></Column>
</DataTable>
data() {
    return {
        multiSortMeta: [
            {field: 'year', order: 1},
            {field: 'brand', order: -1}
        ]
    }
}

Filtering

DataTable has advanced filtering capabilities that does the heavy lifting while providing flexible customization. Filtering has two layout alternatives defined with the filterDisplay. In row setting, filter elements are displayed in a separate row at the header section whereas in menu mode filter elements are displayed inside an overlay. Filter metadata is specified using the filters as a v-model and UI elements for the filtering are placed inside the filter template. The template filter gets a filterModel and filterCallback, use filterModel.value to populate the filter with your own form components and call the filterCallback with the event of your choice like @input, @change, @click.

import CustomerService from '../../service/CustomerService';
import {FilterMatchMode} from 'primevue/api';

export default {
    data() {
        return {
            customers: null,
            filters: {
                'name': {value: null, matchMode: FilterMatchMode.STARTS_WITH}
            }
        }
    },
    created() {
        this.customerService = new CustomerService();
    },
    mounted() {
        this.customerService.getCustomersLarge().then(data => this.customers = data);
    }
}

Filter Row Input field is displayed in a separate header row.

<DataTable :value="customers1"
                    dataKey="id" v-model:filters="filters" filterDisplay="row" :loading="loading">
    <Column field="name" header="Name">
        <template #filter="{filterModel,filterCallback}">
            <InputText type="text" v-model="filterModel.value" @keydown.enter="filterCallback()" class="p-column-filter" :placeholder="`Search by name - ${filterModel.matchMode}`"/>
        </template>
    </Column>
<DataTable>

Filter Row Input field is displayed in an overlay.

<DataTable :value="customers1"
                    dataKey="id" v-model:filters="filters" filterDisplay="menu" :loading="loading">
    <Column field="name" header="Name">
        <template #filter="{filterModel,filterCallback}">
            <InputText type="text" v-model="filterModel.value" @keydown.enter="filterCallback()" class="p-column-filter" :placeholder="`Search by name - ${filterModel.matchMode}`"/>
        </template>
    </Column>
<DataTable>

Multiple Constraints In "menu" display, it is possible to add more constraints to a same filter. In this case, metadata could be an array of constraints. The operator defines whether all or any of the constraints should match.

data() {
    return {
        customers: null,
        filters: {
            'name': {operator: FilterOperator.AND, constraints: [{value: null, matchMode: FilterMatchMode.STARTS_WITH}]},
        }
    }
}

Populate Filters Providing a filters with predefined values would be enough to display the table as filtered by default. This approach can also be used to clear filters progammatically.

data() {
    return {
        customers: null,
        filters: {
            'name': {operator: FilterOperator.AND, constraints: [
                {value: 'Prime', matchMode: FilterMatchMode.STARTS_WITH},
                {value: 'Vue', matchMode: FilterMatchMode.CONTAINS}
            ]}
        }
    }
}

Match Modes Depending on the dataType of the column, suitable match modes are displayed. Default configuration is available at PrimeVue.filterMatchModeOptions which can be used to customize the modes globally for all tables.

import {createApp} from 'vue';
import PrimeVue from 'primevue/config';
import FilterMatchMode from 'primevue/api',
const app = createApp(App);

app.use(PrimeVue, {
    filterMatchModeOptions: {
        text: [
            FilterMatchMode.STARTS_WITH,
            FilterMatchMode.CONTAINS,
            FilterMatchMode.NOT_CONTAINS,
            FilterMatchMode.ENDS_WITH,
            FilterMatchMode.EQUALS,
            FilterMatchMode.NOT_EQUALS
        ],
        numeric: [
            FilterMatchMode.EQUALS,
            FilterMatchMode.NOT_EQUALS,
            FilterMatchMode.LESS_THAN,
            FilterMatchMode.LESS_THAN_OR_EQUAL_TO,
            FilterMatchMode.GREATER_THAN,
            FilterMatchMode.GREATER_THAN_OR_EQUAL_TO
        ],
        date: [
            FilterMatchMode.DATE_IS,
            FilterMatchMode.DATE_IS_NOT,
            FilterMatchMode.DATE_BEFORE,
            FilterMatchMode.DATE_AFTER
        ]
    }
});

If you need to override the match modes for a particular column use the filterMatchModeOptions property and provide an array with label-value pairs.

<Column field="name" header="Name" :filterMatchModeOptions="matchModes">
    <template #filter="{filterModel,filterCallback}">
        <InputText type="text" v-model="filterModel.value" @keydown.enter="filterCallback()" class="p-column-filter" :placeholder="`Search by name - ${filterModel.matchMode}`"/>
    </template>
</Column>
matchModes: [
    {label: 'Starts With', FilterMatchMode.STARTS_WITH},
    {label: 'Contains', FilterMatchMode.CONTAINS},
]

Custom Filter Custom filtering is implemented using the FilterService, first register your filter and add it to your filterMatchModeOptions.

import {FilterService} from 'primevue/api';

FilterService.register('myfilter', (a,b) => a === b);
matchModes: [
    {label: 'My Filter', "myfilter"},
    {label: 'Starts With', FilterMatchMode.STARTS_WITH},
    {label: 'Contains', FilterMatchMode.CONTAINS},
]

Filter Slots Filter menu overlay can be customized even further with various templates including filterheader, filterfooter, filterclear, filterapply. Example here changes the buttons and adds a footer.

<Column header="Country" filterField="country.name">
    <template #filter="{filterModel}">
        <InputText type="text" v-model="filterModel.value" class="p-column-filter" placeholder="Search by country"/>
    </template>
    <template #filterclear="{filterCallback}">
        <Button type="button" icon="pi pi-times" @click="filterCallback()" class="p-button-secondary"></Button>
    </template>
    <template #filterapply="{filterCallback}">
        <Button type="button" icon="pi pi-check" @click="filterCallback()" class="p-button-success"></Button>
    </template>
    <template #filterfooter>
        <div class="p-px-3 p-pt-0 p-pb-3 p-text-center p-text-bold">Customized Buttons</div>
    </template>
</Column>

Selection

DataTable provides single and multiple selection modes on click of a row. Selected rows are bound to the selection property and updated using the v-model directive. Alternatively column based selection can be done using radio buttons or checkboxes using selectionMode of a particular column. In addition row-select and row-unselect events are provided as optional callbacks.

The dataKey property identifies a unique value of a row in the dataset, it is not mandatory however being able to define it increases the performance of the table significantly.

In single mode, selection binding is an object reference.

<DataTable :value="cars" v-model:selection="selectedCar" selectionMode="single" dataKey="vin">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

In multiple mode, selection binding should be an array and multiple items can either be selected using metaKey or toggled individually depending on the value of metaKeySelection property value which is true by default. On touch enabled devices metaKeySelection is turned off automatically. Additionally ShiftKey is supported for range selection.

<DataTable :value="cars" v-model:selection="selectedCars" selectionMode="multiple" dataKey="vin">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

If you prefer a radioButton or a checkbox instead of a row click, use the selectionMode of a column instead. Following datatable displays a checkbox at the first column of each row and automatically adds a header checkbox to toggle selection of all rows.

<DataTable :value="cars" v-model:selection="selectedCars" selectionMode="multiple" dataKey="vin">
    <Column selectionMode="multiple"></Column>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Scrolling

DataTable supports both horizontal and vertical scrolling as well as frozen columns and rows. Scrollable DataTable is enabled using scrollable property and scrollHeight to define the viewport height.

<DataTable :value="cars" :scrollable="true" scrollHeight="400px">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Column Widths of a Scrollable Table

Scrollable table uses flex layout so there are a couple of rules to consider when adjusting the widths of columns.

  • Use min-width in vertical scrolling only so that when there is enough space columns may grow and for smaller screens a horizontal scrollbar is displayed to provide responsive design.
  • When horizontal scrolling is enabled, prefer width instead of min-width.
  • In vertical scrolling only, use flex to disable grow and shrink while defining a initial width. When horizontal scrolling is enabled, this is not required as columns do not grow or shrink in horizontal scrolling.
<Column field="vin" header="Vin" style="flex: 0 0 4rem"></Column>

Flex Scroll In cases where viewport should adjust itself according to the table parent's height instead of a fixed viewport height, set scrollHeight option as flex. In example below, table is inside a Dialog where viewport size dynamically responds to the dialog size changes such as maximizing.

<Button label="Show" icon="pi pi-external-link" @click="openDialog" />
<Dialog header="Flex Scroll" v-model:visible="dialogVisible" :style="{width: '50vw'}" :maximizable="true" :modal="true" :contentStyle="{height: '300px'}">
    <DataTable :value="cars" :scrollable="true" scrollHeight="flex">
        <Column field="vin" header="Vin"></Column>
        <Column field="year" header="Year"></Column>
        <Column field="brand" header="Brand"></Column>
        <Column field="color" header="Color"></Column>
    </DataTable>
    <template #footer>
        <Button label="Yes" icon="pi pi-check" @click="closeDialog" />
        <Button label="No" icon="pi pi-times" @click="closeDialog" class="p-button-secondary"/>
    </template>
</Dialog>

Full Page Scroll FlexScroll can also be used for cases where scrollable viewport should be responsive with respect to the window size. See the Full Page demo for an example.

<div style="height: calc(100vh - 143px)">
    <DataTable :value="cars" :scrollable="true" scrollHeight="flex">
        <Column field="vin" header="Vin"></Column>
        <Column field="year" header="Year"></Column>
        <Column field="brand" header="Brand"></Column>
        <Column field="color" header="Color"></Column>
    </DataTable>
</div>

Horizontal Scrolling

For horizontal scrolling, it is required to set scrollDirection to "horizontal" and give fixed widths to columns.

<DataTable :value="customers" :scrollable="true"  scrollDirection="horizontal">
    <Column field="id" header="Id" footer="Id" :style="{width:'200px'}"></Column>
    <Column field="name" header="Name" footer="Name" :style="{width:'200px'}"></Column>
    <Column field="country.name" header="Country" footer="Country" :style="{width:'200px'}"></Column>
    <Column field="date" header="Date" footer="Date" :style="{width:'200px'}"></Column>
    <Column field="balance" header="Balance" footer="Balance" :style="{width:'200px'}"></Column>
    <Column field="company" header="Company" footer="Company" :style="{width:'200px'}"></Column>
    <Column field="status" header="Status" footer="Status" :style="{width:'200px'}"></Column>
    <Column field="activity" header="Activity" footer="Activity" :style="{width:'200px'}"></Column>
    <Column field="representative.name" header="Representative" footer="Representative" :style="{width:'200px'}"></Column>
</DataTable>

Horizontal and Vertical Scrolling Set scrollDirection to "both" and give fixed widths to columns to scroll both ways.

<DataTable :value="customers" :scrollable="true" scrollHeight="400px" scrollDirection="both">
    <Column field="id" header="Id" footer="Id" :style="{width:'200px'}"></Column>
    <Column field="name" header="Name" footer="Name" :style="{width:'200px'}"></Column>
    <Column field="country.name" header="Country" footer="Country" :style="{width:'200px'}"></Column>
    <Column field="date" header="Date" footer="Date" :style="{width:'200px'}"></Column>
    <Column field="balance" header="Balance" footer="Balance" :style="{width:'200px'}"></Column>
    <Column field="company" header="Company" footer="Company" :style="{width:'200px'}"></Column>
    <Column field="status" header="Status" footer="Status" :style="{width:'200px'}"></Column>
    <Column field="activity" header="Activity" footer="Activity" :style="{width:'200px'}"></Column>
    <Column field="representative.name" header="Representative" footer="Representative" :style="{width:'200px'}"></Column>
</DataTable>

Frozen Rows Frozen rows are used to fix certain rows while scrolling, this data is defined with the frozenValue property.

<DataTable :value="customers" :frozenValue="lockedCustomers" :scrollable="true" scrollHeight="400px">
    <Column field="name" header="Name"></Column>
    <Column field="country.name" header="Country"></Column>
    <Column field="representative.name" header="Representative"></Column>
    <Column field="status" header="Status"></Column>
</DataTable>

Frozen Columns Certain columns can be frozen by using the frozen property of the column component. In addition alignFrozen is available to define whether the column should be fixed on the left or right.

<DataTable :value="customers" :scrollable="true" scrollHeight="400px" scrollDirection="both">
    <Column field="name" header="Name" :style="{width:'200px'}" frozen></Column>
    <Column field="id" header="Id" :style="{width:'100px'}" :frozen="idFrozen"></Column>
    <Column field="name" header="Name" :style="{width:'200px'}"></Column>
    <Column field="country.name" header="Country" :style="{width:'200px'}"></Column>
    <Column field="date" header="Date" :style="{width:'200px'}"></Column>
    <Column field="company" header="Company" :style="{width:'200px'}"></Column>
    <Column field="status" header="Status" :style="{width:'200px'}"></Column>
    <Column field="activity" header="Activity" :style="{width:'200px'}"></Column>
    <Column field="representative.name" header="Representative" :style="{width:'200px'}"></Column>
    <Column field="balance" header="Balance" :style="{width:'200px'}" frozen alignFrozen="right"></Column>
</DataTable>

Scrollable RowGroup Row groups with subheaders have exclusive support for filtering, when the table scrolls the subheaders stay fixed as long as their data are still displayed. No additional configuration is required to enable this feature. View the Row Group demo for an example.

Lazy Loading

Lazy mode is handy to deal with large datasets, instead of loading the entire data, small chunks of data is loaded by invoking corresponding callbacks such as paging and sorting. Sample belows imitates lazy paging by using an in memory list. It is also important to assign the logical number of rows to totalRecords by doing a projection query for paginator configuration so that paginator displays the UI assuming there are actually records of totalRecords size although in reality they aren't as in lazy mode, only the records that are displayed on the current page exist.

Lazy loading is implemented by handling pagination and sorting using page and sort events by making a remote query using the information passed to the events such as first offset, number of rows and sort field for ordering. Filtering is handled differently as filter elements are defined using templates. filter event is not triggered in lazy mode instead use the event you prefer on your form elements such as input, change, blur to make a remote call by passing the filters property to update the displayed data. Note that, in lazy filtering, totalRecords should also be updated to align the data with the paginator.

Here is a sample paging implementation with in memory data, a more enhanced example with a backend is being worked on and will be available at a GitHub repository.

<DataTable :value="cars" :lazy="true" :paginator="true" :rows="10"
    :totalRecords="totalRecords" :loading="loading" @page="onPage($event)">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            loading: false,
            totalRecords: 0,
            cars: null
        }
    },
    datasource: null,
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.loading = true;

        setTimeout(() => {
            this.carService.getCarsLarge().then(data => {
                this.datasource = data;
                this.totalRecords = data.length,
                this.cars = this.datasource.slice(0, 10);
                this.loading = false;
            });
        }, 1000);
    },
    methods: {
        onPage(event) {
            this.loading = true;

            setTimeout(() => {
                this.cars = this.datasource.slice(event.first, event.first + event.rows);
                this.loading = false;
            }, 1000);
        }
    }
}

Row Expansion

Rows can be expanded to display additional content using the expandedRows property with the v-model directive accompanied by a template named "expansion". row-expand and row-collapse are optional callbacks that are invoked when a row is expanded or toggled.

The dataKey property identifies a unique value of a row in the dataset, it is not mandatory in row expansion functionality however being able to define it increases the performance of the table significantly.

<DataTable :value="cars" v-model:expandedRows="expandedRows" dataKey="vin"
    @row-expand="onRowExpand" @row-collapse="onRowCollapse">
    <template #header>
        <div class="table-header-container">
            <Button icon="pi pi-plus" label="Expand All" @click="expandAll" />
            <Button icon="pi pi-minus" label="Collapse All" @click="collapseAll" />
        </div>
    </template>
    <Column :expander="true" headerStyle="width: 3em" />
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
    <template #expansion="slotProps">
        <div class="car-details">
            <div>
                <img :src="'demo/images/car/' + slotProps.data.brand + '.png'" :alt="slotProps.data.brand"/>
                <div class="p-grid">
                    <div class="p-col-12">Vin: <b>{{slotProps.data.vin}}</b></div>
                    <div class="p-col-12">Year: <b>{{slotProps.data.year}}</b></div>
                    <div class="p-col-12">Brand: <b>{{slotProps.data.brand}}</b></div>
                    <div class="p-col-12">Color: <b>{{slotProps.data.color}}</b></div>
                </div>
            </div>
            <Button icon="pi pi-search"></Button>
        </div>
    </template>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null,
            expandedRows: []
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    },
    methods: {
        onRowExpand(event) {
            this.$toast.add({severity: 'info', summary: 'Row Expanded', detail: 'Vin: ' + event.data.vin, life: 3000});
        },
        onRowCollapse(event) {
            this.$toast.add({severity: 'success', summary: 'Row Collapsed', detail: 'Vin: ' + event.data.vin, life: 3000});
        },
        expandAll() {
            this.expandedRows = this.cars.filter(car => car.vin);
            this.$toast.add({severity: 'success', summary: 'All Rows Expanded', life: 3000});
        },
        collapseAll() {
            this.expandedRows = null;
            this.$toast.add({severity: 'success', summary: 'All Rows Collapsed', life: 3000});
        }
    }
}

InCell Editing

In cell editing provides a rapid and user friendly way to manipulate the data. The datatable provides a flexible API so that the editing behavior is implemented by the page author whether it utilizes v-model or vuex.

Individuals cell editing is configured by setting the editMode to "cell" and defining editors with the "editor" template. The content of the editor defines how the editing is implemented, below example demonstrates two cases. In the first example, simple v-model editors are utilized. This is pretty straightforward in most cases. On the other hand, second example is more advanced to consider validations and ability to revert values with the escape key.

<h3>Basic Cell Editing</h3>
<p>Simple editors with v-model.</p>
<DataTable :value="cars1" editMode="cell">
    <Column field="vin" header="Vin">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" />
        </template>
    </Column>
    <Column field="year" header="Year">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" />
        </template>
    </Column>
    <Column field="brand" header="Brand">
        <template #editor="slotProps">
            <Dropdown v-model="slotProps.data['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand">
                <template #option="optionProps">
                    <div class="p-dropdown-car-option">
                        <img :alt="optionProps.option.brand" :src="'demo/images/car/' + optionProps.option.brand + '.png'" />
                        <span>{{optionProps.option.brand}}</span>
                    </div>
                </template>
            </Dropdown>
        </template>
    </Column>
    <Column field="color" header="Color">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" />
        </template>
    </Column>
</DataTable>

<h3>Advanced Cell Editing</h3>
<p>Custom implementation with validations, dynamic columns and reverting values with the escape key.</p>
<DataTable :value="cars2" editMode="cell" @cell-edit-complete="onCellEditComplete">
    <Column v-for="col of columns" :field="col.field" :header="col.header" :key="col.field">
        <template #editor="slotProps">
            <InputText :value="slotProps.data[slotProps.column.field]" @input="onCellEdit($event, slotProps)" />
        </template>
    </Column>
</DataTable>
import CarService from '../../service/CarService';
import Vue from 'vue';

export default {
    data() {
        return {
            cars1: null,
            cars2: null,
            cars3: null,
            editingCellRows: {},
            columns: null,
            brands: [
                {brand: 'Audi', value: 'Audi'},
                {brand: 'BMW', value: 'BMW'},
                {brand: 'Fiat', value: 'Fiat'},
                {brand: 'Honda', value: 'Honda'},
                {brand: 'Jaguar', value: 'Jaguar'},
                {brand: 'Mercedes', value: 'Mercedes'},
                {brand: 'Renault', value: 'Renault'},
                {brand: 'Volkswagen', value: 'Volkswagen'},
                {brand: 'Volvo', value: 'Volvo'}
            ]
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();

        this.columns = [
            {field: 'vin', header: 'Vin'},
            {field: 'year', header: 'Year'},
            {field: 'brand', header: 'Brand'},
            {field: 'color', header: 'Color'}
        ];
    },
    methods: {
        onCellEditComplete(event) {
            if (!this.editingCellRows[event.index]) {
                return;
            }

            const editingCellValue = this.editingCellRows[event.index][event.field];

            switch (event.field) {
                case 'year':
                    if (this.isPositiveInteger(editingCellValue))
                        Vue.set(this.cars2, event.index, this.editingCellRows[event.index]);
                    else
                        event.preventDefault();
                break;

                default:
                    if (editingCellValue.trim().length > 0)
                        Vue.set(this.cars2, event.index, this.editingCellRows[event.index]);
                    else
                        event.preventDefault();
                break;
            }
        },
        onCellEdit(newValue, props) {
            if (!this.editingCellRows[props.index]) {
                this.editingCellRows[props.index] = {...props.data};
            }

            this.editingCellRows[props.index][props.column.field] = newValue;
        },
        isPositiveInteger(val) {
            let str = String(val);
            str = str.trim();
            if (!str) {
                return false;
            }
            str = str.replace(/^0+/, "") || "0";
            var n = Math.floor(Number(str));
            return n !== Infinity && String(n) === str && n >= 0;
        }
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars1 = data);
        this.carService.getCarsSmall().then(data => this.cars2 = data);
    }
}

Row Editing is defined by setting cellEdit as "row", defining editingRows with the v-model directive to hold the reference to the editing rows and adding a row editor column to provide the editing controls. Note that since editingRows is two-way binding enabled, you may use it to initially display one or more rows in editing more or programmatically toggle row editing.

<h3>Row Editing</h3>
<DataTable :value="cars" editMode="row" dataKey="vin" v-model:editingRows="editingRows"
    @row-edit-init="onRowEditInit" @row-edit-cancel="onRowEditCancel">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" autofocus/>
        </template>
    </Column>
    <Column field="brand" header="Brand">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" />
        </template>
    </Column>
    <Column field="color" header="Color">
        <template #editor="slotProps">
            <InputText v-model="slotProps.data[slotProps.column.field]" />
        </template>
    </Column>
    <Column :rowEditor="true" headerStyle="width:7rem" bodyStyle="text-align:center"></Column>
</DataTable>
import CarService from '../../service/CarService';
import Vue from 'vue';

export default {
    data() {
        return {
            cars: null,
            editingRows: []
        }
    },
    carService: null,
    originalRows: null,
    created() {
        this.carService = new CarService();

        this.originalRows = {};
    },
    methods: {
        onRowEditInit(event) {
            this.originalRows[event.index] = {...this.cars3[event.index]};
        },
        onRowEditCancel(event) {
            Vue.set(this.cars3, event.index, this.originalRows[event.index]);
        }
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    }
}

Column Resize

Columns can be resized using drag drop by setting the resizableColumns to true. There are two resize modes; "fit" and "expand". Fit is the default one and the overall table width does not change when a column is resized. In "expand" mode, table width also changes along with the column width. column-resize-end is a callback that passes the resized column header and delta change as a parameter.

<DataTable :value="cars" :resizableColumns="true" columnResizeMode="fit | expand">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

It is important to note that when you need to change column widths, since table width is 100%, giving fixed pixel widths does not work well as browsers scale them, instead give percentage widths.

<DataTable :value="cars" :resizableColumns="true" columnResizeMode="fit | expand">
    <Column field="vin" header="Vin" headerStyle="width: 20%"></Column>
    <Column field="year" header="Year" headerStyle="width: 40%"></Column>
    <Column field="brand" header="Brand" headerStyle="width: 20%"></Column>
    <Column field="color" header="Color" headerStyle="width: 20%"></Column>
</DataTable>

Column Reorder

Columns can be reordered using drag drop by setting the reorderableColumns to true. column-reorder is a callback that is invoked when a column is reordered. DataTable keeps the column order state internally using keys that identifies a column using the field property. If the column has no field, use columnKey instead as it is mandatory for columns to have unique keys when reordering is enabled.

<DataTable :value="cars" :reorderableColumns="true">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Row Reorder

Data can be reordered using drag drop by adding a reorder column that will display an icon as a drag handle along with the row-order event which is mandatory to update the new order. Note that the reorder icon can be customized using rowReorderIcon of the column component.

<DataTable :value="cars" @row-reorder="onRowReorder">
    <Column :rowReorder="true" headerStyle="width: 3em" />
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    },
    methods: {
        onRowReorder(event) {
            //update new order
            this.cars = event.value;
        }
    }
}

Row Group

Row Grouping comes in two modes, in "subheader" mode rows are grouped by a header row along with an optional group footer. In addition, the groups can be made toggleable by enabling expandableRowGroups as true. On the other hand, the "rowspan" mode uses rowspans instead of a header to group rows. groupRowsBy property defines the field to use in row grouping. Multiple row grouping is available in "rowspan" mode by specifying the groupRowsBy as an array of fields.

Example below demonstrates the all grouping alternatives. Note that data needs to be sorted for grouping which can also be done by the table itself by speficying the sort properties.

<h3>Subheader Grouping</h3>
<DataTable :value="cars" rowGroupMode="subheader" groupRowsBy="brand"
    sortMode="single" sortField="brand" :sortOrder="1">
    <Column field="brand" header="Brand"></Column>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="color" header="Color"></Column>
    <Column field="price" header="Price"></Column>
    <template #groupheader="slotProps">
        <span>{{slotProps.data.brand}}</span>
    </template>
    <template #groupfooter="slotProps">
        <td colspan="3" style="text-align: right">Total Price</td>
        <td>{{calculateGroupTotal(slotProps.data.brand)}}</td>
    </template>
</DataTable>

<h3>Expandable Row Groups</h3>
<DataTable :value="cars" rowGroupMode="subheader" groupRowsBy="brand"
    sortMode="single" sortField="brand" :sortOrder="1"
    :expandableRowGroups="true" v-model:expandedRowGroups="expandedRowGroups"
    @rowgroup-expand="onRowExpand" @rowgroup-collapse="onRowCollapse">
    <Column field="brand" header="Brand"></Column>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="color" header="Color"></Column>
    <Column field="price" header="Price"></Column>
    <template #groupheader="slotProps">
        <span>{{slotProps.data.brand}}</span>
    </template>
    <template #groupfooter="slotProps">
        <td colspan="3" style="text-align: right">Total Price</td>
        <td>{{calculateGroupTotal(slotProps.data.brand)}}</td>
    </template>
</DataTable>

<h3>RowSpan Grouping</h3>
<DataTable :value="cars" rowGroupMode="rowspan" groupRowsBy="brand"
    sortMode="single" sortField="brand" :sortOrder="1">
    <Column header="#" headerStyle="width:3em">
        <template #body="slotProps">
            {{slotProps.index}}
        </template>
    </Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="vin" header="Vin"></Column>
    <Column field="color" header="Color"></Column>
    <Column field="price" header="Price"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null,
            expandedRowGroups: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsMedium().then(data => this.cars = data);
    },
    methods: {
        onRowGroupExpand(event) {
            this.$toast.add({severity: 'info', summary: 'Row Group Expanded', detail: 'Value: ' + event.data, life: 3000});
        },
        onRowGroupCollapse(event) {
            this.$toast.add({severity: 'success', summary: 'Row Group Collapsed', detail: 'Value: ' + event.data, life: 3000});
        },
        calculateGroupTotal(brand) {
            let total = 0;

            if (this.cars) {
                for (let car of this.cars) {
                    if (car.brand === brand) {
                        total += car.price;
                    }
                }
            }

            return total;
        }
    }
}

Data Export

DataTable can export its data in CSV format using exportCSV() method.

<DataTable :value="cars" ref="dt">
    <template #header>
        <div style="text-align: left">
            <Button icon="pi pi-external-link" label="Export" @click="exportCSV($event)" />
        </div>
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    },
    methods: {
        exportCSV() {
            this.$refs.dt.exportCSV();
        }
    }
}

TableState

Stateful table allows keeping the state such as page, sort and filtering either at local storage or session storage so that when the page is visited again, table would render the data using its last settings. Enabling state is easy as defining a unique stateKey, the storage to keep the state is defined with the stateStorage property that accepts session for sessionStorage and local for localStorage. Currently following features are supported by TableState; paging, sorting, filtering, column resizing, column reordering, row expansion, row group expansion and row selection.

<DataTable :value="cars" :paginator="true" :rows="10" v-model:filters="filters"
    stateStorage="session" stateKey="dt-state-demo-session"
    v-model:selection="selectedCar" selectionMode="single" dataKey="vin">
    <template #header>
        <div style="text-align: right">
            <i class="pi pi-search" style="margin: 4px 4px 0px 0px;"></i>
            <InputText v-model="filters['global']" placeholder="Global Search" size="50" />
        </div>
    </template>
    <Column field="vin" header="Vin" filterMatchMode="startsWith" :sortable="true">
        <template #filter>
            <InputText type="text" v-model="filters['vin']" class="p-column-filter" placeholder="Starts with" />
        </template>
    </Column>
    <Column field="year" header="Year" filterMatchMode="contains" :sortable="true">
        <template #filter>
            <InputText type="text" v-model="filters['year']" class="p-column-filter" placeholder="Contains" />
        </template>
    </Column>
    <Column field="brand" header="Brand" filterMatchMode="equals" :sortable="true">
        <template #filter>
            <Dropdown v-model="filters['brand']" :options="brands" optionLabel="brand" optionValue="value" placeholder="Select a Brand" class="p-column-filter" :showClear="true">
                <template #option="slotProps">
                    <div class="p-dropdown-car-option">
                        <img :alt="slotProps.option.brand" :src="'demo/images/car/' + slotProps.option.brand + '.png'" />
                        <span>{{slotProps.option.brand}}</span>
                    </div>
                </template>
            </Dropdown>
        </template>
    </Column>
    <Column field="color" header="Color" filterMatchMode="in" :sortable="true">
        <template #filter>
            <MultiSelect v-model="filters['color']" :options="colors" optionLabel="name" optionValue="value" placeholder="Select a Color" />
        </template>
    </Column>
    <template #empty>
        No records found.
    </template>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            filters: {},
            brands: [
                {brand: 'Audi', value: 'Audi'},
                {brand: 'BMW', value: 'BMW'},
                {brand: 'Fiat', value: 'Fiat'},
                {brand: 'Honda', value: 'Honda'},
                {brand: 'Jaguar', value: 'Jaguar'},
                {brand: 'Mercedes', value: 'Mercedes'},
                {brand: 'Renault', value: 'Renault'},
                {brand: 'Volkswagen', value: 'Volkswagen'},
                {brand: 'Volvo', value: 'Volvo'}
            ],
            colors: [
                {name: 'White', value: 'White'},
                {name: 'Green', value: 'Green'},
                {name: 'Silver', value: 'Silver'},
                {name: 'Black', value: 'Black'},
                {name: 'Red', value: 'Red'},
                {name: 'Maroon', value: 'Maroon'},
                {name: 'Brown', value: 'Brown'},
                {name: 'Orange', value: 'Orange'},
                {name: 'Blue', value: 'Blue'}
            ],
            selectedCar: null,
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsMedium().then(data => this.cars = data);
    }
}

ContextMenu

DataTable provides exclusive integration with the ContextMenu component using, contextMenu, contextMenuSelection property along with the row-contextmenu event.

<DataTable :value="cars" contextMenu v-model:contextMenuSelection="selectedCar" @row-contextmenu="onRowContextMenu">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

<ContextMenu :model="menuModel" ref="cm" />
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            cars: null,
            selectedCar: null,
            menuModel: [
                {label: 'View', icon: 'pi pi-fw pi-search', command: () => this.viewCar(this.selectedCar)},
                {label: 'Delete', icon: 'pi pi-fw pi-times', command: () => this.deleteCar(this.selectedCar)}
            ]
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    },
    methods: {
        onRowContextMenu(event) {
            this.$refs.cm.show(event.originalEvent);
        },
        viewCar(car) {
            this.$toast.add({severity: 'info', summary: 'Car Selected', detail: car.vin + ' - ' + car.brand});
        },
        deleteCar(car) {
            this.cars = this.cars.filter((c) => c.vin !== car.vin);
            this.$toast.add({severity: 'info', summary: 'Car Deleted', detail: car.vin + ' - ' + car.brand});
            this.selectedCar = null;
        }
    }
}

Empty Message

When there is no data, you may use the empty template to display a message.

<DataTable :value="cars">
    <template #empty>
        No records found
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>

Loading

A loading status indicator can be displayed when the loading property is enabled. The icon is customized through loadingIcon property. Additionally an option loading template is available to render as the body until the data is loaded.

<DataTable :value="cars" :loading="loading">
     <template #loading>
        Loading records, please wait...
    </template>
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year"></Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            loading: false,
            cars: null
        }
    },
    datasource: null,
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.loading = true;

        this.carService.getCarsLarge().then(data => {
            this.cars = data
            this.loading = false;
        });
    }
}

Responsive

DataTable responsive layout can be achieved in two ways; first approach is displaying a horizontal scrollbar for smaller screens and second one is defining a breakpoint to display the cells of a row as stacked. Scrollable tables use the scroll layout approach internally and do not require additional configuration.

Scroll Layout Set responsiveLayout to scroll to enabled this layout. Note that, when scroll mode is enabled table-layout automatically switches to auto from fixed as a result table widths are likely to differ and resizable columns are not supported. Read more about table-layout for more details.

<DataTable :value="products" responsiveLayout="scroll">

</DataTable>

Stack Layout In stack layout, columns are displayed as stacked after a certain breakpoint. Default is '960px'.

<DataTable :value="products" responsiveLayout="stack" breakpoint="640px">

</DataTable>

Row and Cell Styling

Certain rows or cells can easily be styled based on conditions. Cell styling is implemented with templating whereas row styling utilizes the rowClass property which takes the row data as a parameter and returns the style class as a string.

<DataTable :value="cars" :rowClass="rowClass">
    <Column field="vin" header="Vin"></Column>
    <Column field="year" header="Year" bodyStyle="padding: 0">
            <template #body="slotProps">
            <div :class="['year-cell', {'old-car': slotProps.data.year < 2010}]">
                {{slotProps.data.year}}
            </div>
        </template>
    </Column>
    <Column field="brand" header="Brand"></Column>
    <Column field="color" header="Color"></Column>
</DataTable>
import CarService from '../../service/CarService';

export default {
    data() {
        return {
            columns: null,
            cars: null
        }
    },
    carService: null,
    created() {
        this.carService = new CarService();
    },
    mounted() {
        this.carService.getCarsSmall().then(data => this.cars = data);
    },
    methods: {
        rowClass(data) {
            return data.color === 'Orange' ? 'orange-car': null;
        }
    }
}
.year-cell {
    padding: 0.429em 0.857rem;

    &.old-car {
        background-color: #41b783;
        font-weight: 700;
        color: #ffffff;
    }
}

.orange-car {
    background-color: #344b5f !important;
    color: #ffffff !important;
}

Theming

DataTable supports various themes featuring Material, Bootstrap, Fluent as well as your own custom themes via the Designer tool.

Resources

Visit the PrimeVue DataTable showcase for demos and documentation.