Web Components
Framework-agnostic Custom Elements for SQL-powered dashboards
Web Components
Duck-UI Web Components (@duck_ui/elements) let you build SQL-powered dashboards in any framework -- or no framework at all. They wrap @duck_ui/core as standard Custom Elements.
Installation
With a bundler
bun add @duck_ui/elementsimport '@duck_ui/elements'All elements are registered on import. No further setup needed.
CDN (no bundler)
<script src="https://unpkg.com/@duck_ui/cdn/dist/duck-ui.min.js"></script>Minimal Example
<duck-provider src="/data/orders.json" table="orders">
<duck-table sql="SELECT * FROM orders" page-size="25" sortable></duck-table>
</duck-provider><duck-provider> boots DuckDB-WASM, fetches the data file, loads it into a table, and <duck-table> renders it with pagination and sorting.
Full Dashboard
<duck-provider src="/data/orders.json" table="orders" theme="dark">
<duck-dashboard columns="3" gap="16">
<duck-panel span="3">
<duck-filter-bar auto source="orders"></duck-filter-bar>
</duck-panel>
<duck-panel>
<duck-kpi
sql="SELECT SUM(total) AS value FROM orders"
label="Revenue"
format="currency"
></duck-kpi>
</duck-panel>
<duck-panel>
<duck-kpi
sql="SELECT COUNT(*) AS value FROM orders"
label="Orders"
format="number"
></duck-kpi>
</duck-panel>
<duck-panel>
<duck-kpi
sql="SELECT AVG(total) AS value FROM orders"
label="Avg Order"
format="currency"
></duck-kpi>
</duck-panel>
<duck-panel span="2">
<duck-chart
sql="SELECT product, SUM(total) AS rev FROM orders GROUP BY 1"
type="bar"
height="300"
title="Revenue by Product"
></duck-chart>
</duck-panel>
<duck-panel>
<duck-chart
sql="SELECT status, COUNT(*) AS n FROM orders GROUP BY 1"
type="bar"
height="300"
title="By Status"
></duck-chart>
</duck-panel>
<duck-panel span="3">
<duck-table sql="SELECT * FROM orders" page-size="20" sortable striped></duck-table>
</duck-panel>
<duck-panel>
<duck-export sql="SELECT * FROM orders" format="csv" file-name="orders" label="Export CSV"></duck-export>
</duck-panel>
</duck-dashboard>
</duck-provider>Elements Reference
All attributes use kebab-case. Boolean attributes are toggled by presence (e.g., sortable with no value means true).
<duck-provider>
Root element. Required as an ancestor of all other duck elements.
| Attribute | Description |
|---|---|
src | URL to a data file (CSV, JSON, Parquet) |
table | DuckDB table name for the loaded data |
format | File format (csv, json, parquet). Auto-detected from URL extension if omitted. |
theme | light or dark |
<duck-chart>
| Attribute | Description |
|---|---|
sql | SQL query (required) |
type | line, bar, area, or scatter (default: line) |
height | Height in pixels (default: 300) |
width | Width in pixels (default: fills container) |
title | Chart title |
<duck-table>
| Attribute | Description |
|---|---|
sql | SQL query (required) |
page-size | Rows per page (default: 25) |
sortable | Enable column sorting |
striped | Alternate row colors |
max-height | Max scroll container height (e.g., 400px) |
<duck-kpi>
| Attribute | Description |
|---|---|
sql | Query returning a single value (required) |
label | KPI label (required) |
format | currency, percent, number, or compact |
currency | Currency code (default: USD) |
compare-sql | SQL for comparison value (shows % change) |
sparkline-sql | SQL for inline trend line |
<duck-dashboard> + <duck-panel>
Grid layout. <duck-dashboard> is the container, <duck-panel> wraps each item.
| Element | Attribute | Description |
|---|---|---|
duck-dashboard | columns | Grid columns (default: 2) |
duck-dashboard | gap | Gap in px (default: 16) |
duck-dashboard | padding | Padding in px (default: 24) |
duck-panel | span | Column span (default: 1) |
duck-panel | row-span | Row span (default: 1) |
<duck-filter-bar>
| Attribute | Description |
|---|---|
auto | Auto-detect filters from table schema |
source | Table name for distinct values |
Individual Filters
| Element | Attributes |
|---|---|
<duck-select-filter> | column (required), source, label |
<duck-range-filter> | column (required), min (required), max (required), step, label |
<duck-date-filter> | column (required), label |
<duck-filter-bar>
<duck-select-filter column="status" source="orders" label="Status"></duck-select-filter>
<duck-range-filter column="total" min="0" max="1000" step="10" label="Total"></duck-range-filter>
<duck-date-filter column="created_at" label="Date"></duck-date-filter>
</duck-filter-bar><duck-export>
| Attribute | Description |
|---|---|
sql | SQL query to export (required) |
format | csv or json (default: csv) |
file-name | File name without extension (default: export) |
label | Button label (default: Export) |
Events
Events are standard CustomEvent instances dispatched on the <duck-provider> element.
| Event | detail | When |
|---|---|---|
duck-ready | {} | Engine initialized and data loaded |
duck-error | { error: Error } | Initialization or query error |
duck-filter-change | { filters: Record<string, FilterValue> } | A filter value changed |
document.querySelector('duck-provider').addEventListener('duck-ready', () => {
console.log('Dashboard is ready')
})
document.querySelector('duck-provider').addEventListener('duck-filter-change', (e) => {
console.log('Filters:', e.detail.filters)
})
document.querySelector('duck-provider').addEventListener('duck-error', (e) => {
console.error('Error:', e.detail.error)
})Theming with CSS Custom Properties
Style all Duck-UI elements using CSS custom properties. Set them on <duck-provider> or any ancestor element.
duck-provider {
--duck-bg: #ffffff;
--duck-text: #374151;
--duck-primary: #2563eb;
--duck-surface: #f9fafb;
--duck-border: #e5e7eb;
--duck-grid: #e5e7eb;
--duck-hover: #f3f4f6;
--duck-muted: #9ca3af;
--duck-error: #ef4444;
--duck-success: #22c55e;
}Dark theme example:
duck-provider[theme="dark"] {
--duck-bg: #111827;
--duck-text: #e5e7eb;
--duck-primary: #60a5fa;
--duck-surface: #1f2937;
--duck-border: #374151;
--duck-grid: #374151;
--duck-hover: #1f2937;
--duck-muted: #6b7280;
}Or apply a fully custom theme:
.corporate-dashboard duck-provider {
--duck-bg: #fafafa;
--duck-text: #1a1a1a;
--duck-primary: #0066cc;
--duck-surface: #ffffff;
--duck-border: #e0e0e0;
}Framework Integration
Web Components work natively in HTML. Most frameworks also handle them well.
Vue
<template>
<duck-provider src="/data/orders.json" table="orders">
<duck-table sql="SELECT * FROM orders" sortable></duck-table>
</duck-provider>
</template>
<script setup>
import '@duck_ui/elements'
</script>Svelte
<script>
import '@duck_ui/elements'
</script>
<duck-provider src="/data/orders.json" table="orders">
<duck-chart sql="SELECT product, SUM(total) AS rev FROM orders GROUP BY 1" type="bar" height="300"></duck-chart>
</duck-provider>Angular
Add CUSTOM_ELEMENTS_SCHEMA to your module or standalone component:
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import '@duck_ui/elements'
@Component({
selector: 'app-dashboard',
schemas: [CUSTOM_ELEMENTS_SCHEMA],
template: `
<duck-provider src="/data/orders.json" table="orders">
<duck-table sql="SELECT * FROM orders" sortable></duck-table>
</duck-provider>
`,
})
export class DashboardComponent {}Astro
---
---
<script>
import '@duck_ui/elements'
</script>
<duck-provider src="/data/orders.json" table="orders">
<duck-table sql="SELECT * FROM orders" page-size="25" sortable></duck-table>
</duck-provider>Parquet Files
Point <duck-provider> at a Parquet file for efficient range-request loading. DuckDB only downloads the row groups it needs.
<duck-provider src="https://my-cdn.com/data/sales.parquet" table="sales" format="parquet">
<duck-chart sql="SELECT month, SUM(revenue) AS rev FROM sales GROUP BY 1 ORDER BY 1" type="line" height="300"></duck-chart>
</duck-provider>Next Steps
- Core API -- use the DuckUI class directly for maximum control
- API Reference -- full attribute and event reference
- Theming -- detailed theme customization
- Filters -- deep dive into the filter system