Data table
Animated sorting and row selection.
Customize
import {Scene, Video} from "odori";
import {DataTable} from "../components/data-table/data-table";
<Video>
<Scene id="data-table" duration="5s">
<DataTable
columns={[
"Video",
"Format",
"Duration",
"State"
]}
rows={[
[
"launch",
"1920x1080",
"12s",
"queued"
],
[
"launch",
"1080x1920",
"12s",
"rendering"
],
[
"announcement",
"1080x1080",
"8s",
"done"
]
]}
selectedIndex={1}
title="Export jobs"
/>
</Scene>
</Video>Installation
pnpm odori add component data-tablenpx odori add component data-tablebunx odori add component data-tableEdit the source in videos/components/data-table/. Use odori diff to compare upstream changes and odori update to apply updates.
Copy these files into your project
videos/components/data-table/data-table.tsx
import {Fill, Easing, interpolate, useBrand, useFrame, useDesignScale} from "odori";
export type DataTableProps = {
columns: string[];
rows: string[][];
/** Row that ends up selected, zero based. */
selectedIndex?: number;
title?: string;
};
/**
* Rows arriving in order, ending on one authored selection. The selected row
* fills rather than recolors its text, so the story survives a brand where
* accent and foreground sit close together.
*/
export const DataTable = ({columns, rows, selectedIndex, title}: DataTableProps) => {
const frame = useFrame();
const brand = useBrand();
const scale = useDesignScale();
const selectAt = 16 + rows.length * 7;
return (
<Fill style={{alignItems: "center", justifyContent: "center", padding: 90 * scale}}>
<div
style={{
background: brand.colors.surface,
border: `1px solid ${brand.colors.border}`,
borderRadius: 26 * scale,
minWidth: 1200 * scale,
overflow: "hidden",
}}
>
{title ? (
<div
style={{
borderBottom: `1px solid ${brand.colors.border}`,
fontSize: 40 * scale,
fontWeight: 560,
padding: `${24 * scale}px ${30 * scale}px`,
}}
>
{title}
</div>
) : null}
<div
style={{
color: brand.colors.muted,
display: "grid",
fontSize: 26 * scale,
gap: 20 * scale,
gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
padding: `${20 * scale}px ${30 * scale}px`,
}}
>
{columns.map((column) => (
<span key={column}>{column}</span>
))}
</div>
{rows.map((row, index) => {
const delay = 14 + index * 7;
const enter = interpolate(frame, [delay, delay + 16], [0, 1], {easing: Easing.standard});
const selected = index === selectedIndex;
const fill = selected ? interpolate(frame, [selectAt, selectAt + 16], [0, 1], {easing: Easing.standard}) : 0;
return (
<div
key={index}
style={{
background: `color-mix(in srgb, ${brand.colors.accent} ${fill * 14}%, transparent)`,
borderTop: `1px solid ${brand.colors.border}`,
display: "grid",
fontSize: 30 * scale,
gap: 20 * scale,
gridTemplateColumns: `repeat(${columns.length}, 1fr)`,
opacity: enter,
padding: `${20 * scale}px ${30 * scale}px`,
transform: `translateY(${(1 - enter) * 10 * scale}px)`,
}}
>
{row.map((cell, cellIndex) => (
<span key={cellIndex} style={{fontFamily: cellIndex === 0 ? brand.typography.sans : brand.typography.mono}}>
{cell}
</span>
))}
</div>
);
})}
</div>
</Fill>
);
};
videos/components/data-table/data-table.preview.tsx
import {defineComponentPreview} from "odori/preview";
import {DataTable} from "./data-table";
export default defineComponentPreview({
title: "Data table",
category: "Data",
description: "Animated sorting and row selection.",
component: DataTable,
canvas: {width: 1920, height: 1080, duration: "5s"},
controls: {
title: {type: "text", defaultValue: "Export jobs"},
selectedIndex: {type: "number", defaultValue: 1, min: 0, max: 5},
},
examples: [
{
name: "Jobs",
props: {
columns: ["Video", "Format", "Duration", "State"],
rows: [
["launch", "1920x1080", "12s", "queued"],
["launch", "1080x1920", "12s", "rendering"],
["announcement", "1080x1080", "8s", "done"],
],
selectedIndex: 1,
},
},
],
});
Add it to a scene
import {Scene, Video} from "odori";
import {DataTable} from "../components/data-table/data-table";
<Video>
<Scene id="data-table" duration="5s">
<DataTable
columns={[
"Video",
"Format",
"Duration",
"State"
]}
rows={[
[
"launch",
"1920x1080",
"12s",
"queued"
],
[
"launch",
"1080x1920",
"12s",
"rendering"
],
[
"announcement",
"1080x1080",
"8s",
"done"
]
]}
selectedIndex={1}
title="Export jobs"
/>
</Scene>
</Video>Timing contract
Use these durations and content limits when composing a scene. Run odori test to check your video.
- Family
- Data
- Recommended duration
- 150 frames · 5s
- Minimum duration
- 75 frames · 2.5s
- Entrance
- 60 frames
- Exit
- 0 frames
- Reduced motion
- rows and the selection appear at once
- Requires
- sans font, mono font
- Content limits
- rows ≤ 6, columns ≤ 5
Props
| Prop | Type | Default | Required |
|---|---|---|---|
columns | string[] | — | Yes |
rows | string[][] | — | Yes |
selectedIndexRow that ends up selected, zero based. | number | — | — |
title | string | — | — |
Related components
All componentsArea chartFilled trend reveal for volume and cumulative change.Bar chartRanked bars with labels, values, and staged growth.Donut chartPart-to-whole display with a stable center metric.HeatmapGrid intensity reveal for activity and distribution.Line chartTime-series path draw with optional focal annotation.Progress ringCircular completion display with restrained motion.