When using the datatable component with custom cell renderers, the underlying TanStack Table library expects cell content to be returned as Lit templates. This creates a problem for Svelte users:
// ❌ This forces Svelte users to import Lit
import { html } from 'lit'
const columns = [
{
id: 'status',
cell: (row) => html`<dap-ds-badge variant="success">${row.status}</dap-ds-badge>`
}
]
Why this is a problem:
- Svelte developers must learn and import Lit's
htmltagged template function - Adds unnecessary Lit as a dependency in Svelte projects
- Defeats the purpose of framework-agnostic web components
- Can't use Svelte components in table cells without Lit
createSvelteCell HelperThe design system provides a createSvelteCell helper that allows you to render Svelte components in datatable cells without importing Lit.
The Svelte helper is included in the main design system package:
npm i dap-design-system
import { createSvelteCell } from 'dap-design-system/helpers/svelte'
import StatusBadge from './StatusBadge.svelte'
const columns = [
{
id: 'status',
header: 'Status',
cell: (info) => createSvelteCell({
component: StatusBadge,
props: {
status: info.row.original.status,
variant: 'success'
}
})
}
]
The createSvelteCell helper:
- Automatically detects Svelte version (4 or 5)
- Uses the appropriate mounting API (
mount()for Svelte 5,new Component()for Svelte 4) - Properly handles component lifecycle (creation and destruction)
- Manages cleanup when cells are removed (pagination, sorting, filtering)
interface SvelteCellOptions {
component: any
props?: Record<string, any>
on?: Record<string, (...args: any[]) => void>
context?: Map<any, any> | Record<string, any>
}
function createSvelteCell(
componentOrOptions: any | SvelteCellOptions,
props?: Record<string, any>
): RenderCallbackContent
This example demonstrates:
- Server-side pagination
- Loading states
- Svelte 5 components as cell renderers
- Event handling with callback props
StatusBadge.svelte:
<script lang="ts">
interface Props {
status: string;
onclick?: (status: string) => void;
}
let { status, onclick }: Props = $props();
// Determine variant based on status
const variant = $derived(
status === 'active' ? 'success' : status === 'pending' ? 'warning' : 'danger'
);
function handleClick() {
onclick?.(status);
}
</script>
<dap-ds-badge {variant} onclick={handleClick} style="cursor: pointer;">
{status}
</dap-ds-badge>UserAvatar.svelte:
<script lang="ts">
interface Props {
name: string;
email: string;
onAvatarClick?: (name: string) => void;
}
let { name, email, onAvatarClick }: Props = $props();
// Generate initials from name
const initials = $derived(
name
.split(' ')
.map((n) => n[0])
.join('')
.toUpperCase()
.slice(0, 2)
);
function handleAvatarClick() {
onAvatarClick?.(name);
}
</script>
<div class="user-cell">
<dap-ds-avatar
initials={initials}
size="sm"
onclick={handleAvatarClick}
interactive
aria-label="View profile for {name}"
/>
<div class="user-info">
<span class="user-name">{name}</span>
<span class="user-email">{email}</span>
</div>
</div>
<style>
.user-cell {
display: flex;
align-items: center;
gap: 0.75rem;
}
.user-info {
display: flex;
flex-direction: column;
}
.user-name {
font-weight: 500;
}
.user-email {
font-size: 0.875rem;
color: var(--dds-color-text-secondary, #666);
}
</style>ActionButtons.svelte:
<script lang="ts">
interface RowData {
id: number;
name: string;
email: string;
status: string;
}
interface Props {
rowData: RowData;
onEdit?: (rowData: RowData) => void;
onDelete?: (rowData: RowData) => void;
}
let { rowData, onEdit, onDelete }: Props = $props();
function handleEdit() {
onEdit?.(rowData);
}
function handleDelete() {
onDelete?.(rowData);
}
</script>
<div class="action-buttons">
<dap-ds-button variant="outline" size="sm" onclick={handleEdit}>
<dap-ds-icon-system-edit-line slot="prefix" />
Edit
</dap-ds-button>
<dap-ds-button variant="outline" size="sm" color="danger" onclick={handleDelete}>
<dap-ds-icon-system-delete-bin-line slot="prefix" />
Delete
</dap-ds-button>
</div>
<style>
.action-buttons {
display: flex;
gap: 0.5rem;
}
</style>+page.svelte:
<script lang="ts">
import { onMount } from 'svelte';
import { createSvelteCell } from 'dap-design-system/helpers/svelte';
import StatusBadge from '$lib/components/StatusBadge.svelte';
import UserAvatar from '$lib/components/UserAvatar.svelte';
import ActionButtons from '$lib/components/ActionButtons.svelte';
// Define User type
interface User {
id: number;
name: string;
email: string;
status: string;
}
// State
let datatable: HTMLElement | null = $state(null);
let currentPage = $state(0);
const pageSize = 10;
// Mock data generator (replace with API call)
function generateMockUsers(page: number, pageSize: number) {
const total = 100;
const start = (page - 1) * pageSize;
const end = Math.min(start + pageSize, total);
const statuses = ['active', 'pending', 'inactive'];
const data: User[] = [];
for (let i = start; i < end; i++) {
data.push({
id: i + 1,
name: `User ${i + 1}`,
email: `user${i + 1}@example.com`,
status: statuses[i % 3]
});
}
return { data, total };
}
// Event handlers
function handleEdit(rowData: User) {
alert(`Editing user: ${rowData.name}`);
}
function handleDelete(rowData: User) {
if (confirm(`Delete user ${rowData.name}?`)) {
fetchData();
}
}
function handleStatusClick(status: string) {
console.log('Status clicked:', status);
}
function handleAvatarClick(name: string) {
alert(`Avatar clicked for: ${name}`);
}
// Column definitions using createSvelteCell for Svelte components
const columns = [
{
id: 'id',
header: 'ID',
accessorKey: 'id',
size: 60
},
{
id: 'user',
header: 'User',
size: 250,
cell: (info: { row: { original: User } }) => {
return createSvelteCell({
component: UserAvatar,
props: {
name: info.row.original.name,
email: info.row.original.email,
onAvatarClick: handleAvatarClick
}
});
}
},
{
id: 'status',
header: 'Status',
cell: (info: { row: { original: User } }) => {
return createSvelteCell({
component: StatusBadge,
props: {
status: info.row.original.status,
onclick: handleStatusClick
}
});
}
},
{
id: 'actions',
header: 'Actions',
cell: (info: { row: { original: User } }) => {
return createSvelteCell({
component: ActionButtons,
props: {
rowData: info.row.original,
onEdit: handleEdit,
onDelete: handleDelete
}
});
}
}
];
// Fetch data function
function fetchData() {
if (!datatable) return;
// Update loading state and clear data
(datatable as any).loading = true;
(datatable as any).data = [];
// Simulate network delay
setTimeout(() => {
const result = generateMockUsers(currentPage + 1, pageSize);
// Update datatable with new data
(datatable as any).data = result.data;
(datatable as any).rowCount = result.total;
(datatable as any).pagination = { pageIndex: currentPage, pageSize };
(datatable as any).loading = false;
}, 500);
}
// Initialize datatable
function initTable() {
const container = document.getElementById('datatable-container');
if (!container) return;
// Create datatable element
datatable = document.createElement('dap-ds-datatable');
// Set static properties
(datatable as any).columns = columns;
(datatable as any).pager = true;
(datatable as any).manualPagination = true;
datatable.setAttribute('loading-type', 'spinner');
// Add event listener for pagination
datatable.addEventListener('dds-pagination-change', ((event: CustomEvent) => {
const newPage = event.detail.pagination.pageIndex;
if (newPage !== currentPage) {
currentPage = newPage;
fetchData();
}
}) as EventListener);
container.appendChild(datatable);
}
// Initialize on mount
onMount(async () => {
// Import design system components
await import('dap-design-system/components');
await import('dap-design-system/icons');
initTable();
fetchData();
});
</script>
<main>
<h1>Datatable with Svelte</h1>
<div id="datatable-container"></div>
</main>createSvelteCell(options)Creates a Svelte component renderer for datatable cells.
Parameters:
options.component(any, required): The Svelte component to renderoptions.props(Object, optional): Props to pass to the componentoptions.on(Object, optional): Event handlers (for Svelte 4 event dispatching)options.context(Map | Object, optional): Context to pass to the component
Shorthand syntax:
// Instead of passing options object:
createSvelteCell({
component: MyComponent,
props: { value: 'test' }
})
// You can use shorthand:
createSvelteCell(MyComponent, { value: 'test' })
Returns: A render callback that the datatable will use to mount the Svelte component.
In Svelte 5, use callback props instead of event dispatching:
<!-- Component -->
<script lang="ts">
interface Props {
value: string;
onClick?: (value: string) => void;
}
let { value, onClick }: Props = $props();
</script>
<button onclick={() => onClick?.(value)}>Click me</button>// Usage
createSvelteCell({
component: MyComponent,
props: {
value: 'test',
onClick: (value) => console.log('clicked', value)
}
})
For Svelte 4 components using createEventDispatcher:
<!-- Component -->
<script>
import { createEventDispatcher } from 'svelte';
const dispatch = createEventDispatcher();
</script>
<button on:click={() => dispatch('click', 'value')}>Click me</button>// Usage
createSvelteCell({
component: MyComponent,
props: { value: 'test' },
on: {
click: (event) => console.log('clicked', event.detail)
}
})
The helper automatically detects which Svelte version you're using:
| Feature | Svelte 4 | Svelte 5 |
|---|---|---|
| Component API | Class-based (new Component()) | Function-based (mount()) |
| Props | export let prop | let { prop } = $props() |
| Events | createEventDispatcher | Callback props |
| Reactivity | $: reactive statements | $derived(), $effect() |
No special configuration is needed for SvelteKit. The design system web components work out of the box.
The createSvelteCell helper automatically manages component lifecycle:
- Mount: Components are created and mounted when cells are rendered
- Unmount: Components are properly destroyed when cells are removed (e.g., pagination, sorting)
- Cleanup: Component instances are unmounted to prevent memory leaks
Each cell with a Svelte component creates its own component instance. For tables with many rows:
- Consider using virtual scrolling for large datasets
- Use
manualPaginationfor server-side pagination (recommended) - Keep cell components lightweight
Make sure you've imported the design system components before using them:
onMount(async () => {
await import('dap-design-system/components');
await import('dap-design-system/icons');
// Now initialize your datatable
});
The Svelte helper includes full TypeScript support:
import { createSvelteCell } from 'dap-design-system/helpers/svelte'
import type { SvelteCellOptions, SvelteCellRenderer } from 'dap-design-system/helpers/svelte'
// Type-safe options
const options: SvelteCellOptions = {
component: MyComponent,
props: {
value: 'test'
}
}
const cellRenderer = createSvelteCell(options)
// Type-safe cell renderer
interface User {
id: number;
status: string;
}
const statusCell: SvelteCellRenderer<User> = (row) => createSvelteCell({
component: StatusBadge,
props: { status: row.status }
})
When using SvelteKit, you can import components from $lib:
import StatusBadge from '$lib/components/StatusBadge.svelte';
Make sure your svelte.config.js has the alias configured (this is default in SvelteKit):
// svelte.config.js
import adapter from '@sveltejs/adapter-auto';
import { vitePreprocess } from '@sveltejs/vite-plugin-svelte';
export default {
preprocess: vitePreprocess(),
kit: {
adapter: adapter(),
alias: {
$lib: './src/lib'
}
}
};