Table Component
The Dynamic Table component provides an efficient and flexible way to display tabular data in a Vue.js application. This component offers features such as sorting, filtering, pagination, and customizable headers. It can easily be integrated into any project requiring a data table with dynamic capabilities.
Dynamic Template

Create a dynamicTable.vue file in the components folder and add the code below
<!-- components/Common/table/dynamicTable.vue -->
<template>
<div class="relative">
<div class=" flex items-center mb-6">
<h5 class=" capitalize" v-if="title">
{{ title }}
</h5>
<div
class="flex items-center ml-2 bg-neutral-20 border border-[#D0D5DD] py-[1px] px-[6px] rounded-full b-shadow"
>
<div class="w-[6px] h-[6px] rounded-full bg-[#F2AE2A] mr-[6px]"></div>
<span class="text-neutral-400 text-[12px] font-medium"
>{{ row_count }} {{ count_label }}</span>
</div>
</div>
<p v-if="description" class="sm:text-lg text-subtxt dark:text-white mb-10">
{{ description }}
</p>
<div class="grid">
<div class="col-12 overflow-auto">
<div class="card text-sm font-normal capitalize">
<DataTable
v-model:selection="selectedProduct"
:value="mockData"
rowGroupMode="rowspan"
sortMode="single"
:sortOrder="1"
scrollable
responsiveLayout="scroll"
class="mb-6"
@sort="sort"
@row-click="view"
>
<slot name="header">
<div
:class="
plainInfo
? 'flex justify-between items-center overflow-auto table-header flex-wrap gap-2'
: 'py-5 flex justify-between items-center overflow-auto table-header flex-wrap gap-2'
"
>
<div
class="flex bg-transparent justify-start items-center flex-wrap gap-2"
>
<div class="text-left" v-if="hasSearch">
<div class="sm:w-[274px] w-full max-w-full">
<IconField class="">
<InputIcon class="pi pi-search"> </InputIcon>
<InputText
v-model="search"
@input="onInput"
placeholder="Search"
class="w-full"
/>
</IconField>
</div>
</div>
<slot name="dateFilter"></slot>
</div>
<div
class="flex bg-transparent xl:justify-end items-center flex-wrap gap-2"
>
<slot name="filter"></slot>
<div class="flex justify-end items-center" v-if="hasLimit">
<label for="limit" class="text-sm text-[#7C7C7A] mr-2">{{
"show"
}}</label>
<FormSelect
placeholder="10"
name="limit"
:options="limits"
id="limit"
v-model="limit"
item-text="name"
item-value="value"
@update:model-value="onLimit"
hideError
/>
</div>
</div>
</div>
</slot>
<slot name="items">
<Column
selectionMode="multiple"
headerStyle="width: 3rem"
v-if="itemsBulkActions && itemsBulkActions.length > 0"
></Column>
<Column
v-for="(column, index) in selectedColumns"
:key="index"
:sortable="column.sortable"
:field="column.field"
:header="column.header"
:style="column.style"
:frozen="!!column.frozen"
:class="column.class"
alignFrozen="right"
class="cursor-pointer body-normal text-neutral-500"
>
<template #body="slotProps">
<slot
:name="column.field"
v-bind="slotProps"
v-if="slotProps && slotProps.data"
>
{{ slotProps.data[column.field] }}
</slot>
</template>
</Column>
</slot>
<template #footer v-if="!mockData || mockData.length < 1">
<div
class="flex flex-col items-center justify-center gap-6 py-12"
>
<svg
width="72"
height="72"
viewBox="0 0 72 72"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect width="72" height="72" rx="36" fill="#F5F5F5" />
<mask
id="mask0_788_10160"
style="mask-type: alpha"
maskUnits="userSpaceOnUse"
x="14"
y="14"
width="44"
height="44"
>
<rect
x="14.3999"
y="14.3999"
width="43.2"
height="43.2"
fill="#D9D9D9"
/>
</mask>
<g mask="url(#mask0_788_10160)">
<path
d="M25.2 53.0999C23.9424 53.0999 22.878 52.6643 22.0068 51.7931C21.1356 50.9219 20.7 49.8575 20.7 48.5999V46.6613C20.7 45.7667 21.0186 45.0008 21.6558 44.3636C22.293 43.7264 23.059 43.4078 23.9539 43.4078H26.1V22.1539C26.1 21.259 26.4186 20.4929 27.0558 19.8557C27.693 19.2185 28.459 18.8999 29.3539 18.8999H48.046C48.9409 18.8999 49.707 19.2185 50.3442 19.8557C50.9814 20.4929 51.3 21.259 51.3 22.1539V48.5999C51.3 49.8575 50.8644 50.9219 49.9932 51.7931C49.122 52.6643 48.0576 53.0999 46.8 53.0999H25.2ZM46.8 50.3999C47.31 50.3999 47.7375 50.2274 48.0825 49.8824C48.4275 49.5374 48.6 49.1099 48.6 48.5999V22.1539C48.6 21.9922 48.5481 21.8594 48.4443 21.7556C48.3405 21.6518 48.2077 21.5999 48.046 21.5999H29.3539C29.1922 21.5999 29.0595 21.6518 28.9557 21.7556C28.8519 21.8594 28.8 21.9922 28.8 22.1539V43.4078H41.746C42.6409 43.4078 43.407 43.7264 44.0442 44.3636C44.6814 45.0008 45 45.7667 45 46.6613V48.5999C45 49.1099 45.1725 49.5374 45.5175 49.8824C45.8625 50.2274 46.29 50.3999 46.8 50.3999ZM32.296 30.046C31.9135 30.046 31.593 29.9165 31.3344 29.6576C31.0755 29.3987 30.946 29.078 30.946 28.6955C30.946 28.3127 31.0755 27.9922 31.3344 27.7339C31.593 27.4753 31.9135 27.346 32.296 27.346H45.1039C45.4864 27.346 45.807 27.4754 46.0656 27.7343C46.3245 27.9932 46.4539 28.3141 46.4539 28.6969C46.4539 29.0794 46.3245 29.3999 46.0656 29.6585C45.807 29.9168 45.4864 30.046 45.1039 30.046H32.296ZM32.296 35.2385C31.9135 35.2385 31.593 35.1091 31.3344 34.8502C31.0755 34.5913 30.946 34.2704 30.946 33.8876C30.946 33.5051 31.0755 33.1846 31.3344 32.926C31.593 32.6677 31.9135 32.5385 32.296 32.5385H45.1039C45.4864 32.5385 45.807 32.668 46.0656 32.9269C46.3245 33.1855 46.4539 33.5062 46.4539 33.889C46.4539 34.2718 46.3245 34.5923 46.0656 34.8506C45.807 35.1092 45.4864 35.2385 45.1039 35.2385H32.296ZM25.2 50.3999H42.3V46.6613C42.3 46.4999 42.2481 46.3673 42.1443 46.2635C42.0405 46.1594 41.9077 46.1074 41.746 46.1074H23.9539C23.7922 46.1074 23.6595 46.1594 23.5557 46.2635C23.4519 46.3673 23.4 46.4999 23.4 46.6613V48.5999C23.4 49.1099 23.5725 49.5374 23.9175 49.8824C24.2625 50.2274 24.69 50.3999 25.2 50.3999ZM25.2 50.3999H23.4H42.3H25.2Z"
fill="#121212"
/>
</g>
</svg>
<div v-if="loading" class="flex items-center justify-center gap-2 py-20">
<p class="body-small text-neutral-text_secondary">Loading data...</p>
<i class="pi pi-spin pi-spinner text-2xl text-primary-300"></i>
</div>
<div v-else class="text-center max-w-[406px]">
<h4 class="mb-2">No Data to Show</h4>
<p class="body-normal text-neutral-text_secondary0">
Your information will be displayed here when available.
</p>
</div>
</div>
</template>
</DataTable>
<div
class="sm:flex sm:justify-center sm:items-center"
v-if="mockData && mockData.length > 0 && hasPagination"
>
<!-- <div
class="mb-6 sm:mb-0 text-[#5C5C5C] bg-primary-50 w-max px-5 py-2 rounded-full"
>
<p class="sm:text-base text-sm lowercase">
Showing {{ metadata.page }} of {{ metadata.pages }}
</p>
</div> -->
<Paginator
v-if="mockData && mockData.length > 0 && hasPagination"
v-model="metadata.page"
:rows="metadata.limit"
:totalRecords="metadata.total"
:rowsPerPageOptions="limits"
@page="onPage($event)"
@update:rows="onPageLimit($event)"
/>
</div>
</div>
</div>
</div>
<Toast />
</div>
</template>
<script setup>
import FormSelect from "~/components/common/input/FormSelect.vue";
const props = defineProps({
loading: {
type: Boolean,
default: false
},
columns: {
type: Array,
required: true,
default: () => [],
},
mockData: {
type: Array,
required: true,
default: () => [],
},
otherFilter: {
type: Object,
default: () => {},
},
url: {
type: String,
default: "",
},
subPath: {
type: String,
default: "",
},
title: {
type: String,
default: "",
},
count_label: {
type: String,
default: "",
},
row_count: {
type: Number,
default: 0,
},
description: {
type: String,
default: "",
},
dateFilter: {
type: Boolean,
default: true,
},
hasSearch: {
type: Boolean,
default: true,
},
hasLimit: {
type: Boolean,
default: false,
},
hasPagination: {
type: Boolean,
default: true,
},
plainInfo: {
type: Boolean,
default: false,
},
itemsBulkActions: {
type: Array,
default: () => [],
},
metadata: {
type: Array,
default: () => [{ page: 1, limit: 10, total: 0, pages: 1 }],
},
});
const first = ref(0);
const selectedProduct = ref(null);
const search = ref("");
// const tableData = ref([]);
const limits = [10, 20, 30];
const page = ref(1);
const limit = ref(10);
const pages = ref(1);
const total = ref(1);
const loading = ref(false);
const filter = ref("");
const order = ref(1);
const selectedColumns = ref([...props.columns]);
const emit = defineEmits([
"bulkChange",
"create",
"onPagination",
"onSearch",
"onSort",
"onLimit",
"onReload",
"onView",
]);
const toast = useToast();
onMounted(() => {
onToggle(selectedColumns.value);
});
onBeforeMount(async () => {
reset();
});
const onToggle = (val) => {
selectedColumns.value = props.columns.filter((col) => val.includes(col));
};
const sort = (e) => {
emit("onSort", "sort", e.sortOrder, e.sortField);
order.value = e.sortOrder;
};
const onPage = ($event) => {
page.value = $event.page + 1;
emit("onPagination", "page", page.value);
};
const onInput = () => {
emit("onSearch", "search", search.value);
};
const view = (e) => {
emit("onView", e.data);
};
const onLimit = (info) => {
emit("onLimit", "limit", limit.value);
};
const tableReload = (info) => {
emit("onReload", "reload");
};
const reset = () => {
first.value = (page.value - 1) * limit.value;
};
</script>
Usage Example
<!-- pages/index.vue -->
<template>
<div>
<CommonTableDynamicTable
:columns="columns"
title="All Transactions"
:mock-data="tableData"
@onPagination="updateStateAndFetch"
@onSort="updateStateAndFetch"
@onSearch="updateStateAndFetch"
@onLimit="updateStateAndFetch"
@onView="onViewDeatails"
:metadata="metadata"
:row_count="total"
>
<template #filter>
<CommonButton
title="Export"
bgColor="bg-primary-300 border border-violet-90 dark:border-violet-60 body-regular !h-[40px]"
textColor="text-neutral-500"
type="button"
createIcon="download"
/>
<CommonButton
title="Add New Broadcast"
bgColor="bg-primary-300 body-regular !h-[40px]"
textColor="text-neutral-500"
type="button"
createIcon="plus"
/>
</template>
<template #full_name="slotProps">
<div class="flex items-center gap-2">
<div>
<p class="mb-1">{{ slotProps.data.full_name }}</p>
<p class="text-neutral-200 text-xs">
{{ slotProps.data.username }}
</p>
</div>
</div>
</template>
<template #date="slotProps">
<div class="flex items-center gap-2">
<div>
<p class="mb-1 body-small text-neutral-300">
{{ moment(slotProps.data.date).format('DD, MMM YYYY') }}
</p>
<p class="text-neutral-200 body-xsmall">
{{ moment(slotProps.data.date).format('h:mm A') }}
</p>
</div>
</div>
</template>
<template #type="slotProps">
<div class="">
<CommonTag :info="slotProps.data.type" />
</div>
</template>
<template #status="slotProps">
<div class="">
<CommonStatus :status="slotProps.data.status" />
</div>
</template>
<template #action="slotProps">
<CommonActionMenu
:items="getMenuItems(slotProps.data)"
:data="slotProps.data"
@view="onViewDeatails(slotProps.data._id)"
@activate="onActivate(slotProps.data._id)"
@suspend="onsuspend(slotProps.data._id)"
/>
</template>
</CommonTableDynamicTable>
</div>
</template>
<script setup>
import moment from 'moment'
const columns = [
{
field: 'title',
header: 'Title',
sortable: false
},
{
field: 'type',
header: 'Type',
sortable: false
},
{
field: 'status',
header: 'Status',
sortable: false
},
{
field: 'date',
header: 'Created Date',
sortable: false
},
{
field: 'action',
header: '',
sortable: false
}
]
const tableData = [
{
title: 'New Feature Update',
type: 'Announcement',
status: 'Sent',
date: '08, Mar 2025',
time: '12:05 PM'
},
{
title: 'System Maintenance',
type: 'Alert',
status: 'Sent',
date: '08, Mar 2025',
time: '12:05 PM'
},
{
title: 'Survey Invitation',
type: 'Engagement',
status: 'Ended',
date: '08, Mar 2025',
time: '12:05 PM'
},
{
title: 'Security Update',
type: 'Alert',
status: 'In Progress',
date: '08, Mar 2025',
time: '12:05 PM'
},
{
title: 'Community Guidelines',
type: 'Announcement',
status: 'Sent',
date: '08, Mar 2025',
time: '12:05 PM'
}
]
const metadata = ref([{ page: 1, limit: 10, total: 0, pages: 1 }])
function getMenuItems(rowData) {
return [
{ label: 'View Broadcast', icon: 'broadcasts', emit: 'edit' },
{ label: 'End', icon: 'cancel-red', emit: 'suspend' },
{ label: 'Delete', icon: 'trash', emit: 'activate' }
]
}
const updateStateAndFetch = (actionType, value1, value2 = null) => {
if (actionType === 'page') {
page.value = value1
} else if (actionType === 'search') {
search.value = value1
} else if (actionType === 'sort') {
order.value = value1
// filter.value = value2;
} else if (actionType === 'limit') {
limit.value = value1
// filter.value = value2;
} else {
console.log('Unknown action type:', actionType)
return // Exit if the action type is not recognized
}
FetchData()
}
</script>
A customizable tag component based on PrimeVue’s Tag that visually represents information like statuses, labels, or categories with optional styling for background and borders.
<!-- /components/common/Tag.vue -->
<template>
<div>
<Tag :value="info" :style="getTagStyle(info)" rounded />
</div>
</template>
<script setup>
import Tag from "primevue/tag";
const props = defineProps({
info: {
type: String,
},
back_g: {
type: Boolean,
default: true,
},
border: {
type: Boolean,
default: false,
},
});
// Example function to determine tag style based on feature
const getTagStyle = (feature) => {
switch (feature) {
case "Estimate Request":
return {
backgroundColor: props.back_g ? "#F7EEFC" : "transparent",
color: "#AF52DE",
borderColor: "#F7EEFC",
};
default:
return {
backgroundColor: props.back_g ? "#FCFCFD" : "transparent",
color: "#3A3A3A",
borderColor: "#D9D9DB",
};
}
};
</script>
A visual status indicator that displays a rounded badge with an icon and text. It adapts its color and icon based on the given status, using Tailwind CSS utility classes and custom icons.
<!-- /components/common/Status.vue -->
<template>
<div class=" body-small">
<button
class=" text-success-300 dark:text-success-75 bg-success-50 dark:bg-success-500 py-[1px] rounded-[100px] pl-[4px] pr-2 flex items-center gap-[4px] capitalize"
v-if="
status === 'Completed' || status === 'Active' || status === 'Paid' || status === 'Sent' || status === 'Resolved'
"
>
<div>
<BaseCustomIcon name="check_circle" />
</div>
{{ status }}
</button>
<button
class=" text-warning-300 dark:text-warning-75 bg-warning-50 dark:bg-warning-500 py-[1px] rounded-[100px] pl-[4px] pr-2 flex items-center gap-[4px] capitalize"
v-else-if="
status === 'Unverified' || status === 'Archived' || status === 'In Progress' || status === 'Pending'
"
>
<div>
<BaseCustomIcon name="not_disturb" />
</div>
{{ status }}
</button>
<button
class=" text-danger-300 dark:text-danger-75 dark:bg-danger-500 bg-[#FDEEEE] py-[1px] rounded-[100px] pl-[4px] pr-2 flex items-center gap-[4px] capitalize"
v-else-if="status === 'Cancelled' || status === 'Suspended' || status === 'Rejected' || status === 'Failed' || status === 'Ended' || status === 'Deleted'"
>
<div>
<BaseCustomIcon name="suspend" />
</div>
{{ status }}
</button>
</div>
</template>
<script setup>
const props = defineProps({
status: {
type: String,
},
});
</script>
A customizable dropdown menu built with PrimeVue’s Menu component. This menu allows you to attach actions to items, emit events dynamically, and trigger context-based operations using icons and labels.
<!-- /components/common/ActionMenu.vue -->
<template>
<div class="card flex justify-end" v-if="items && items.length > 0">
<button @click="toggle" class="dark:text-[white]">
<BaseCustomIcon name="3dot" />
</button>
<Menu ref="menu" :model="items" :popup="true" class="rounded-[8px] !min-w-[143px]">
<template #item="{ item }">
<button
class="flex items-center justify-between gap-2 py-2 px-3 w-full"
@click="$emit(item.emit, data)"
>
<span class="font-normal text-neutral-500 body-small text-nowrap">{{ item.label }}</span>
<div class="text-neutral-l-600">
<BaseCustomIcon :name="item.icon" />
</div>
</button>
</template>
</Menu>
</div>
</template>
<script setup>
import { ref, defineEmits } from 'vue'
const menu = ref()
const props = defineProps({
items: {
type: Array,
required: true,
default: () => []
},
data: {
type: Object,
default: () => null
}
})
const toggle = (event) => {
menu.value.toggle(event)
}
</script>
A versatile and fully customizable button component that supports loading state, icon placement, dynamic colors, and click events. Perfect for use across your application wherever buttons are needed with consistent styling and behavior.
<!-- /components/common/button.vue -->
<template>
<div :class="buttonContainerClass">
<button
:class="[
'w-full h-[48px] px-5 rounded-[8px] flex justify-center items-center',
borderClass,
bgClass,
textColorClass,
borderColor !== '' ? `border ${borderColor}` : '',
{ 'opacity-50 cursor-not-allowed': disabled || loading }
]"
@click="handleClick"
:disabled="disabled || loading"
:type="type"
>
<!-- Show spinner when loading -->
<span v-if="loading">
<i class="pi pi-spin pi-spinner mr-2"></i>
</span>
<!-- Show title and icon when not loading -->
<span class="flex items-center justify-center">
<div v-if="iconPosition === 'before' && createIcon" :class="title ? 'mr-2' : ''">
<BaseCustomIcon :name="createIcon" />
</div>
{{ title }}
<div v-if="iconPosition === 'after' && createIcon" class="ml-2">
<BaseCustomIcon :name="createIcon" />
</div>
</span>
</button>
</div>
</template>
<script setup>
import { computed } from "vue";
// Define props
const props = defineProps({
title: {
type: String,
default: "",
},
createIcon: {
type: String,
default: "",
},
iconPosition: {
type: String,
default: "before", // can be 'before' or 'after'
},
bgColor: {
type: String,
default: "bg-blue-500",
},
textColor: {
type: String,
default: "text-white",
},
borderColor: {
type: String,
default: "",
},
buttonContainerClass: {
type: String,
default: "flex items-center gap-3",
},
loading: {
type: Boolean,
default: false,
},
type: {
type: String,
default: "",
},
disabled: {
type: Boolean,
default: false,
}
});
// Define emit
const emit = defineEmits(["click"]);
// Compute classes
const bgClass = computed(() => props.bgColor);
const textColorClass = computed(() => props.textColor);
const borderClass = computed(() => props.borderColor);
// Handle click event
const handleClick = () => {
if (props.disabled || props.loading) return; // Prevent clicking when disabled or loading
emit("click");
};
</script>
<style scoped>
/* Style for the spinner is already defined in PrimeIcons */
</style>
/* global.css */
.own-filter .p-select {
height: 48px;
flex-direction: row-reverse;
box-shadow: 0px 1px 2px 0px rgba(16, 24, 40, 0.05) !important;
border-radius: 8px;
padding: 0 3px;
}
.own-filter .p-select .p-inputtext {
line-height: 1rem;
border: none !important;
background: none !important;
font-size: 14px;
}
.own-filter .p-select .p-dropdown-trigger {
width: 16px;
}
.own-filter .p-select .p-placeholder {
@apply font-normal text-neutral-200 text-sm;
}
.own-filter .p-select-label {
@apply !pr-[7px];
}
.p-popover-content {
@apply !p-0;
}
.p-textarea {
@apply border-none bg-neutral-30 text-neutral-500 !important;
}
.p-tag {
@apply !border !rounded-[100px] !text-xs !font-normal;
}
@media screen and (min-width: 640px) {
.p-paginator-current {
flex: auto;
margin: auto;
display: inline-flex;
justify-content: flex-start;
}
}
.th-center {
.p-datatable-column-header-content {
justify-content: center;
}
}
.dark {
.p-datatable-thead tr th {
background: #323942 !important;
color: white;
}
.p-datatable-tbody tr {
background: transparent;
border-bottom: 1px solid #bfbfbf33 !important;
border-top: 1px solid #bfbfbf33 !important;
&:hover {
background: #bfbfbf33 !important;
}
}
}
.p-select-label {
margin-top: auto;
margin-bottom: auto;
}
.p-placeholder {
color: #97989A !important;
font-size: 12px;
font-weight: 400;
}
.p-select {
border-radius: 8px !important;
border-color: #D9D9DB !important;
background: transparent !important;
box-shadow: none !important;
font-size: 12px !important;
.p-select-label {
padding: 0 !important;
/* color: #121212 !important; */
font-weight: 400;
}
&::placeholder {
color: #97989A !important;
}
}
.dark {
.p-select {
border-color: #494850 !important;
background: #05040F !important;
color: #ffffff !important;
}
}
.p-select-list {
font-size: 12px;
}
.p-datepicker .p-inputtext::placeholder {
color: #97989a !important;
font-size: 14px;
}
.p-datatable-thead tr th {
background: #fafafa !important;
font-size: 14px;
font-weight: 500;
padding: 13px 24px !important;
border-bottom: 0.6px solid #bfbfbf66;
text-wrap: nowrap;
color: #080a09;
}
.p-datatable-thead tr {
border-bottom: 1px solid #eaecf0;
}
.p-datatable-tbody tr {
border-bottom: 1px solid #eaecf0;
border-top: 1px solid #eaecf0;
td {
padding: 13px 24px !important;
}
&:hover {
background: #f8f8fc;
}
}
.p-datatable .p-datatable-footer {
border: none !important;
background-color: white;
min-height: 50px;
}
.p-datatable .p-datatable-footer {
@apply dark:bg-neutral-10;
}
.p-paginator-bottom {
border: none;
margin-top: 20px;
}
.p-paginator {
background: transparent;
border: none;
}
Note: The dynamic table component accepts various props such as columns, title, pagination, search, limit, and more, which can be used across all pages. Be sure to review your project's specific requirements to determine how these props should be configured and utilized.