Data Table
A component for displaying data in a table format with sorting and pagination
Multi-Select with Page Size Selector
Standard table with checkbox multi-selection and page size options
1
2<script lang="ts">
3 import { DataTable, DataTablePrimitives } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable
110 {data}
111 {columns}
112 filterColumn="email"
113 filterPlaceholder="Filter emails..."
114 pageSize={5}
115 pageSizeOptions={[5, 10, 20, 50]}
116 selectionMode="multi"
117 onRowSelectionChange={(selected) => console.log("Selected rows:", selected)}
118/>Single Select
Table with single row selection (radio button behavior)
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable
110 {data}
111 {columns}
112 filterColumn="email"
113 filterPlaceholder="Filter emails..."
114 pageSize={5}
115 selectionMode="single"
116 onRowSelectionChange={(selected) => console.log("Selected row:", selected)}
117/>No Selection
Table without row selection capability
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 accessorKey: "status",
32 header: "Status",
33 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
34 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
35 const { status } = getStatus();
36 return { render: () => `<div class="capitalize">${status}</div>` };
37 });
38 return renderSnippet(statusSnippet, { status: row.original.status });
39 },
40 },
41 {
42 accessorKey: "email",
43 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
44 renderComponent(DataTableColumnHeader, {
45 title: "Email",
46 onclick: column.getToggleSortingHandler(),
47 }),
48 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
49 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
50 const { email } = getEmail();
51 return { render: () => `<div class="lowercase">${email}</div>` };
52 });
53 return renderSnippet(emailSnippet, { email: row.original.email });
54 },
55 },
56 {
57 accessorKey: "amount",
58 header: () => {
59 const amountHeaderSnippet = createRawSnippet(() => {
60 return { render: () => `<div class="text-end">Amount</div>` };
61 });
62 return renderSnippet(amountHeaderSnippet);
63 },
64 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
65 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
66 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
67 const { amount } = getAmount();
68 const formatted = formatter.format(amount);
69 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
70 });
71 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
72 },
73 },
74 {
75 id: "actions",
76 enableHiding: false,
77 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
78 renderComponent(DataTableActions, {
79 id: row.original.id,
80 copyLabel: "Copy payment ID",
81 actions: [
82 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
83 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
84 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
85 ],
86 }),
87 },
88 ];
89</script>
90
91<DataTable
92 {data}
93 {columns}
94 filterColumn="email"
95 filterPlaceholder="Filter emails..."
96 pageSize={5}
97 selectionMode="none"
98/>Striped Variant
Alternating row background colors for better readability
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable
110 {data}
111 {columns}
112 variant="striped"
113 filterColumn="email"
114 filterPlaceholder="Filter emails..."
115 pageSize={5}
116/>Bordered Variant
Enhanced borders between columns and around the table
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable
110 {data}
111 {columns}
112 variant="bordered"
113 filterColumn="email"
114 filterPlaceholder="Filter emails..."
115 pageSize={5}
116/>Compact Variant
Reduced padding and smaller text for dense data display
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4</script>
5
6<DataTable
7 {data}
8 {columns}
9 variant="compact"
10 filterColumn="email"
11 filterPlaceholder="Filter emails..."
12 pageSize={8}
13/>Minimal Configuration
No filter, no column toggle, larger page size
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable
110 {data}
111 {columns}
112 showFilter={false}
113 showColumnToggle={false}
114 pageSize={10}
115/>No Pagination
Shows all rows without pagination controls
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4</script>
5
6<DataTable
7 {data}
8 {columns}
9 showPagination={false}
10 pageSize={100}
11/>Expandable Rows
Click the chevron to expand rows and view additional details
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13
14 const data: Payment[] = [
15 { id: "m5gr84i9", amount: 316, status: "Success", email: "ken99@yahoo.com" },
16 { id: "3u1reuv4", amount: 242, status: "Success", email: "Abe45@gmail.com" },
17 { id: "derv1ws0", amount: 837, status: "Processing", email: "Monserrat44@gmail.com" },
18 { id: "5kma53ae", amount: 874, status: "Success", email: "Silas22@gmail.com" },
19 { id: "bhqecj4p", amount: 721, status: "Failed", email: "carmella@hotmail.com" },
20 { id: "a1b2c3d4", amount: 450, status: "Pending", email: "john.doe@example.com" },
21 { id: "e5f6g7h8", amount: 920, status: "Success", email: "jane.smith@gmail.com" },
22 { id: "i9j0k1l2", amount: 350, status: "Processing", email: "bob.wilson@yahoo.com" },
23 { id: "m3n4o5p6", amount: 680, status: "Failed", email: "alice.brown@hotmail.com" },
24 { id: "q7r8s9t0", amount: 530, status: "Success", email: "charlie.davis@gmail.com" },
25 { id: "u1v2w3x4", amount: 290, status: "Pending", email: "diana.miller@example.com" },
26 { id: "y5z6a7b8", amount: 775, status: "Processing", email: "edward.jones@yahoo.com" },
27 ];
28
29 const columns: DataTablePrimitives.ColumnDef<Payment>[] = [
30 {
31 id: "select",
32 header: ({ table }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
33 renderComponent(DataTableCheckbox, {
34 checked: table.getIsAllPageRowsSelected(),
35 indeterminate: table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
36 onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
37 "aria-label": "Select all",
38 }),
39 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
40 renderComponent(DataTableCheckbox, {
41 checked: row.getIsSelected(),
42 onCheckedChange: (value) => row.toggleSelected(!!value),
43 "aria-label": "Select row",
44 }),
45 enableSorting: false,
46 enableHiding: false,
47 },
48 {
49 accessorKey: "status",
50 header: "Status",
51 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
52 const statusSnippet = createRawSnippet<[{ status: string }]>((getStatus) => {
53 const { status } = getStatus();
54 return { render: () => `<div class="capitalize">${status}</div>` };
55 });
56 return renderSnippet(statusSnippet, { status: row.original.status });
57 },
58 },
59 {
60 accessorKey: "email",
61 header: ({ column }: DataTablePrimitives.HeaderContext<Payment, unknown>) =>
62 renderComponent(DataTableColumnHeader, {
63 title: "Email",
64 onclick: column.getToggleSortingHandler(),
65 }),
66 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
67 const emailSnippet = createRawSnippet<[{ email: string }]>((getEmail) => {
68 const { email } = getEmail();
69 return { render: () => `<div class="lowercase">${email}</div>` };
70 });
71 return renderSnippet(emailSnippet, { email: row.original.email });
72 },
73 },
74 {
75 accessorKey: "amount",
76 header: () => {
77 const amountHeaderSnippet = createRawSnippet(() => {
78 return { render: () => `<div class="text-end">Amount</div>` };
79 });
80 return renderSnippet(amountHeaderSnippet);
81 },
82 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) => {
83 const formatter = new Intl.NumberFormat("en-US", { style: "currency", currency: "USD" });
84 const amountCellSnippet = createRawSnippet<[{ amount: number }]>((getAmount) => {
85 const { amount } = getAmount();
86 const formatted = formatter.format(amount);
87 return { render: () => `<div class="text-end font-medium">${formatted}</div>` };
88 });
89 return renderSnippet(amountCellSnippet, { amount: row.original.amount });
90 },
91 },
92 {
93 id: "actions",
94 enableHiding: false,
95 cell: ({ row }: DataTablePrimitives.CellContext<Payment, unknown>) =>
96 renderComponent(DataTableActions, {
97 id: row.original.id,
98 copyLabel: "Copy payment ID",
99 actions: [
100 { label: "View details", onclick: () => console.log("View details for", row.original.id) },
101 { label: "Edit payment", onclick: () => console.log("Edit payment", row.original.id) },
102 { label: "Delete", onclick: () => console.log("Delete payment", row.original.id) },
103 ],
104 }),
105 },
106 ];
107</script>
108
109<DataTable {data} {columns} expandable={true} pageSize={5}>
110 {#snippet renderSubComponent({ row })}
111 {@const payment = row.original}
112 <div class="space-y-2 text-sm">
113 <div class="grid grid-cols-2 gap-4">
114 <div>
115 <span class="font-semibold">Payment ID:</span>
116 {payment.id}
117 </div>
118 <div>
119 <span class="font-semibold">Status:</span>
120 <span class="capitalize">{payment.status}</span>
121 </div>
122 <div>
123 <span class="font-semibold">Amount:</span>
124 ${payment.amount.toFixed(2)}
125 </div>
126 <div>
127 <span class="font-semibold">Email:</span>
128 {payment.email}
129 </div>
130 </div>
131 </div>
132 {/snippet}
133</DataTable>Responsive Design
The DataTable automatically adapts to different screen sizes. Use the buttons to force different display modes.
1
2<script lang="ts">
3 import { DataTable } from "@kareyes/aether";
4
5 const { DataTableCheckbox, DataTableColumnHeader, renderComponent } = DataTablePrimitives;
6let selectedMode: DataTablePrimitives.ResponsiveMode = $state("scroll");
7 type Payment = {
8 id: string;
9 amount: number;
10 status: "Pending" | "Processing" | "Success" | "Failed";
11 email: string;
12 };
13 const paymentData2: Payment2[] = [
14 {
15 id: "PAY-001",
16 amount: 316,
17 status: "Success",
18 email: "ken99@yahoo.com",
19 date: "2025-01-15",
20 method: "Credit Card",
21 },
22 {
23 id: "PAY-002",
24 amount: 242,
25 status: "Success",
26 email: "abe45@gmail.com",
27 date: "2025-01-14",
28 method: "PayPal",
29 },
30 {
31 id: "PAY-003",
32 amount: 837,
33 status: "Processing",
34 email: "monserrat44@gmail.com",
35 date: "2025-01-14",
36 method: "Bank Transfer",
37 }
38 ];
39 const responsiveColumns: DataTablePrimitives.ColumnDef<Payment2>[] = [
40 {
41 id: "select",
42 header: ({ table }: DataTablePrimitives.HeaderContext<Payment2, unknown>) =>
43 renderComponent(DataTableCheckbox, {
44 checked: table.getIsAllPageRowsSelected(),
45 indeterminate:
46 table.getIsSomePageRowsSelected() &&
47 !table.getIsAllPageRowsSelected(),
48 onCheckedChange: (value) =>
49 table.toggleAllPageRowsSelected(!!value),
50 "aria-label": "Select all",
51 }),
52 cell: ({ row }:DataTablePrimitives.CellContext<Payment2, unknown>) =>
53 renderComponent(DataTableCheckbox, {
54 checked: row.getIsSelected(),
55 onCheckedChange: (value) => row.toggleSelected(!!value),
56 "aria-label": "Select row",
57 }),
58 enableSorting: false,
59 enableHiding: false,
60 },
61 {
62 accessorKey: "status",
63 header: "Status",
64 meta: {
65 mobileLabel: "Payment Status",
66 priority: 1,
67 alwaysVisible: true,
68 } as DataTablePrimitives.DataTableColumnMeta,
69 cell: ({ row }) => {
70 const status = row.original.status;
71 const snippet = createRawSnippet<[{ status: string }]>(
72 (getStatus) => {
73 const { status } = getStatus();
74 const colorClass =
75 status === "Success"
76 ? "text-green-600"
77 : status === "Failed"
78 ? "text-red-600"
79 : status === "Processing"
80 ? "text-blue-600"
81 : "text-yellow-600";
82 return {
83 render: () =>
84 `<span class="capitalize font-medium ${colorClass}">${status}</span>`,
85 };
86 },
87 );
88 return renderSnippet(snippet, { status });
89 },
90 },
91 {
92 accessorKey: "email",
93 header: ({ column }: DataTablePrimitives.HeaderContext<Payment2, unknown>) =>
94 renderComponent(DataTableColumnHeader, {
95 title: "Email",
96 onclick: column.getToggleSortingHandler(),
97 }),
98 meta: {
99 mobileLabel: "Customer Email",
100 priority: 2,
101 } as DataTablePrimitives.DataTableColumnMeta,
102 cell: ({ row }) => {
103 const snippet = createRawSnippet<[{ email: string }]>(
104 (getEmail) => {
105 const { email } = getEmail();
106 return {
107 render: () =>
108 `<span class="lowercase">${email}</span>`,
109 };
110 },
111 );
112 return renderSnippet(snippet, { email: row.original.email });
113 },
114 },
115 {
116 accessorKey: "amount",
117 header: "Amount",
118 meta: {
119 mobileLabel: "Amount (USD)",
120 priority: 3,
121 } as DataTablePrimitives.DataTableColumnMeta,
122 cell: ({ row }) => {
123 const formatter = new Intl.NumberFormat("en-US", {
124 style: "currency",
125 currency: "USD",
126 });
127 const snippet = createRawSnippet<[{ amount: string }]>(
128 (getAmount) => {
129 const { amount } = getAmount();
130 return {
131 render: () =>
132 `<span class="font-semibold">${amount}</span>`,
133 };
134 },
135 );
136 return renderSnippet(snippet, {
137 amount: formatter.format(row.original.amount),
138 });
139 },
140 },
141 {
142 accessorKey: "date",
143 header: "Date",
144 meta: {
145 mobileLabel: "Transaction Date",
146 priority: 4,
147 } as DataTablePrimitives.DataTableColumnMeta,
148 },
149 {
150 accessorKey: "method",
151 header: "Method",
152 meta: {
153 mobileLabel: "Payment Method",
154 priority: 5,
155 hiddenOnMobile: false,
156 } as DataTablePrimitives.DataTableColumnMeta,
157 },
158 {
159 id: "actions",
160 enableHiding: false,
161 cell: ({ row }) =>
162 renderComponent(DataTableActions, {
163 id: row.original.id,
164 copyLabel: "Copy payment ID",
165 actions: [
166 {
167 label: "View details",
168 onclick: () => console.log("View", row.original),
169 },
170 {
171 label: "Refund",
172 onclick: () => console.log("Refund", row.original),
173 },
174 {
175 label: "Delete",
176 onclick: () => console.log("Delete", row.original),
177 },
178 ],
179 }),
180 },
181 ];
182 </script>
183
184
185 <DataTable
186 data={paymentData2}
187 columns={responsiveColumns}
188 responsiveMode={selectedMode}
189 filterColumn="email"
190 filterPlaceholder="Filter by email..."
191 pageSize={5}
192 variant="default"
193 selectionMode="multi"
194 />Column Mobile Metadata
Customize how columns appear in mobile card view using the meta field.
Data Table Component
A powerful, flexible data table component built with TanStack Table and Svelte 5 featuring advanced customization, row selection, pagination, filtering, sorting, and expandable rows.
Features
- ✅ Sorting - Click column headers to sort data
- ✅ Filtering - Search and filter across columns
- ✅ Pagination - Navigate through large datasets with customizable page sizes
- ✅ Row Selection - Multi-select, single-select, or no selection modes
- ✅ Column Visibility - Show/hide columns dynamically
- ✅ Expandable Rows - Collapsible row details with custom content
- ✅ Table Variants - Multiple visual styles (default, striped, bordered, compact)
- ✅ Responsive Design - Card-based layout on mobile, table on desktop
- ✅ Customizable - Full control over rendering with Svelte 5 snippets
- ✅ TypeScript - Fully typed with generics support
Quick Start
<script lang="ts">
import { DataTable, type ColumnDef } from "@kareyes/aether";
type User = {
id: string;
name: string;
email: string;
};
const data: User[] = [
{ id: "1", name: "John Doe", email: "john@example.com" },
{ id: "2", name: "Jane Smith", email: "jane@example.com" },
];
const columns: ColumnDef<User>[] = [
{ accessorKey: "name", header: "Name" },
{ accessorKey: "email", header: "Email" },
];
</script>
<DataTable {data} {columns} />
Props Reference
DataTable Props
| Prop | Type | Default | Description |
|---|---|---|---|
data |
TData[] |
required | Array of data objects to display |
columns |
ColumnDef<TData, TValue>[] |
required | Column definitions |
filterColumn |
string |
"email" |
Column key to filter |
filterPlaceholder |
string |
"Filter..." |
Filter input placeholder text |
pageSize |
number |
10 |
Initial number of rows per page |
pageSizeOptions |
number[] |
[10, 20, 30, 40, 50] |
Available page size options |
showPageSizeSelector |
boolean |
true |
Show page size dropdown selector |
showFilter |
boolean |
true |
Show filter input |
showColumnToggle |
boolean |
true |
Show column visibility dropdown |
showPagination |
boolean |
true |
Show pagination controls |
showRowSelection |
boolean |
true |
Show row selection count |
variant |
'default' | 'striped' | 'bordered' | 'compact' |
'default' |
Table visual style |
expandable |
boolean |
false |
Enable row expansion |
selectionMode |
'multi' | 'single' | 'none' |
'multi' |
Row selection behavior |
getRowCanExpand |
(row: Row<TData>) => boolean |
() => true |
Determine which rows can expand |
renderSubComponent |
Snippet<[{ row: Row<TData> }]> |
undefined |
Snippet for expanded row content |
onPageChange |
(page: number, action?: 'next' | 'previous') => void |
undefined |
Callback when page changes |
onPageSizeChange |
(pageSize: number) => void |
undefined |
Callback when page size changes |
onRowSelectionChange |
(selectedRows: TData[]) => void |
undefined |
Callback when selection changes |
responsiveMode |
'auto' | 'card' | 'scroll' |
'scroll' |
Mobile display mode |
showFilterColumnSelector |
boolean |
true |
Show dropdown to select which column to filter |
Table Variants
Default
Standard table appearance with hover effects.
<DataTable {data} {columns} variant="default" />
Striped
Alternating row background colors for better readability.
<DataTable {data} {columns} variant="striped" />
Bordered
Enhanced borders between columns and around the table.
<DataTable {data} {columns} variant="bordered" />
Compact
Reduced padding and smaller text for dense data display.
<DataTable {data} {columns} variant="compact" />
Responsive Design
The DataTable automatically adapts to different screen sizes. On mobile devices (< 768px), it switches from a traditional table layout to a card-based layout for better readability and touch interaction.
Responsive Modes
| Mode | Description |
|---|---|
'auto' |
Automatically switches to cards on mobile, table on desktop (default) |
'card' |
Always display as cards regardless of screen size |
'scroll' |
Keep table layout with horizontal scrolling on all screen sizes |
Auto Mode (Default)
The table automatically detects the screen size and switches layouts:
<DataTable {data} {columns} responsiveMode="auto" />
- Desktop (≥768px): Traditional table layout
- Mobile (<768px): Card-based stacked layout
Force Card Mode
Always display data as cards, useful for dashboards or when you prefer the card layout:
<DataTable {data} {columns} responsiveMode="card" />
Scroll Mode
Keep the traditional table with horizontal scrolling on smaller screens:
<DataTable {data} {columns} responsiveMode="scroll" />
Column Mobile Metadata
Customize how columns appear in the mobile card view using the meta field on column definitions.
DataTableColumnMeta Type
interface DataTableColumnMeta {
/** Hide this column in mobile card view */
hiddenOnMobile?: boolean;
/** Custom label for mobile view (defaults to header text) */
mobileLabel?: string;
/** Display priority in card view - lower numbers appear first (default: 100) */
priority?: number;
/** Always show this column in card view regardless of visibility settings */
alwaysVisible?: boolean;
}
Example: Columns with Mobile Metadata
import type { ColumnDef, DataTableColumnMeta } from "@kareyes/aether";
const columns: ColumnDef<Payment>[] = [
{
accessorKey: "status",
header: "Status",
meta: {
mobileLabel: "Payment Status", // Custom label in card view
priority: 1, // Appears first in card
alwaysVisible: true // Highlighted in card view
} as DataTableColumnMeta,
},
{
accessorKey: "email",
header: "Email",
meta: {
mobileLabel: "Email Address",
priority: 2
} as DataTableColumnMeta,
},
{
accessorKey: "amount",
header: "Amount",
meta: {
mobileLabel: "Amount (USD)",
priority: 3
} as DataTableColumnMeta,
},
{
accessorKey: "internalId",
header: "Internal ID",
meta: {
hiddenOnMobile: true // Hidden in card view
} as DataTableColumnMeta,
},
];
Mobile Card Layout
In card mode, each row renders as a card with the following structure:
┌─────────────────────────────────────┐
│ [checkbox] [expand] [actions] │ ← Card header (selection/actions)
├─────────────────────────────────────┤
│ LABEL │
│ Cell value │ ← Data fields (sorted by priority)
│ │
│ LABEL │
│ Cell value │
├─────────────────────────────────────┤
│ Expanded content (if expanded) │ ← Optional expanded content
└─────────────────────────────────────┘
Responsive Header & Footer
The filter controls and pagination automatically adapt to mobile:
Mobile Layout:
- Filter input expands to full width
- Column toggle button expands to full width
- Pagination shows "Page X of Y" indicator
- Previous/Next buttons expand side-by-side
- Page size selector hidden (saves space)
Desktop Layout:
- Standard horizontal layout with all controls visible
Selection Modes
Multi-Select (Default)
Allow selecting multiple rows with checkboxes.
<DataTable
{data}
{columns}
selectionMode="multi"
onRowSelectionChange={(selected) => console.log(selected)}
/>
Single-Select
Radio button behavior - only one row at a time.
<DataTable
{data}
{columns}
selectionMode="single"
onRowSelectionChange={(selected) => console.log(selected[0])}
/>
No Selection
Disable row selection entirely.
<DataTable {data} {columns} selectionMode="none" />
Column Definitions
Basic Column
const columns: ColumnDef<User>[] = [
{
accessorKey: "name",
header: "Name",
},
];
Sortable Column
import { DataTableColumnHeader, renderComponent } from "@kareyes/aether";
const columns: ColumnDef<User>[] = [
{
accessorKey: "email",
header: ({ column }) =>
renderComponent(DataTableColumnHeader, {
title: "Email",
onclick: column.getToggleSortingHandler()
}),
},
];
Checkbox Selection Column
import {
DataTableCheckbox,
renderComponent,
type HeaderContext,
type CellContext
} from "@kareyes/aether";
const columns: ColumnDef<User>[] = [
{
id: "select",
header: ({ table }: HeaderContext<User, unknown>) =>
renderComponent(DataTableCheckbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate:
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
"aria-label": "Select all"
}),
cell: ({ row }: CellContext<User, unknown>) =>
renderComponent(DataTableCheckbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
"aria-label": "Select row"
}),
enableSorting: false,
enableHiding: false
},
];
Custom Cell Rendering with Snippets
import { createRawSnippet } from "svelte";
import { renderSnippet } from "@kareyes/aether";
const columns: ColumnDef<User>[] = [
{
accessorKey: "status",
header: "Status",
cell: ({ row }) => {
const snippet = createRawSnippet<[{ status: string }]>((getStatus) => {
const { status } = getStatus();
return {
render: () => `<span class="capitalize">${status}</span>`
};
});
return renderSnippet(snippet, { status: row.original.status });
}
},
];
Actions Column
import { DataTableActions, renderComponent } from "@kareyes/aether";
const columns: ColumnDef<User>[] = [
{
id: "actions",
enableHiding: false,
cell: ({ row }) =>
renderComponent(DataTableActions, {
id: row.original.id,
copyLabel: "Copy user ID",
actions: [
{
label: "View details",
onclick: () => console.log("View", row.original)
},
{
label: "Edit",
onclick: () => console.log("Edit", row.original)
},
{
label: "Delete",
onclick: () => console.log("Delete", row.original)
}
]
})
},
];
Expandable Rows
Enable row expansion to show additional details.
<script lang="ts">
import { DataTable, type ColumnDef } from "@kareyes/aether";
type Payment = {
id: string;
amount: number;
email: string;
details?: string;
};
const data: Payment[] = [
{ id: "1", amount: 100, email: "user@example.com", details: "Additional info" },
];
const columns: ColumnDef<Payment>[] = [
{ accessorKey: "id", header: "ID" },
{ accessorKey: "amount", header: "Amount" },
{ accessorKey: "email", header: "Email" },
];
</script>
<DataTable {data} {columns} expandable={true}>
{#snippet renderSubComponent({ row })}
{@const payment = row.original}
<div class="p-4 space-y-2">
<h4 class="font-semibold">Payment Details</h4>
<p>ID: {payment.id}</p>
<p>Amount: ${payment.amount}</p>
<p>Email: {payment.email}</p>
{#if payment.details}
<p class="text-muted-foreground">{payment.details}</p>
{/if}
</div>
{/snippet}
</DataTable>
Conditional Row Expansion
<DataTable
{data}
{columns}
expandable={true}
getRowCanExpand={(row) => row.original.hasDetails === true}
>
{#snippet renderSubComponent({ row })}
<!-- Expanded content -->
{/snippet}
</DataTable>
Pagination
Custom Page Size
<DataTable {data} {columns} pageSize={20} />
Custom Page Size Options
<DataTable
{data}
{columns}
pageSize={5}
pageSizeOptions={[5, 10, 25, 50, 100]}
/>
Page Change Callback
<DataTable
{data}
{columns}
onPageChange={(page, action) => {
console.log(`Navigated to page ${page} via ${action}`);
}}
onPageSizeChange={(size) => {
console.log(`Page size changed to ${size}`);
}}
/>
Disable Pagination
<DataTable
{data}
{columns}
showPagination={false}
pageSize={1000}
/>
Filtering
Filter Column Selector
By default, a dropdown appears next to the filter input allowing users to choose which column to filter. This is useful when your table has multiple filterable columns.
<DataTable
{data}
{columns}
showFilterColumnSelector={true}
filterPlaceholder="Search..."
/>
The dropdown automatically lists all columns that have an accessorKey defined.
Set Initial Filter Column
<DataTable
{data}
{columns}
filterColumn="name"
filterPlaceholder="Search by name..."
/>
Disable Filter Column Selector
If you want a fixed filter column without the dropdown:
<DataTable
{data}
{columns}
filterColumn="email"
showFilterColumnSelector={false}
filterPlaceholder="Filter by email..."
/>
Disable Filter
<DataTable {data} {columns} showFilter={false} />
Column Visibility
The column visibility dropdown is shown by default. Users can toggle which columns are visible.
Disable Column Toggle
<DataTable {data} {columns} showColumnToggle={false} />
Non-Hideable Columns
const columns: ColumnDef<User>[] = [
{
accessorKey: "id",
header: "ID",
enableHiding: false // Cannot be hidden
},
{
accessorKey: "name",
header: "Name",
// Can be hidden by default
},
];
Advanced Examples
Complete Feature-Rich Table
<script lang="ts">
import { DataTable, type ColumnDef } from "@kareyes/aether";
import {
DataTableCheckbox,
DataTableColumnHeader,
DataTableActions,
renderComponent
} from "@kareyes/aether";
type User = {
id: string;
name: string;
email: string;
role: string;
status: "active" | "inactive";
};
const users: User[] = [
{ id: "1", name: "John", email: "john@example.com", role: "Admin", status: "active" },
// ... more users
];
const columns: ColumnDef<User>[] = [
{
id: "select",
header: ({ table }) =>
renderComponent(DataTableCheckbox, {
checked: table.getIsAllPageRowsSelected(),
indeterminate:
table.getIsSomePageRowsSelected() && !table.getIsAllPageRowsSelected(),
onCheckedChange: (value) => table.toggleAllPageRowsSelected(!!value),
}),
cell: ({ row }) =>
renderComponent(DataTableCheckbox, {
checked: row.getIsSelected(),
onCheckedChange: (value) => row.toggleSelected(!!value),
}),
enableSorting: false,
enableHiding: false
},
{
accessorKey: "name",
header: ({ column }) =>
renderComponent(DataTableColumnHeader, {
title: "Name",
onclick: column.getToggleSortingHandler()
}),
},
{
accessorKey: "email",
header: ({ column }) =>
renderComponent(DataTableColumnHeader, {
title: "Email",
onclick: column.getToggleSortingHandler()
}),
},
{
accessorKey: "role",
header: "Role",
},
{
accessorKey: "status",
header: "Status",
},
{
id: "actions",
enableHiding: false,
cell: ({ row }) =>
renderComponent(DataTableActions, {
id: row.original.id,
copyLabel: "Copy user ID",
actions: [
{ label: "View", onclick: () => viewUser(row.original) },
{ label: "Edit", onclick: () => editUser(row.original) },
{ label: "Delete", onclick: () => deleteUser(row.original) },
]
})
},
];
function viewUser(user: User) {
console.log("Viewing", user);
}
function editUser(user: User) {
console.log("Editing", user);
}
function deleteUser(user: User) {
console.log("Deleting", user);
}
</script>
<DataTable
data={users}
{columns}
variant="striped"
filterColumn="name"
filterPlaceholder="Search users..."
pageSize={10}
pageSizeOptions={[10, 20, 50, 100]}
selectionMode="multi"
onPageChange={(page, action) => console.log("Page:", page, action)}
onPageSizeChange={(size) => console.log("Page size:", size)}
onRowSelectionChange={(selected) => console.log("Selected:", selected)}
/>
Minimal Table
<DataTable
{data}
{columns}
showFilter={false}
showColumnToggle={false}
showPagination={false}
showRowSelection={false}
selectionMode="none"
/>
Components
DataTable
Main table component with all features.
DataTableCheckbox
Checkbox component for row selection.
Props:
checked?: boolean- Checkbox stateindeterminate?: boolean- Indeterminate stateonCheckedChange?: (checked: boolean) => void- Change handleraria-label?: string- Accessibility label
DataTableColumnHeader
Sortable column header with visual indicators.
Props:
title: string- Header textonclick?: () => void- Click handler for sorting
DataTableActions
Dropdown menu for row actions.
Props:
id: string- Row identifier (used for copy action)copyLabel?: string- Label for copy action (default: "Copy ID")actions?: Array<{ label: string; onclick: () => void }>- Custom actions
DataTableMobileCard
Card component for rendering a single row in mobile view.
Props:
row: Row<TData>- The row data to renderexpandable?: boolean- Enable row expansionshowRowSelection?: boolean- Show selection checkboxselectionMode?: 'multi' | 'single' | 'none'- Selection moderenderSubComponent?: Snippet- Content for expanded statevariant?: TableVariant- Visual variantindex?: number- Row index for striped variant
DataTableMobileView
Container component that renders all rows as cards in mobile view.
Props:
table: Table<TData>- The TanStack Table instanceexpandable?: boolean- Enable row expansionshowRowSelection?: boolean- Show selection checkboxesselectionMode?: 'multi' | 'single' | 'none'- Selection moderenderSubComponent?: Snippet- Content for expanded rowsvariant?: TableVariant- Visual variant
Utility Functions
renderComponent
Renders a Svelte component within a table cell.
import { renderComponent } from "@kareyes/aether";
renderComponent(MyComponent, { prop1: "value" })
renderSnippet
Renders a Svelte snippet within a table cell.
import { createRawSnippet } from "svelte";
import { renderSnippet } from "@kareyes/aether";
const snippet = createRawSnippet(() => ({
render: () => `<div>Content</div>`
}));
renderSnippet(snippet, { data: "value" })
getColumnMobileLabel
Extracts the display label for a column in mobile card view.
import { getColumnMobileLabel } from "@kareyes/aether";
// Returns: meta.mobileLabel > string header > formatted column id
const label = getColumnMobileLabel(column);
getMobileVisibleColumns
Filters and sorts columns for mobile card view based on meta configuration.
import { getMobileVisibleColumns } from "@kareyes/aether";
// Returns columns sorted by priority, excluding those with hiddenOnMobile: true
const mobileColumns = getMobileVisibleColumns(allColumns);
isSpecialColumn
Checks if a column is a special column (select or actions) that should be rendered separately in the card header.
import { isSpecialColumn } from "@kareyes/aether";
if (isSpecialColumn(column)) {
// Render in card header instead of body
}
TypeScript Support
The DataTable component is fully typed with TypeScript generics:
import type { ColumnDef } from "@kareyes/aether";
type MyData = {
id: string;
name: string;
};
const columns: ColumnDef<MyData>[] = [
{ accessorKey: "name", header: "Name" }
];
Best Practices
- Define Types: Always define TypeScript types for your data
- Column IDs: Use unique IDs for columns without accessorKey
- Memoization: Consider memoizing large datasets
- Accessibility: Provide aria-labels for selection checkboxes
- Performance: Use
enableSorting: falsefor non-sortable columns - Callbacks: Handle selection and pagination changes appropriately
Troubleshooting
Table not updating when data changes
Ensure you're reassigning the data array, not mutating it:
// ✅ Good
data = [...data, newItem];
// ❌ Bad
data.push(newItem);
Selection not working
Make sure you include the selection column in your columns definition and use the correct selection mode.
Expandable rows not showing
Verify that:
expandable={true}is set- You've provided a
renderSubComponentsnippet getRowCanExpandreturns true for the rows you want to expand
License
Part of the Aether UI component library.