Table

Displays data in a structured table format with styling variants and semantic HTML structure.

PreviousNext
A list of your recent invoices.
NameAgeAddress
John Brown32New York No. 1 Lake Park
Jim Green42London No. 1 Lake Park
Joe Black32Sidney No. 1 Lake Park
import { Component } from '@angular/core';
 
import { ZardTableComponent } from '../table.component';
 
interface Person {
  key: string;
  name: string;
  age: number;
  address: string;
}
 
@Component({
  selector: 'z-demo-table-simple',
  imports: [ZardTableComponent],
  standalone: true,
  template: `
    <table z-table>
      <caption>
        A list of your recent invoices.
      </caption>
      <thead>
        <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Address</th>
        </tr>
      </thead>
      <tbody>
        @for (data of listOfData; track data.key) {
          <tr>
            <td class="font-medium">{{ data.name }}</td>
            <td>{{ data.age }}</td>
            <td>{{ data.address }}</td>
          </tr>
        }
      </tbody>
    </table>
  `,
})
export class ZardDemoTableSimpleComponent {
  listOfData: Person[] = [
    {
      key: '1',
      name: 'John Brown',
      age: 32,
      address: 'New York No. 1 Lake Park',
    },
    {
      key: '2',
      name: 'Jim Green',
      age: 42,
      address: 'London No. 1 Lake Park',
    },
    {
      key: '3',
      name: 'Joe Black',
      age: 32,
      address: 'Sidney No. 1 Lake Park',
    },
  ];
}
 

Installation

1

Run the CLI

Use the CLI to add the component to your project.

npx @ngzard/ui@latest add table
1

Add the component files

Create the component directory structure and add the following files to your project.

table.component.ts
table.component.ts
import { ChangeDetectionStrategy, Component, computed, input, ViewEncapsulation } from '@angular/core';
 
import type { ClassValue } from 'clsx';
 
import {
  tableVariants,
  tableHeaderVariants,
  tableBodyVariants,
  tableRowVariants,
  tableHeadVariants,
  tableCellVariants,
  tableCaptionVariants,
  type ZardTableVariants,
} from './table.variants';
import { mergeClasses } from '../../shared/utils/utils';
 
@Component({
  selector: 'table[z-table]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTable',
})
export class ZardTableComponent {
  readonly zType = input<ZardTableVariants['zType']>('default');
  readonly zSize = input<ZardTableVariants['zSize']>('default');
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() =>
    mergeClasses(
      tableVariants({
        zType: this.zType(),
        zSize: this.zSize(),
      }),
      this.class(),
    ),
  );
}
 
@Component({
  selector: 'thead[z-table-header]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableHeader',
})
export class ZardTableHeaderComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableHeaderVariants(), this.class()));
}
 
@Component({
  selector: 'tbody[z-table-body]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableBody',
})
export class ZardTableBodyComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableBodyVariants(), this.class()));
}
 
@Component({
  selector: 'tr[z-table-row]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableRow',
})
export class ZardTableRowComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableRowVariants(), this.class()));
}
 
@Component({
  selector: 'th[z-table-head]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableHead',
})
export class ZardTableHeadComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableHeadVariants(), this.class()));
}
 
@Component({
  selector: 'td[z-table-cell]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableCell',
})
export class ZardTableCellComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableCellVariants(), this.class()));
}
 
@Component({
  selector: 'caption[z-table-caption]',
  standalone: true,
  template: `<ng-content />`,
  changeDetection: ChangeDetectionStrategy.OnPush,
  encapsulation: ViewEncapsulation.None,
  host: {
    '[class]': 'classes()',
  },
  exportAs: 'zTableCaption',
})
export class ZardTableCaptionComponent {
  readonly class = input<ClassValue>('');
 
  protected readonly classes = computed(() => mergeClasses(tableCaptionVariants(), this.class()));
}
 
table.variants.ts
table.variants.ts
import { cva, type VariantProps } from 'class-variance-authority';
 
export const tableVariants = cva(
  'w-full caption-bottom text-sm [&_thead_tr]:border-b [&_tbody]:border-0 [&_tbody_tr:last-child]:border-0 [&_tbody_tr]:border-b [&_tbody_tr]:transition-colors [&_tbody_tr]:hover:bg-muted/50 [&_tbody_tr]:data-[state=selected]:bg-muted [&_th]:h-10 [&_th]:px-2 [&_th]:text-left [&_th]:align-middle [&_th]:font-medium [&_th]:text-muted-foreground [&_th:has([role=checkbox])]:pr-0 [&_th>[role=checkbox]]:translate-y-[2px] [&_td]:p-2 [&_td]:align-middle [&_td:has([role=checkbox])]:pr-0 [&_td>[role=checkbox]]:translate-y-[2px] [&_caption]:mt-4 [&_caption]:text-sm [&_caption]:text-muted-foreground',
  {
    variants: {
      zType: {
        default: '',
        striped: '[&_tbody_tr:nth-child(odd)]:bg-muted/50',
        bordered: 'border border-border',
      },
      zSize: {
        default: '',
        compact: '[&_td]:py-2 [&_th]:py-2',
        comfortable: '[&_td]:py-4 [&_th]:py-4',
      },
    },
    defaultVariants: {
      zType: 'default',
      zSize: 'default',
    },
  },
);
 
