| Title | Brand | Release |
|---|---|---|
| ATRI -My Dear Moments- | ANIPLEX.EXE | 2020-06-19 |
| Summer Pockets | Key | 2018-06-29 |
| サクラノ詩 | 枕 | 2015-10-23 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const games = [
{ title: 'ATRI -My Dear Moments-', brand: 'ANIPLEX.EXE', date: '2020-06-19' },
{ title: 'Summer Pockets', brand: 'Key', date: '2018-06-29' },
{ title: 'サクラノ詩', brand: '枕', date: '2015-10-23' },
]
</script>
<template>
<Table caption="Selected titles" class="w-full max-w-xl">
<TableHeader>
<TableRow>
<TableHead>Title</TableHead>
<TableHead>Brand</TableHead>
<TableHead align="end">Release</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="game in games" :key="game.title">
<TableCell>{{ game.title }}</TableCell>
<TableCell>{{ game.brand }}</TableCell>
<TableCell align="end">{{ game.date }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Usage
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
A table is built from six components that mirror the structure of an HTML table: Table is the root, TableHeader and TableBody are the header and data sections, TableRow is a row, TableHead is a header cell and TableCell is a data cell. Every component except Table, TableHead and TableCell takes only class.
Table brings its own horizontal scroll container, so a wide table scrolls within the container instead of stretching the surrounding layout.
| Volume | Pages | Release |
|---|---|---|
| Volume 1 | 328 | 2021-03-10 |
| Volume 2 | 304 | 2021-09-10 |
| Volume 3 | 352 | 2022-02-10 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
</script>
<template>
<Table class="w-full max-w-lg">
<TableHeader>
<TableRow>
<TableHead>Volume</TableHead>
<TableHead>Pages</TableHead>
<TableHead>Release</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Volume 1</TableCell>
<TableCell>328</TableCell>
<TableCell>2021-03-10</TableCell>
</TableRow>
<TableRow>
<TableCell>Volume 2</TableCell>
<TableCell>304</TableCell>
<TableCell>2021-09-10</TableCell>
</TableRow>
<TableRow>
<TableCell>Volume 3</TableCell>
<TableCell>352</TableCell>
<TableCell>2022-02-10</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
This component handles presentation. Use DataTable for column definitions, sorting, filtering, pagination and row selection.
Examples
Variants
primary is a panel with its own background, border and shadow, suited to a table that stands on its own. secondary drops the border and shadow and keeps only the header background, suited to a table already inside a card or panel that needs no second container. The default is primary.
primary
| Character | Voice |
|---|---|
| 夏目 佳月 | 藤咲ウサ |
| 斑鳩 千鹤 | 御苑生メイ |
secondary
| Character | Voice |
|---|---|
| 夏目 佳月 | 藤咲ウサ |
| 斑鳩 千鹤 | 御苑生メイ |
<script setup lang="ts">
import {
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Text,
} from '@hina-ui/vue'
const rows = [
{ name: '夏目 佳月', cv: '藤咲ウサ' },
{ name: '斑鳩 千鹤', cv: '御苑生メイ' },
]
</script>
<template>
<Stack gap="lg" class="w-full max-w-lg">
<Stack gap="sm">
<Text size="sm" tone="faint">primary</Text>
<Table variant="primary">
<TableHeader>
<TableRow>
<TableHead>Character</TableHead>
<TableHead>Voice</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.name">
<TableCell>{{ row.name }}</TableCell>
<TableCell>{{ row.cv }}</TableCell>
</TableRow>
</TableBody>
</Table>
</Stack>
<Stack gap="sm">
<Text size="sm" tone="faint">secondary</Text>
<Table variant="secondary">
<TableHeader>
<TableRow>
<TableHead>Character</TableHead>
<TableHead>Voice</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.name">
<TableCell>{{ row.name }}</TableCell>
<TableCell>{{ row.cv }}</TableCell>
</TableRow>
</TableBody>
</Table>
</Stack>
</Stack>
</template>
Alignment
align sets how cell content is aligned: start, center or end, defaulting to start. Numeric columns usually read better aligned to the end, and a header should be aligned the same way as the column below it.
Tabular figures are enabled, so digits in the same column share one width and never sit ragged.
| Volume | Chapters | Rating |
|---|---|---|
| Volume 1 | 12 | 8.6 |
| Volume 2 | 9 | 9.1 |
| Volume 3 | 14 | 8.8 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const volumes = [
{ name: 'Volume 1', chapters: 12, rating: '8.6' },
{ name: 'Volume 2', chapters: 9, rating: '9.1' },
{ name: 'Volume 3', chapters: 14, rating: '8.8' },
]
</script>
<template>
<Table class="w-full max-w-lg">
<TableHeader>
<TableRow>
<TableHead>Volume</TableHead>
<TableHead align="center">Chapters</TableHead>
<TableHead align="end">Rating</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="volume in volumes" :key="volume.name">
<TableCell>{{ volume.name }}</TableCell>
<TableCell align="center">{{ volume.chapters }}</TableCell>
<TableCell align="end">{{ volume.rating }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Caption
caption renders a line of text above the table and serves as the table’s accessible name. It stays visible at the start edge while the table scrolls horizontally.
| Volume | Release |
|---|---|
| Volume 1 | 2021-03-10 |
| Volume 2 | 2021-09-10 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
</script>
<template>
<Table caption="Tankobon release schedule" class="w-full max-w-lg">
<TableHeader>
<TableRow>
<TableHead>Volume</TableHead>
<TableHead>Release</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow>
<TableCell>Volume 1</TableCell>
<TableCell>2021-03-10</TableCell>
</TableRow>
<TableRow>
<TableCell>Volume 2</TableCell>
<TableCell>2021-09-10</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Row hover
Data rows raise a faint state layer on hover, which helps the eye stay on one row across a wide table. Set hover to false when the table lists fixed information and its rows are not interactive.
| Item | Value |
|---|---|
| Format | Download / Physical |
| Length | About 20 hours |
| Language | Japanese / Simplified Chinese |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const specs = [
{ key: 'Format', value: 'Download / Physical' },
{ key: 'Length', value: 'About 20 hours' },
{ key: 'Language', value: 'Japanese / Simplified Chinese' },
]
</script>
<template>
<Table :hover="false" class="w-full max-w-lg">
<TableHeader>
<TableRow>
<TableHead>Item</TableHead>
<TableHead>Value</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="spec in specs" :key="spec.key">
<TableCell>{{ spec.key }}</TableCell>
<TableCell>{{ spec.value }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Row headers
When the first cell of a row names the row rather than holding data, use TableHead with scope="row" instead of TableCell. Screen readers then treat that cell as the heading for the whole row and announce it before the cells that follow.
| Platform | Release | Price |
|---|---|---|
| Windows | 2020-06-19 | 3,278 円 |
| Nintendo Switch | 2022-03-24 | 4,180 円 |
| PlayStation 4 | 2022-03-24 | 4,180 円 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const rows = [
{ platform: 'Windows', release: '2020-06-19', price: '3,278 円' },
{ platform: 'Nintendo Switch', release: '2022-03-24', price: '4,180 円' },
{ platform: 'PlayStation 4', release: '2022-03-24', price: '4,180 円' },
]
</script>
<template>
<Table class="w-full max-w-xl">
<TableHeader>
<TableRow>
<TableHead>Platform</TableHead>
<TableHead>Release</TableHead>
<TableHead align="end">Price</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.platform">
<TableHead scope="row">{{ row.platform }}</TableHead>
<TableCell>{{ row.release }}</TableCell>
<TableCell align="end">{{ row.price }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Sticky header
sticky-header pins the header row to the top of the container while the body scrolls. It needs a height limit to work, otherwise the table never scrolls vertically. With it on, the scroll container handles both axes.
| No. | Title | Updated |
|---|---|---|
| 1 | Episode 1 | 2024-01-15 |
| 2 | Episode 2 | 2024-01-15 |
| 3 | Episode 3 | 2024-01-15 |
| 4 | Episode 4 | 2024-01-15 |
| 5 | Episode 5 | 2024-02-15 |
| 6 | Episode 6 | 2024-02-15 |
| 7 | Episode 7 | 2024-02-15 |
| 8 | Episode 8 | 2024-02-15 |
| 9 | Episode 9 | 2024-03-15 |
| 10 | Episode 10 | 2024-03-15 |
| 11 | Episode 11 | 2024-03-15 |
| 12 | Episode 12 | 2024-03-15 |
| 13 | Episode 13 | 2024-04-15 |
| 14 | Episode 14 | 2024-04-15 |
| 15 | Episode 15 | 2024-04-15 |
| 16 | Episode 16 | 2024-04-15 |
| 17 | Episode 17 | 2024-05-15 |
| 18 | Episode 18 | 2024-05-15 |
| 19 | Episode 19 | 2024-05-15 |
| 20 | Episode 20 | 2024-05-15 |
| 21 | Episode 21 | 2024-06-15 |
| 22 | Episode 22 | 2024-06-15 |
| 23 | Episode 23 | 2024-06-15 |
| 24 | Episode 24 | 2024-06-15 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const episodes = Array.from({ length: 24 }, (_, index) => ({
no: index + 1,
title: `Episode ${index + 1}`,
date: `2024-${String(Math.floor(index / 4) + 1).padStart(2, '0')}-15`,
}))
</script>
<template>
<Table sticky-header class="max-h-72 w-full max-w-lg">
<TableHeader>
<TableRow>
<TableHead>No.</TableHead>
<TableHead>Title</TableHead>
<TableHead align="end">Updated</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="episode in episodes" :key="episode.no">
<TableCell>{{ episode.no }}</TableCell>
<TableCell>{{ episode.title }}</TableCell>
<TableCell align="end">{{ episode.date }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Sticky column
With many columns, add sticky to the TableHead and TableCell of the first column: it stays pinned to the start edge while the remaining columns slide underneath. A sticky column carries its own solid background and a dividing border, so nothing shows through it.
Both the header cell and the data cells of that column need sticky — setting it on only one of them makes the column drift out of line while scrolling.
| Title | Brand | Release | Platform | Language | Length | Rating |
|---|---|---|---|---|---|---|
| ATRI -My Dear Moments- | ANIPLEX.EXE | 2020-06-19 | Windows | Japanese / Simplified Chinese | About 20 hours | 8.9 |
| Summer Pockets | Key | 2018-06-29 | Windows | Japanese | About 40 hours | 8.5 |
| サクラノ詩 | 枕 | 2015-10-23 | Windows | Japanese | About 50 hours | 9.0 |
<script setup lang="ts">
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
const works = [
{
title: 'ATRI -My Dear Moments-',
brand: 'ANIPLEX.EXE',
date: '2020-06-19',
platform: 'Windows',
language: 'Japanese / Simplified Chinese',
length: 'About 20 hours',
rating: '8.9',
},
{
title: 'Summer Pockets',
brand: 'Key',
date: '2018-06-29',
platform: 'Windows',
language: 'Japanese',
length: 'About 40 hours',
rating: '8.5',
},
{
title: 'サクラノ詩',
brand: '枕',
date: '2015-10-23',
platform: 'Windows',
language: 'Japanese',
length: 'About 50 hours',
rating: '9.0',
},
]
</script>
<template>
<Table class="w-full max-w-lg [&_td]:whitespace-nowrap [&_th]:whitespace-nowrap">
<TableHeader>
<TableRow>
<TableHead sticky>Title</TableHead>
<TableHead>Brand</TableHead>
<TableHead>Release</TableHead>
<TableHead>Platform</TableHead>
<TableHead>Language</TableHead>
<TableHead>Length</TableHead>
<TableHead align="end">Rating</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="work in works" :key="work.title">
<TableCell sticky>{{ work.title }}</TableCell>
<TableCell>{{ work.brand }}</TableCell>
<TableCell>{{ work.date }}</TableCell>
<TableCell>{{ work.platform }}</TableCell>
<TableCell>{{ work.language }}</TableCell>
<TableCell>{{ work.length }}</TableCell>
<TableCell align="end">{{ work.rating }}</TableCell>
</TableRow>
</TableBody>
</Table>
</template>
Density
Row height comes from the density tokens, so setting data-density="compact" on a container tightens the whole table without touching the table itself. Use it on information-dense list pages.
comfortable (default)
| Volume | Release |
|---|---|
| Volume 1 | 2021-03-10 |
| Volume 2 | 2021-09-10 |
| Volume 3 | 2022-02-10 |
compact
| Volume | Release |
|---|---|
| Volume 1 | 2021-03-10 |
| Volume 2 | 2021-09-10 |
| Volume 3 | 2022-02-10 |
<script setup lang="ts">
import {
Stack,
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
Text,
} from '@hina-ui/vue'
const rows = [
{ name: 'Volume 1', date: '2021-03-10' },
{ name: 'Volume 2', date: '2021-09-10' },
{ name: 'Volume 3', date: '2022-02-10' },
]
</script>
<template>
<Stack gap="lg" class="w-full max-w-lg">
<Stack gap="sm">
<Text size="sm" tone="faint">comfortable (default)</Text>
<Table>
<TableHeader>
<TableRow>
<TableHead>Volume</TableHead>
<TableHead align="end">Release</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.name">
<TableCell>{{ row.name }}</TableCell>
<TableCell align="end">{{ row.date }}</TableCell>
</TableRow>
</TableBody>
</Table>
</Stack>
<Stack gap="sm" data-density="compact">
<Text size="sm" tone="faint">compact</Text>
<Table>
<TableHeader>
<TableRow>
<TableHead>Volume</TableHead>
<TableHead align="end">Release</TableHead>
</TableRow>
</TableHeader>
<TableBody>
<TableRow v-for="row in rows" :key="row.name">
<TableCell>{{ row.name }}</TableCell>
<TableCell align="end">{{ row.date }}</TableCell>
</TableRow>
</TableBody>
</Table>
</Stack>
</Stack>
</template>
Accessibility
- The structure uses native table elements, so screen readers can navigate by row and column and announce the current position.
scopeonTableHeaddefaults tocol, declaring the cell as the header of its column; a row header must set it torowexplicitly.captionis the table’s accessible name. When a heading already precedes the table, repeat it here so assistive technology does not meet an unnamed table.- Neither alignment nor row hover changes reading order.
API
Table
| Prop | Type | Default | Description |
|---|---|---|---|
variant | 'primary' | 'secondary' | 'primary' | Visual style |
hover | boolean | true | Whether data rows raise a state layer |
stickyHeader | boolean | false | Whether the header pins to the top edge |
caption | string | — | Table caption, also its accessible name |
class | string | — | Classes appended to the scroll container |
| Slot | Description |
|---|---|
default | Table content, usually TableHeader and TableBody |
TableHead
| Prop | Type | Default | Description |
|---|---|---|---|
scope | 'col' | 'row' | 'colgroup' | 'rowgroup' | 'col' | Whether the cell heads a column or a row |
align | 'start' | 'center' | 'end' | 'start' | Content alignment |
sticky | boolean | false | Whether it pins to the start edge |
class | string | — | Classes appended to the root |
TableCell
| Prop | Type | Default | Description |
|---|---|---|---|
align | 'start' | 'center' | 'end' | 'start' | Content alignment |
sticky | boolean | false | Whether it pins to the start edge |
class | string | — | Classes appended to the root |
TableHeader, TableBody and TableRow
These render thead, tbody and tr respectively. Each takes only class and receives its content through the default slot.