Table

Static data organised into rows and columns.

Selected titles
TitleBrandRelease
ATRI -My Dear Moments-ANIPLEX.EXE2020-06-19
Summer PocketsKey2018-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>
vue

Usage

import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@hina-ui/vue'
ts

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.

VolumePagesRelease
Volume 13282021-03-10
Volume 23042021-09-10
Volume 33522022-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>
vue

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

CharacterVoice
夏目 佳月藤咲ウサ
斑鳩 千鹤御苑生メイ

secondary

CharacterVoice
夏目 佳月藤咲ウサ
斑鳩 千鹤御苑生メイ
<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>
vue

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.

VolumeChaptersRating
Volume 1128.6
Volume 299.1
Volume 3148.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>
vue

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.

Tankobon release schedule
VolumeRelease
Volume 12021-03-10
Volume 22021-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>
vue

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.

ItemValue
FormatDownload / Physical
LengthAbout 20 hours
LanguageJapanese / 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>
vue

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.

PlatformReleasePrice
Windows2020-06-193,278 円
Nintendo Switch2022-03-244,180 円
PlayStation 42022-03-244,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>
vue

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.

TitleBrandReleasePlatformLanguageLengthRating
ATRI -My Dear Moments-ANIPLEX.EXE2020-06-19WindowsJapanese / Simplified ChineseAbout 20 hours8.9
Summer PocketsKey2018-06-29WindowsJapaneseAbout 40 hours8.5
サクラノ詩枕2015-10-23WindowsJapaneseAbout 50 hours9.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>
vue

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)

VolumeRelease
Volume 12021-03-10
Volume 22021-09-10
Volume 32022-02-10

compact

VolumeRelease
Volume 12021-03-10
Volume 22021-09-10
Volume 32022-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>
vue

Accessibility

  • The structure uses native table elements, so screen readers can navigate by row and column and announce the current position.
  • scope on TableHead defaults to col, declaring the cell as the header of its column; a row header must set it to row explicitly.
  • caption is 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

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.