export const tableHeaderVariants = cva('[&_tr]:border-b', {
  variants: {},
  defaultVariants: {},
});
 
export const tableBodyVariants = cva('[&_tr:last-child]:border-0', {
  variants: {},
  defaultVariants: {},
});
 
export const tableRowVariants = cva('border-b transition-colors hover:bg-muted/50 data-[state=selected]:bg-muted', {
  variants: {},
  defaultVariants: {},
});
 
export const tableHeadVariants = cva(
  'h-10 px-2 text-left align-middle font-medium text-muted-foreground [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
  {
    variants: {},
    defaultVariants: {},
  },
);
 
export const tableCellVariants = cva(
  'p-2 align-middle [&:has([role=checkbox])]:pr-0 [&>[role=checkbox]]:translate-y-[2px]',
  {
    variants: {},
    defaultVariants: {},
  },
);
 
export const tableCaptionVariants = cva('mt-4 text-sm text-muted-foreground', {
  variants: {},
  defaultVariants: {},
});
 
export type ZardTableVariants = VariantProps<typeof tableVariants>;
export type ZardTableHeaderVariants = VariantProps<typeof tableHeaderVariants>;
export type ZardTableBodyVariants = VariantProps<typeof tableBodyVariants>;
export type ZardTableRowVariants = VariantProps<typeof tableRowVariants>;
export type ZardTableHeadVariants = VariantProps<typeof tableHeadVariants>;
export type ZardTableCellVariants = VariantProps<typeof tableCellVariants>;
export type ZardTableCaptionVariants = VariantProps<typeof tableCaptionVariants>;
 
table.module.ts
table.module.ts
import { NgModule } from '@angular/core';
 
import {
  ZardTableComponent,
  ZardTableHeaderComponent,
  ZardTableBodyComponent,
  ZardTableRowComponent,
  ZardTableHeadComponent,
  ZardTableCellComponent,
  ZardTableCaptionComponent,
} from './table.component';
 
const TABLE_COMPONENTS = [
  ZardTableComponent,
  ZardTableHeaderComponent,
  ZardTableBodyComponent,
  ZardTableRowComponent,
  ZardTableHeadComponent,
  ZardTableCellComponent,
  ZardTableCaptionComponent,
];
 
@NgModule({
  imports: [...TABLE_COMPONENTS],
  exports: [...TABLE_COMPONENTS],
})
export class ZardTableModule {}
 

Examples

simple

A list of your recent invoices.
NameAgeAddress
John Brown32New York No. 1 Lake Park
Jim Green42London No. 1 Lake Park
Joe Black32Sidney No. 1 Lake Park
import { Component } from '@angular/core';
 
import { ZardTableComponent } from '../table.component';
 
interface Person {
  key: string;
  name: string;
  age: number;
  address: string;
}
 
@Component({
  selector: 'z-demo-table-simple',
  imports: [ZardTableComponent],
  standalone: true,
  template: `
    <table z-table>
      <caption>
        A list of your recent invoices.
      </caption>
      <thead>
        <tr>
          <th>Name</th>
          <th>Age</th>
          <th>Address</th>
        </tr>
      </thead>
      <tbody>
        @for (data of listOfData; track data.key) {
          <tr>
            <td class="font-medium">{{ data.name }}</td>
            <td>{{ data.age }}</td>
            <td>{{ data.address }}</td>
          </tr>
        }
      </tbody>
    </table>
  `,
})
export class ZardDemoTableSimpleComponent {
  listOfData: Person[] = [
    {
      key: '1',
      name: 'John Brown',
      age: 32,
      address: 'New York No. 1 Lake Park',
    },
    {
      key: '2',
      name: 'Jim Green',
      age: 42,
      address: 'London No. 1 Lake Park',
    },
    {
      key: '3',
      name: 'Joe Black',
      age: 32,
      address: 'Sidney No. 1 Lake Park',
    },
  ];
}
 

payments

StatusEmailAmountActions
success
ken99@example.com
$316.00
success
Abe45@example.com
$242.00
processing
Monserrat44@example.com
$837.00
success
Silas22@example.com
$874.00
failed
carmella@example.com
$721.00
pending
jane.doe@example.com
$456.00
import { Component } from '@angular/core';
 
import { ZardBadgeComponent } from '../../badge/badge.component';
import { ZardButtonComponent } from '../../button/button.component';
import { ZardIconComponent } from '../../icon/icon.component';
import {
  ZardTableBodyComponent,
  ZardTableCellComponent,
  ZardTableComponent,
  ZardTableHeadComponent,
  ZardTableHeaderComponent,
  ZardTableRowComponent,
} from '../table.component';
 
export interface Payment {
  id: string;
  amount: number;
  status: 'pending' | 'processing' | 'success' | 'failed';
  email: string;
}
 
@Component({
  selector: 'z-demo-table-payments',
  imports: [
    ZardTableComponent,
    ZardTableHeaderComponent,
    ZardTableBodyComponent,
    ZardTableRowComponent,
    ZardTableHeadComponent,
    ZardTableCellComponent,
    ZardBadgeComponent,
    ZardButtonComponent,
    ZardIconComponent,
  ],
  standalone: true,
  template: `
    <div class="w-full">
      <div class="overflow-hidden rounded-md border">
        <table z-table>
          <thead z-table-header>
            <tr z-table-row>
              <th z-table-head>Status</th>
              <th z-table-head>Email</th>
              <th z-table-head class="text-right">Amount</th>
              <th z-table-head class="w-16">Actions</th>
            </tr>
          </thead>
          <tbody z-table-body>
            @for (payment of payments; track payment.id) {
              <tr z-table-row>
                <td z-table-cell>
                  <z-badge [zType]="getStatusVariant(payment.status)">
                    {{ payment.status }}
                  </z-badge>
                </td>
                <td z-table-cell>
                  <div class="lowercase">{{ payment.email }}</div>
                </td>
                <td z-table-cell>
                  <div class="text-right font-medium">{{ formatCurrency(payment.amount) }}</div>
                </td>
                <td z-table-cell>
                  <div class="flex items-center gap-2">
                    <z-button zType="ghost" (click)="copyPaymentId(payment.id)" title="Copy payment ID">
                      <div z-icon zType="copy"></div>
                    </z-button>
                    <z-button zType="ghost" (click)="viewDetails(payment)" title="View details">
                      <div z-icon zType="eye"></div>
                    </z-button>
                  </div>
                </td>
              </tr>
            } @empty {
              <tr z-table-row>
                <td z-table-cell [attr.colspan]="4" class="h-24 text-center">No results.</td>
              </tr>
            }
          </tbody>
        </table>
      </div>
    </div>
  `,
})
export class ZardDemoTablePaymentsComponent {
  payments: Payment[] = [
    {
      id: 'm5gr84i9',
      amount: 316,
      status: 'success',
      email: 'ken99@example.com',
    },
    {
      id: '3u1reuv4',
      amount: 242,
      status: 'success',
      email: 'Abe45@example.com',
    },
    {
      id: 'derv1ws0',
      amount: 837,
      status: 'processing',
      email: 'Monserrat44@example.com',
    },
    {
      id: '5kma53ae',
      amount: 874,
      status: 'success',
      email: 'Silas22@example.com',
    },
    {
      id: 'bhqecj4p',
      amount: 721,
      status: 'failed',
      email: 'carmella@example.com',
    },
    {
      id: 'abc123ef',
      amount: 456,
      status: 'pending',
      email: 'jane.doe@example.com',
    },
  ];
 
  formatCurrency(amount: number): string {
    return new Intl.NumberFormat('en-US', {
      style: 'currency',
      currency: 'USD',
    }).format(amount);
  }
 
  getStatusVariant(status: Payment['status']): 'default' | 'secondary' | 'destructive' | 'outline' {
    switch (status) {
      case 'success':
        return 'default';
      case 'processing':
        return 'secondary';
      case 'failed':
        return 'destructive';
      case 'pending':
        return 'outline';
      default:
        return 'secondary';
    }
  }
 
  copyPaymentId(id: string): void {
    navigator.clipboard.writeText(id);
    console.log('Payment ID copied:', id);
  }
 
  viewDetails(payment: Payment): void {
    console.log('View payment details:', payment);
  }
}
 

API

[z-table] Directive

z-table is a directive that accepts all properties supported by a native <table>. It automatically styles all nested table elements (thead, tbody, tr, th, td, caption) without requiring additional directives.

To customize the table, pass the following props to the directive.

Property Description Type Default
zType Table type default | striped | bordered default
zSize Table size default | compact | comfortable default

Usage

Basic Table

<table z-table>
  <thead>
    <tr>
      <th>Name</th>
      <th>Email</th>
      <th>Status</th>
    </tr>
  </thead>
  <tbody>
    <tr>
      <td>John Doe</td>
      <td>john@example.com</td>
      <td>Active</td>
    </tr>
  </tbody>
</table>

With Data Binding

<table z-table zType="striped">
  <thead>
    <tr>
      <th>Name</th>
      <th>Age</th>
    </tr>
  </thead>
  <tbody>
    @for (person of people; track person.id) {
    <tr>
      <td>{{ person.name }}</td>
      <td>{{ person.age }}</td>
    </tr>
    }
  </tbody>
</table>

Optional Sub-Components

For more granular control, you can use individual table components:

[z-table-header] Component

thead[z-table-header] applies styles to table header sections.

[z-table-body] Component

tbody[z-table-body] applies styles to table body sections.

[z-table-row] Component

tr[z-table-row] applies styles to table rows.

[z-table-head] Component

th[z-table-head] applies styles to table header cells.

[z-table-cell] Component

td[z-table-cell] applies styles to table data cells.

[z-table-caption] Component

caption[z-table-caption] applies styles to table captions.