Tables

For displaying tabular data, BTable supports pagination, filtering, sorting, custom rendering, various style options, events, and asynchronous data. For simple display of tabular data without all the fancy features, BootstrapVueNext provides two lightweight alternative components BTableLite and BTableSimple.

Basic Usage

AgeFirst nameLast name
40DickersonMacdonald
21LarsenShaw
89GenevaWilson
38JamiCarney
HTML
vue
<template>
  <div>
    <BTable striped hover :items="items" />
  </div>
</template>

<script setup lang="ts">
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {age: 38, first_name: 'Jami', last_name: 'Carney'},
]
</script>

Items (record data)

items is the table data in array format, where each record (row) data are keyed objects. Example format:

ts
const items = [
  {age: 32, first_name: 'Cyndi'},
  {age: 27, first_name: 'Havij'},
  {age: 42, first_name: 'Robert'},
]

<BTable> automatically samples the first row to extract field names (the keys in the record data). Field names are automatically "humanized" by converting kebab-case, snake_case, and camelCase to individual words and capitalizes each word. Example conversions:

  • first_name becomes First Name
  • last-name becomes Last Name
  • age becomes Age
  • YEAR remains YEAR
  • isActive becomes Is Active

These titles will be displayed in the table header, in the order they appear in the first record of data. See the Fields section below for customizing how field headings appear.

NOTE

Field order is not guaranteed. Fields will typically appear in the order they were defined in the first row, but this may not always be the case depending on the version of browser in use. See section Fields (column definitions) below to see how to guarantee the order of fields, and to override the headings generated.

Record data may also have additional special reserved name keys for colorizing rows and individual cells (variants), and for triggering additional row detail. The type TableItem defines the supported optional item record modifier properties (make sure your field keys do not conflict with these names):

PropertyTypeDescription
_cellVariantsPartial<Record<keyof T, ColorVariant>>Bootstrap contextual state applied to individual cells. Keyed by field (See the Color Variants for supported values). These variants map to classes table-${variant} or bg-${variant} (when the dark prop is set).
_rowVariantColorVariantBootstrap contextual state applied to the entire row (See the Color Variants for supported values). These variants map to classes table-${variant} or bg-${variant} (when the dark prop is set)
_showDetailsbooleanUsed to trigger the display of the row-details scoped slot. See section Row details support below for additional information

Example: Using variants for table cells

AgeFirst nameLast name
40DickersonMacdonald
21LarsenShaw
89GenevaWilson
40ThorMacDonald
29DickDunlap
HTML
vue
<template>
  <div>
    <BTable hover :items="items" />
  </div>
</template>

<script setup lang="ts">
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {
    age: 89,
    first_name: 'Geneva',
    last_name: 'Wilson',
    _rowVariant: 'danger',
  },
  {
    age: 40,
    first_name: 'Thor',
    last_name: 'MacDonald',
    _cellVariants: {age: 'info', first_name: 'warning'},
  },
  {age: 29, first_name: 'Dick', last_name: 'Dunlap'},
]
</script>

items can also be a reference to a provider function, which returns an Array of items data. Provider functions can also be asynchronous:

  • By returning null (or undefined) and calling a callback, when the data is ready, with the data array as the only argument to the callback,
  • By returning a Promise that resolves to an array.

See the "Using Items Provider functions" section below for more details.

Table item notes and warnings

  • Avoid manipulating record data in place, as changes to the underlying items data will cause either the row or entire table to be re-rendered. See Primary Key, below, for ways to minimize Vue's re-rendering of rows.
  • items array records should be a simple object and must avoid placing data that may have circular references in the values within a row. <BTable> serializes the row data into strings for sorting and filtering, and circular references will cause stack overflows to occur and your app to crash!

Fields (column definitions)

The fields prop is used to customize the table columns headings, and in which order the columns of data are displayed. The field object keys (i.e. age or first_name as shown below) are used to extract the value from each item (record) row, and to provide additional features such as enabling sorting on the column, etc.

Fields can be provided as a simple array or an array of objects. Internally the fields data will be normalized into the array of objects format. Events or slots that include the column field data will be in the normalized field object format (array of objects for fields, or an object for an individual field).

Fields as a simple array

Fields can be a simple array, for defining the order of the columns, and which columns to display:

First nameLast nameAge
DickersonMacdonald40
LarsenShaw21
GenevaWilson89
JamiCarney38
HTML
vue
<template>
  <div>
    <BTable striped hover :items="items" :fields="fields" />
  </div>
</template>

<script setup lang="ts">
// Note `isActive` is left out and will not appear in the rendered table
const fields = ['first_name', 'last_name', 'age']
const items = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]
</script>

Fields as an array of objects

Fields can be a an array of objects, providing additional control over the fields (such as sorting, formatting, etc.). Only columns (keys) that appear in the fields array will be shown:

Last NameFirst NamePerson age
MacdonaldDickerson40
ShawLarsen21
WilsonGeneva89
CarneyJami38
HTML
vue
<template>
  <div>
    <BTable striped hover :items="items" :fields="fields" />
  </div>
</template>

<script setup lang="ts">
import type {TableFieldRaw} from 'bootstrap-vue-next'

interface Person {
  first_name: string
  last_name: string
  age: number
  isActive: boolean
}

// Note 'isActive' is left out and will not appear in the rendered table
const fields: TableFieldRaw<Person>[] = [
  {
    key: 'last_name',
    sortable: true,
  },
  {
    key: 'first_name',
    sortable: false,
  },
  {
    key: 'age',
    label: 'Person age',
    sortable: true,
    // Variant applies to the whole column, including the header and footer
    variant: 'danger',
  },
]

const items: Person[] = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]
</script>

Field Definition Reference

The following field properties (defined as TableField) are recognized:

PropertyTypeDescription
keyLiteralUnion<keyof T>The key for selecting data from the record in the items array. Required when setting the fields via an array of objects. The key is also used for generating the custom data rendering and custom header and footer slot names.
labelstringAppears in the columns table header (and footer if foot-clone is set). Defaults to the field's key (in humanized format) if not provided. It's possible to use empty labels by assigning an empty string "" but be sure you also set headerTitle to provide non-sighted users a hint about the column contents.
headerTitlestringText to place on the fields header <th> attribute title. Defaults to no title attribute.
headerAbbrstringText to place on the fields header <th> attribute abbr. Set this to the unabbreviated version of the label (or title) if label (or title) is an abbreviation. Defaults to no abbr attribute.
classClassValueClass name (or array of class names) to add to <th> and <td> in the column.
formatterTableFieldFormatter<T>A formatter callback function can be used instead of (or in conjunction with) scoped field slots. The formatter will be called with the syntax formatter<T>(value: unknown, key: string, item: T). Refer to Custom Data Rendering for more details.
sortablebooleanEnable sorting on this column. Refer to the Sorting Section for more details.
sortDirectionstringSet the initial sort direction on this column when it becomes sorted. Refer to the Change initial sort direction Section for more details.Not yet implemented
sortByFormattedboolean | TableFieldFormatter<T>Sort the column by the result of the field's formatter callback function when set to true. Default is false. Boolean has no effect if the field does not have a formatter. Optionally accepts a formatter function reference to format the value for sorting purposes only. Refer to the Sorting Section for more details.
filterByFormattedboolean | TableFieldFormatter<T>Filter the column by the result of the field's formatter callback function when set to true. Default is false. Boolean has no effect if the field does not have a formatter. Optionally accepts a formatter function reference to format the value for filtering purposes only. Refer to the Filtering section for more details.
tdClassTableStrictClassValue | ((value: unknown, key: string, item: T) => TableStrictClassValue)Class name (or array of class names) to add to <tbody> data <td> cells in the column. If custom classes per cell are required, a callback function can be specified instead. See the typescript definition for accepted parameters and return types.
thClassClassValueClass name (or array of class names) to add to this field's <thead>/<tfoot> heading <th> cell.
thStyleStyleValueCSS styles you would like to apply to the table <thead>/<tfoot> field <th> cell.
variantColorVariant | nullApply contextual class to all the <th> and <td> in the column.
tdAttrAttrsValue | ((value: unknown, key: string, item: T) => AttrsValue)Object representing additional attributes to apply to the <tbody> field <td> cell. If custom attributes per cell are required, a callback function can be specified instead. See the typescript definition for accepted parameters and return types.
thAttrAttrsValue | ((value: unknown, key: string, item: T | null, type: TableRowThead) => AttrsValueObject representing additional attributes to apply to the field's <thead>/<tfoot> heading <th> cell. If the field's isRowHeader is set to true, the attributes will also apply to the <tbody> field <th> cell. If custom attributes per cell are required, a callback function can be specified instead. See the typescript definition for accepted parameters and return types.
isRowHeaderbooleanWhen set to true, the field's item data cell will be rendered with <th> rather than the default of <td>.
stickyColumnbooleanWhen set to true, and the table in responsive mode or has sticky headers, will cause the column to become fixed to the left when the table's horizontal scrollbar is scrolled. See Sticky columns for more details

Notes:

  • Field properties, if not present, default to null (falsey) unless otherwise stated above.
  • class, thClass, tdClass etc. will not work with classes that are defined in scoped CSS, unless you are using Vue's Deep selector.
  • For information on the syntax supported by thStyle, see Class and Style Bindings in the Vue.js guide.
  • Any additional properties added to the field definition objects will be left intact - so you can access them via the named scoped slots for custom data, header, and footer rendering.

For information and usage about scoped slots and formatters, refer to the Custom Data Rendering section below.

Feel free to mix and match simple array and object array together:

ts
const fields = [
  {key: 'first_name', label: 'First'},
  {key: 'last_name', label: 'Last'},
  'age',
  'sex',
]

Primary Key

<BTable> provides an additional prop primary-key, which you can use to identify the name of the field key that uniquely identifies the row.

The value specified by the primary column key must be either a string or number, and must be unique across all rows in the table.

The primary key column does not need to appear in the displayed fields.

Table row ID generation

When provided, the primary-key will generate a unique ID for each item row <tr> element. The ID will be in the format of {table-id}__row_{primary-key-value}, where {table-id} is the unique ID of the <BTable> and {primary-key-value} is the value of the item's field value for the field specified by primary-key.

Table render and transition optimization

The primary-key is also used by <BTable> to help Vue optimize the rendering of table rows. Internally, the value of the field key specified by the primary-key prop is used as the Vue :key value for each rendered item row <tr> element.

If you are seeing rendering issue (i.e. tooltips hiding or unexpected subcomponent re-usage when item data changes or data is sorted/filtered/edited) or table row transitions are not working, setting the primary-key prop (if you have a unique identifier per row) can alleviate these issues.

Specifying the primary-key column is handy if you are using 3rd party table transitions or drag and drop plugins, as they rely on having a consistent and unique per row :key value.

If primary-key is not provided, <BTable> will auto-generate keys based on the displayed row's index number (i.e. position in the displayed table rows). This may cause GUI issues such as sub components/elements that are rendering with previous results (i.e. being re-used by Vue's render patch optimization routines). Specifying a primary-key column can alleviate this issue (or you can place a unique :key on your element/components in your custom formatted field slots).

Refer to the Table body transition support section for additional details.

Table Style Options

Table styling

<BTable> provides several props to alter the style of the table:

propTypeDescription
stripedbooleanAdd zebra-striping to the table rows within the <tbody>
striped-columnsbooleanAdd zebra-striping to the table colums within the <tbody>
borderedbooleanFor borders on all sides of the table and cells.
borderlessbooleanremoves inner borders from table.
outlinedbooleanFor a thin border on all sides of the table. Has no effect if bordered is set.
smallbooleanTo make tables more compact by cutting cell padding in half.
hoverbooleanTo enable a hover highlighting state on table rows within a <tbody>
darkbooleanInvert the colors — with light text on dark backgrounds (equivalent to Bootstrap v5 class .table-dark)
fixedbooleanGenerate a table with equal fixed-width columns (table-layout: fixed;) Not yet implemented
responsiveboolean | BreakpointGenerate a responsive table to make it scroll horizontally. Set to true for an always responsive table, or set it to one of the breakpoints 'sm', 'md', 'lg', 'xl' or 'xxl' to make the table responsive (horizontally scroll) only on screens smaller than the breakpoint. See Responsive tables below for details.
sticky-headerboolean | NumberishGenerates a vertically scrollable table with sticky headers. Set to true to enable sticky headers (default table max-height of 300px), or set it to a string containing a height (with CSS units) to specify a maximum height other than 300px. See the Sticky header section below for details.
stackedboolean | BreakpointGenerate a responsive stacked table. Set to true for an always stacked table, or set it to one of the breakpoints 'sm', 'md', 'lg', 'xl' or 'xxl' to make the table visually stacked only on screens smaller than the breakpoint. See Stacked tables below for details.
caption-topboolean | NumberishIf the table has a caption, and this prop is set to true, the caption will be visually placed above the table. If false (the default), the caption will be visually placed below the table.
variantColorVariant | nullGive the table an overall theme color variant.
head-variantColorVariant | nullMake the table head a theme color different from the table
foot-row-variantColorVariant | nullMake the table foot a theme color different from the table. If not set, head-variant will be used. Has no effect if foot-clone is not set
head-row-variantColorVariant | nullMake the only the <tr> part of the <head> a specific theme color
foot-variantColorVariant | nullMake the only the <tr> part of the <foot> a specific theme color. If not set, head-row-variant will be used. Has no effect if foot-clone is not set
foot-clonebooleanTurns on the table footer, and defaults with the same contents a the table header
no-footer-sortingbooleanWhen foot-clone is true and the table is sortable, disables the sorting icons and click behaviour on the footer heading cells. Refer to the Sorting section below for more details. Not yet implemented
no-border-collapseBooleanDisables the default of collapsing of the table borders. Mainly for use with sticky headers and/or sticky columns. Will cause the appearance of double borders in some situations. Not yet implemented

NOTE

The table style options fixed, stacked, no-border-collapse, sticky headers, sticky columns and the table sorting feature, all require BootstrapVueNext's custom CSS.

Table Options
First nameLast nameAge
DickersonMacdonald40
LarsenShaw21
GenevaWilson89
HTML
vue
<template>
  <div>
    <BFormGroup v-slot="{ariaDescribedby}" label="Table Options" label-cols-lg="2">
      <BFormCheckbox v-model="striped" :aria-describedby="ariaDescribedby" inline
        >Striped</BFormCheckbox
      >
      <BFormCheckbox v-model="stripedColumns" :aria-describedby="ariaDescribedby" inline
        >Striped Columns</BFormCheckbox
      >
      <BFormCheckbox v-model="bordered" :aria-describedby="ariaDescribedby" inline
        >Bordered</BFormCheckbox
      >
      <BFormCheckbox v-model="borderless" :aria-describedby="ariaDescribedby" inline
        >Borderless</BFormCheckbox
      >
      <BFormCheckbox v-model="outlined" :aria-describedby="ariaDescribedby" inline
        >Outlined</BFormCheckbox
      >
      <BFormCheckbox v-model="small" :aria-describedby="ariaDescribedby" inline
        >Small</BFormCheckbox
      >
      <BFormCheckbox v-model="hover" :aria-describedby="ariaDescribedby" inline
        >Hover</BFormCheckbox
      >
      <BFormCheckbox v-model="dark" :aria-describedby="ariaDescribedby" inline>Dark</BFormCheckbox>
      <BFormCheckbox v-model="fixed" :aria-describedby="ariaDescribedby" inline
        >Fixed</BFormCheckbox
      >
      <BFormCheckbox v-model="footClone" :aria-describedby="ariaDescribedby" inline
        >Foot Clone</BFormCheckbox
      >
      <BFormCheckbox v-model="noCollapse" :aria-describedby="ariaDescribedby" inline
        >No border collapse</BFormCheckbox
      >
    </BFormGroup>

    <BFormGroup label="Variant" label-for="table-style-variant" label-cols-lg="2" class="my-2">
      <BFormSelect id="table-style-variant" v-model="variant" :options="variants">
        <template #first>
          <option :value="null">-- None --</option>
        </template>
      </BFormSelect>
    </BFormGroup>

    <BFormGroup label="Head Variant" label-for="head-style-variant" label-cols-lg="2" class="my-2">
      <BFormSelect id="head-style-variant" v-model="headVariant" :options="variants">
        <template #first>
          <option :value="null">-- None --</option>
        </template>
      </BFormSelect>
    </BFormGroup>

    <BFormGroup label="Foot Variant" label-for="foot-style-variant" label-cols-lg="2" class="my-2">
      <BFormSelect id="foot-style-variant" v-model="footVariant" :options="variants">
        <template #first>
          <option :value="null">-- None --</option>
        </template>
      </BFormSelect>
    </BFormGroup>

    <BTable
      :striped="striped"
      :striped-columns="stripedColumns"
      :bordered="bordered"
      :borderless="borderless"
      :outlined="outlined"
      :small="small"
      :hover="hover"
      :dark="dark"
      :fixed="fixed"
      :foot-clone="footClone"
      :no-border-collapse="noCollapse"
      :items="items"
      :fields="fields"
      :variant="variant"
      :head-variant="headVariant"
      :foot-variant="footVariant"
    />
  </div>
</template>

<script setup lang="ts">
import type {ColorVariant} from 'bootstrap-vue-next'
import {ref} from 'vue'

const fields = ['first_name', 'last_name', 'age']
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
]

const striped = ref(false)
const stripedColumns = ref(false)
const bordered = ref(false)
const borderless = ref(false)
const outlined = ref(false)
const small = ref(false)
const hover = ref(false)
const dark = ref(false)
const fixed = ref(false)
const footClone = ref(false)
const variant = ref<ColorVariant | null>(null)
const headVariant = ref<ColorVariant | null>(null)
const footVariant = ref<ColorVariant | null>(null)
const noCollapse = ref(false)

const variants = ['primary', 'secondary', 'info', 'danger', 'warning', 'success', 'light', 'dark']
</script>

Row styling and attributes

You can also style every row using the tbody-tr-class prop, and optionally supply additional attributes via the tbody-tr-attr prop:

PropertyTypeDescription
tbody-tr-class((item: Items | null, type: TableRowType) => TableStrictClassValue) | TableStrictClassValueClasses to be applied to every row on the table.
tbody-tr-attr((item: Items | null, type: TableRowType) => AttrsValue) | AttrsValueAttributes to be applied to every row on the table.

When passing a function reference to tbody-tr-class or tbody-tr-attr, the function's arguments will be as follows:

  • item - The item record data associated with the row. For rows that are not associated with an item record, this value will be null or undefined
  • type - The type of row being rendered (TableRowType). 'row' for an item row, 'row-details' for an item details row, 'row-top' for the fixed row top slot, 'row-bottom' for the fixed row bottom slot, or 'table-busy' for the table busy slot.
First nameLast nameAge
DickersonMacdonald40
LarsenShaw21
GenevaWilson89
HTML
vue
<template>
  <div>
    <BTable :items="items" :fields="fields" :tbody-tr-class="rowClass" />
  </div>
</template>

<script setup lang="ts">
import type {TableRowType, TableStrictClassValue} from 'bootstrap-vue-next'

interface Person {
  age: number
  first_name: string
  last_name: string
  status?: string
}

const fields = ['first_name', 'last_name', 'age']
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald', status: 'awesome'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
]

const rowClass = (item: Person, type: TableRowType): TableStrictClassValue =>
  type === 'row' && item.status === 'awesome' ? 'table-success' : ''
</script>

Responsive tables

Responsive tables allow tables to be scrolled horizontally with ease. Make any table responsive across all viewports by setting the prop responsive to true. Or, pick a maximum breakpoint with which to have a responsive table up to by setting the prop responsive to one of the breakpoint values: sm, md, lg, or xl.

Heading1Heading2Heading3Heading4Heading5Heading6Heading7Heading8Heading9Heading10Heading11Heading12
table celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable cell
table celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable cell
table celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable celltable cell
HTML
vue
<template>
  <div>
    <BTable responsive :items="items" />
  </div>
</template>

<script setup lang="ts">
const items = [
  {
    heading1: 'table cell',
    heading2: 'table cell',
    heading3: 'table cell',
    heading4: 'table cell',
    heading5: 'table cell',
    heading6: 'table cell',
    heading7: 'table cell',
    heading8: 'table cell',
    heading9: 'table cell',
    heading10: 'table cell',
    heading11: 'table cell',
    heading12: 'table cell',
  },
  {
    heading1: 'table cell',
    heading2: 'table cell',
    heading3: 'table cell',
    heading4: 'table cell',
    heading5: 'table cell',
    heading6: 'table cell',
    heading7: 'table cell',
    heading8: 'table cell',
    heading9: 'table cell',
    heading10: 'table cell',
    heading11: 'table cell',
    heading12: 'table cell',
  },
  {
    heading1: 'table cell',
    heading2: 'table cell',
    heading3: 'table cell',
    heading4: 'table cell',
    heading5: 'table cell',
    heading6: 'table cell',
    heading7: 'table cell',
    heading8: 'table cell',
    heading9: 'table cell',
    heading10: 'table cell',
    heading11: 'table cell',
    heading12: 'table cell',
  },
]
</script>

Responsive table notes:

  • Possible vertical clipping/truncation. Responsive tables make use of overflow-y: hidden, which clips off any content that goes beyond the bottom or top edges of the table. In particular, this may clip off dropdown menus and other third-party widgets.
  • Using props responsive and fixed together will not work as expected. Fixed table layout uses the first row (table header in this case) to compute the width required by each column (and the overall table width) to fit within the width of the parent container — without taking cells in the <tbody> into consideration — resulting in table that may not be responsive. To get around this limitation, you would need to specify widths for the columns (or certain columns) via one of the following methods:
    • Use <col> elements within the table-colgroup slot that have widths set (e.g. <col style="width: 20rem">), or
    • Wrap header cells in <div> elements, via the use of custom header rendering, which have a minimum width set on them, or
    • Use the thStyle property of the field definition object to set a width for the column(s), or
    • Use custom CSS to define classes to apply to the columns to set widths, via the thClass or class properties of the field definition object.

Stacked tables

An alternative to responsive tables, BootstrapVue includes the stacked table option (using custom SCSS/CSS), which allow tables to be rendered in a visually stacked format. Make any table stacked across all viewports by setting the prop stacked to true. Or, alternatively, set a breakpoint at which the table will return to normal table format by setting the prop stacked to one of the breakpoint values 'sm', 'md', 'lg', or 'xl'.

Column header labels will be rendered to the left of each field value using a CSS ::before pseudo element, with a width of 40%.

The stacked prop takes precedence over the sticky-header prop and the stickyColumn field definition property.

AgeFirst nameLast name
40DickersonMacdonald
21LarsenShaw
89GenevaWilson
HTML
vue
<template>
  <div>
    <BTable stacked :items="items" />
  </div>
</template>

<script setup lang="ts">
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
]
</script>

Note: When the table is visually stacked:

  • The table header (and table footer) will be hidden.
  • Custom rendered header slots will not be shown, rather, the fields' label will be used.
  • The table cannot be sorted by clicking the rendered field labels. You will need to provide an external control to select the field to sort by and the sort direction. See the Sorting section below for sorting control information, as well as the complete example at the bottom of this page for an example of controlling sorting via the use of form controls.
  • The slots top-row and bottom-row will be hidden when visually stacked.
  • The table caption, if provided, will always appear at the top of the table when visually stacked.
  • In an always stacked table, the table header and footer, and the fixed top and bottom row slots will not be rendered.

BootstrapVueNext's custom CSS is required in order to support stacked tables.

Table caption

Add an optional caption to your table via the prop caption or the named slot table-caption (the slot takes precedence over the prop). The default Bootstrap v4 styling places the caption at the bottom of the table:

First nameLast nameAge
DickersonMacdonald40
LarsenShaw21
GenevaWilson89
This is a table caption.
HTML
vue
<template>
  <div>
    <BTable :items="items" :fields="fields">
      <template #table-caption>This is a table caption.</template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
const fields = ['first_name', 'last_name', 'age']
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
]
</script>

You can have the caption placed at the top of the table by setting the caption-top prop to true:

First nameLast nameAge
DickersonMacdonald40
LarsenShaw21
GenevaWilson89
This is a table caption at the top.
HTML
vue
<template>
  <div>
    <BTable :items="items" :fields="fields" caption-top>
      <template #table-caption>This is a table caption at the top.</template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
const fields = ['first_name', 'last_name', 'age']
const items = [
  {age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {age: 89, first_name: 'Geneva', last_name: 'Wilson'},
]
</script>

You can also use custom CSS to control the caption positioning.

Table colgroup

Not yet implemented The table-colgroup slot is not yet implemented.

Table busy state

<BTable> provides a busy model that will flag the table as busy, which you can set to true just before you update your items, and then set it to false once you have your items. When in the busy state, the table will have the attribute aria-busy="true".

During the busy state, the table will be rendered in a "muted" look (opacity: 0.55), using the following custom CSS:

css
/* Busy table styling */
.table.b-table[aria-busy="true"] {
  opacity: 0.55;
}

table-busy slot

First nameLast nameAge
DickersonMacDonald40
LarsenShaw21
GenevaWilson89
JamiCarney38
HTML
vue
<template>
  <div>
    <BButton @click="toggleBusy">Toggle Busy State</BButton>

    <BTable :items="items" :busy="isBusy" class="mt-3" outlined>
      <!-- <template #table-busy>
        <div class="text-center text-danger my-2">
          <BSpinner class="align-middle" />
          <strong>Loading...</strong>
        </div>
      </template> -->
    </BTable>
  </div>
</template>

<script setup lang="ts">
import {ref} from 'vue'
const isBusy = ref(false)
const items = [
  {first_name: 'Dickerson', last_name: 'MacDonald', age: 40},
  {first_name: 'Larsen', last_name: 'Shaw', age: 21},
  {first_name: 'Geneva', last_name: 'Wilson', age: 89},
  {first_name: 'Jami', last_name: 'Carney', age: 38},
]

const toggleBusy = () => {
  isBusy.value = !isBusy.value
}
</script>

Also see the Using Items Provider Functions below for additional information on the busy state.

Notes:

  • All click related and hover events, and sort-changed events will not be emitted when the table is in the busy state.
  • Busy styling and slot are not available in the <BTableLite> component.

Custom data rendering

Custom rendering for each data field in a row is possible using either scoped slots or a formatter callback function, or a combination of both.

Scoped field slots

Scoped field slots give you greater control over how the record data appears. You can use scoped slots to provided custom rendering for a particular field. If you want to add an extra field which does not exist in the records, just add it to the fields array, and then reference the field(s) in the scoped slot(s). Scoped field slots use the following naming syntax: `'cell(${field_key})'`.

You can use the default fall-back scoped slot 'cell()' to format any cells that do not have an explicit scoped slot provided.

IndexFull NameAgeSexFirst name and age
1DOE, John42MaleJohn is 42 years old
2DOE, Jane36FemaleJane is 36 years old
3KINCADE, Rubin73MaleRubin is 73 years old
4PARTRIDGE, Shirley62FemaleShirley is 62 years old
HTML
vue
<template>
  <div>
    <BTable small :fields="fields" :items="items" responsive="sm">
      <!-- A virtual column -->
      <template #cell(index)="data">
        {{ data.index + 1 }}
      </template>

      <!-- A custom formatted column -->
      <template #cell(name)="data">
        <b class="text-info">{{ data.item.name.last.toUpperCase() }}</b
        >, <b>{{ data.item.name.first }}</b>
      </template>

      <!-- A virtual composite column -->
      <template #cell(nameage)="data">
        {{ data.item.name.first }} is {{ data.item.age }} years old
      </template>

      <!-- Optional default data cell scoped slot -->
      <template #cell()="data">
        <i>{{ data.value }}</i>
      </template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
interface Person {
  name: {first: string; last: string}
  sex: string
  age: number
}

const fields = [
  // A virtual column that doesn't exist in items
  'index',
  // A column that needs custom formatting
  {key: 'name', label: 'Full Name'},
  // A regular column
  'age',
  // A regular column
  'sex',
  // A virtual column made up from two fields
  {key: 'nameage', label: 'First name and age'},
]
const items: Person[] = [
  {name: {first: 'John', last: 'Doe'}, sex: 'Male', age: 42},
  {name: {first: 'Jane', last: 'Doe'}, sex: 'Female', age: 36},
  {name: {first: 'Rubin', last: 'Kincade'}, sex: 'Male', age: 73},
  {name: {first: 'Shirley', last: 'Partridge'}, sex: 'Female', age: 62},
]
</script>

The slot's scope variable (data in the above sample) will have the following properties:

PropertyTypeDescription
indexnumberThe row number (indexed from zero) relative to the displayed rows
itemItemsThe entire raw record data (i.e. items[index]) for this row (before any formatter is applied)
valueunknownThe value for this key in the record (null or undefined if a virtual column), or the output of the field's formatter function
unformattedunknownThe raw value for this key in the item record (null or undefined if a virtual column), before being passed to the field's formatter function
field(typeof computedFields.value)[0]The field's normalized field definition object
detailsShowingbooleanWill be true if the row's row-details scoped slot is visible. See section Row details support below for additional information
toggleDetails() => voidCan be called to toggle the visibility of the rows row-details scoped slot. See section Row details support below for additional information
rowSelectedbooleanWill be true if the row has been selected. See section Row select support for additional information
selectRow(index?: number) => voidWhen called, selects the current row. See section Row select support for additional information
unselectRow(index?: number) => voidWhen called, unselects the current row. See section Row select support for additional information

Notes:

  • index will not always be the actual row's index number, as it is computed after filtering, sorting and pagination have been applied to the original table data. The index value will refer to the displayed row number.
  • When using the v-slot syntax, note that slot names cannot contain spaces, and when using in-browser DOM templates the slot names will always be lower cased. To get around this, you can pass the slot name using Vue's dynamic slot names

Displaying raw HTML

By default BTable escapes HTML tags in items data and results of formatter functions, if you need to display raw HTML code in BTable, you should use v-html directive on an element in a in scoped field slot.

TextHtml
This is <i>escaped</i> contentThis is raw HTML content
HTML
vue
<template>
  <div>
    <b-table :items="items">
      <template #cell(html)="data">
        <!-- eslint-disable vue/no-v-html -->
        <span v-html="data.value" />
      </template>
    </b-table>
  </div>
</template>

<script setup lang="ts">
const items = [
  {
    text: 'This is <i>escaped</i> content',
    html: 'This is <i>raw <strong>HTML</strong></i> <span style="color:red">content</span>',
  },
]
</script>

WARNING

Be cautious of using the v-html method to display user supplied content, as it may make your application vulnerable to XSS attacks, if you do not first sanitize the user supplied string.

Formatter callback

Optionally, you can customize field output by using a formatter callback function. To enable this, the field's formatter property is used. The value of this property may be String or function reference. In case of a String value, the function must be defined at the parent component's methods. When providing formatter as a Function, it must be declared at global scope (window or as global mixin at Vue, or as an anonymous function), unless it has been bound to a this context.

The callback function accepts three arguments - value, key, and item, and should return the formatted value as a string (HTML strings are not supported)

Full NameAgeSexCalculated Birth Year
John Doe42M1983
Jane Doe36F1989
Rubin Kincade73M1952
Shirley Partridge62F1963
HTML
vue
<template>
  <div>
    <BTable :fields="fields" :items="items">
      <template #cell(name)="data">
        <!-- `data.value` is the value after formatted by the Formatter -->
        <a :href="`#${(data.value as any as string).replace(/[^a-z]+/i, '-').toLowerCase()}`">{{
          data.value
        }}</a>
      </template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
import type {TableFieldRaw} from 'bootstrap-vue-next'

interface Name {
  first: string
  last: string
}

interface Person {
  name: Name
  sex: string
  age: number
}

const fullName = (value: unknown) => {
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  const name = value as any as Name
  return `${name.first} ${name.last}`
}

const fields: TableFieldRaw<Person>[] = [
  {
    // A column that needs custom formatting,
    // calling formatter 'fullName' in this app
    key: 'name',
    label: 'Full Name',
    formatter: fullName,
  },
  // A regular column
  'age',
  {
    // A regular column with custom formatter
    key: 'sex',
    // eslint-disable-next-line @typescript-eslint/no-explicit-any
    formatter: (value) => (value as any as string).charAt(0).toUpperCase(),
  },
  {
    // A virtual column with custom formatter
    key: 'birthYear',
    label: 'Calculated Birth Year',
    formatter: (value_, key_, item: Person) => (new Date().getFullYear() - item.age).toString(),
  },
]

const items = [
  {name: {first: 'John', last: 'Doe'}, sex: 'Male', age: 42},
  {name: {first: 'Jane', last: 'Doe'}, sex: 'Female', age: 36},
  {name: {first: 'Rubin', last: 'Kincade'}, sex: 'male', age: 73},
  {name: {first: 'Shirley', last: 'Partridge'}, sex: 'female', age: 62},
]
</script>

It is also possible to provide custom rendering for the table's thead and tfoot elements. Note by default the table footer is not rendered unless foot-clone is set to true.

Scoped slots for the header and footer cells uses a special naming convention of 'head(<fieldkey>)' and 'foot(<fieldkey>)' respectively. if a 'foot(...)' slot for a field is not provided, but a 'head(...)' slot is provided, then the footer will use the 'head(...)' slot content.

You can use a default fall-back scoped slot 'head()' or 'foot()' to format any header or footer cells that do not have an explicit scoped slot provided.

FULL NAMEAgeSex
John Doe42Male
Jane Doe36Female
Rubin Kincade73Male
Shirley Partridge62Female
Full Name
Age
Sex
HTML
vue
<template>
  <div>
    <BTable :fields="fields" :items="items" foot-clone>
      <!-- A custom formatted data column cell -->
      <template #cell(name)="data">
        {{ (data.value as any as Name).first }} {{ (data.value as any as Name).last }}
      </template>

      <!-- A custom formatted header cell for field 'name' -->
      <template #head(name)="data">
        <span class="text-info">{{ data.label!.toUpperCase() }}</span>
      </template>

      <!-- A custom formatted footer cell for field 'name' -->
      <template #foot(name)="data">
        <span class="text-danger">{{ data.label }}</span>
      </template>

      <!-- Default fall-back custom formatted footer cell -->
      <template #foot()="data">
        <i>{{ data.label }}</i>
      </template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
type Name = {first: string; last: string}

const fields = [
  // A column that needs custom formatting
  {key: 'name', label: 'Full Name'},
  // A regular column
  'age',
  // A regular column
  'sex',
]
const items = [
  {name: {first: 'John', last: 'Doe'}, sex: 'Male', age: 42},
  {name: {first: 'Jane', last: 'Doe'}, sex: 'Female', age: 36},
  {name: {first: 'Rubin', last: 'Kincade'}, sex: 'Male', age: 73},
  {name: {first: 'Shirley', last: 'Partridge'}, sex: 'Female', age: 62},
]
</script>

The slots can be optionally scoped (data in the above example), and will have the following properties:

PropertyTypeDescription
columnLiteralUnion<keyof Items>The fields's key value
fieldTableField<Items>the field's object (from the fields prop)
labelstring | undefinedThe fields label value (also available as data.field.label)
isFootbooleanCurrently rending the foot if true
selectAllRows() => voidSelect all rows (applicable if the table is in selectable mode
clearSelected() => voidUnselect all rows (applicable if the table is in selectable mode

When placing inputs, buttons, selects or links within a head(...) or foot(...) slot, note that head-clicked event will not be emitted when the input, select, textarea is clicked (unless they are disabled). head-clicked will never be emitted when clicking on links or buttons inside the scoped slots (even when disabled)

Notes:

  • Slot names cannot contain spaces, and when using in-browser DOM templates the slot names will always
  • be lower cased. To get around this, you can pass the slot name using Vue's dynamic slot names

Adding additional rows to the header

If you wish to add additional rows to the header you may do so via the thead-top slot. This slot is inserted before the header cells row, and is not automatically encapsulated by <tr>..</tr> tags. It is recommended to use the BootstrapVue table helper components, rather than native browser table child elements.

Name and IDType 1Type 2Type 3
NameIDType 1Type 2AType 2BType 2CType 3
Stephen Hawking1falsetruefalsefalsefalse
Johnny Appleseed2falsetruetruefalsefalse
George Washington3falsefalsefalsefalsetrue
Albert Einstein4truefalsefalsetruefalse
Isaac Newton5truetruefalsetruefalse
HTML
vue
<template>
  <div>
    <BTable :items="items" :fields="fields" responsive="sm">
      <template #thead-top>
        <BTr>
          <BTh colspan="2"><span class="visually-hidden">Name and ID</span></BTh>
          <BTh variant="secondary">Type 1</BTh>
          <BTh variant="primary" colspan="3">Type 2</BTh>
          <BTh variant="danger">Type 3</BTh>
        </BTr>
      </template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
const items = [
  {
    name: 'Stephen Hawking',
    id: 1,
    type1: false,
    type2a: true,
    type2b: false,
    type2c: false,
    type3: false,
  },
  {
    name: 'Johnny Appleseed',
    id: 2,
    type1: false,
    type2a: true,
    type2b: true,
    type2c: false,
    type3: false,
  },
  {
    name: 'George Washington',
    id: 3,
    type1: false,
    type2a: false,
    type2b: false,
    type2c: false,
    type3: true,
  },
  {
    name: 'Albert Einstein',
    id: 4,
    type1: true,
    type2a: false,
    type2b: false,
    type2c: true,
    type3: false,
  },
  {
    name: 'Isaac Newton',
    id: 5,
    type1: true,
    type2a: true,
    type2b: false,
    type2c: true,
    type3: false,
  },
]
const fields = [
  'name',
  {key: 'id', label: 'ID'},
  {key: 'type1', label: 'Type 1'},
  {key: 'type2a', label: 'Type 2A'},
  {key: 'type2b', label: 'Type 2B'},
  {key: 'type2c', label: 'Type 2C'},
  {key: 'type3', label: 'Type 3'},
]
</script>

Slot thead-top can be optionally scoped, receiving an object with the following properties:

PropertyTypeDescription
columnsnumberThe number of columns in the rendered table
fieldsTableField<Items>[]Array of field definition objects (normalized to the array of objects format)
selectAllRows() => voidSelect all rows (applicable if the table is in selectable mode
clearSelected() => voidUnselect all rows (applicable if the table is in selectable mode

If you need greater layout control of the content of the <tfoot>, you can use the optionally scoped slot custom-foot to provide your own rows and cells. Use BootstrapVue's table helper sub-components <BTr>, <BTh>, and <BTd> to generate your custom footer layout.

Slot custom-foot can be optionally scoped, receiving an object with the following properties:

PropertyTypeDescription
columnsnumberThe number of columns in the rendered table
fieldsTableField<Items>[]Array of field definition objects (normalized to the array of objects format)
itemsreadonly Items[]Array of the currently displayed items records - after filtering, sorting and pagination

Notes:

  • The custom-foot slot will not be rendered if the foot-clone prop has been set.
  • head-clicked events are not be emitted when clicking on custom-foot cells.
  • Sorting and sorting icons are not available for cells in the custom-foot slot.
  • The custom footer will not be shown when the table is in visually stacked mode.

Custom empty and empty-filtered rendering via slots

Aside from using empty-text, empty-filtered-text, it is also possible to provide custom rendering for tables that have no data to display using named slots.

In order for these slots to be shown, the show-empty attribute must be set and items must be either falsy or an array of length 0.

template
<BTable :fields="fields" :items="items" show-empty>
  <template #empty="scope">
    <h4>{{ scope.emptyText }}</h4>
  </template>
  <template #empty-filtered="scope">
    <h4>{{ scope.emptyFilteredText }}</h4>
  </template>
</BTable>

The slot can optionally be scoped. The slot's scope (scope in the above example) will have the following properties:

PropertyTypeDescription
emptyFilteredHtmlstringThe empty-filtered-html prop
emptyFilteredTextstringThe empty-filtered-text prop
fieldsTableField<Items>[]The fields prop
itemsItems[]The items prop. Exposed here to check null vs []

NOTE

If you prefiously used the emptyHtml or emtpyFilteredHtml scoped slots or the empty-html or empty-filtered-html props, please convert to using the empty-text or empty-filtered-text slots instead. See our migration guide for details.

Advanced Features

Sticky headers

Use the sticky-header prop to enable a vertically scrolling table with headers that remain fixed (sticky) as the table body scrolls. Setting the prop to true (or no explicit value) will generate a table that has a maximum height of 300px. To specify a maximum height other than 300px, set the sticky-header prop to a valid CSS height (including units), i.e. sticky-header="200px". Tables with sticky-header enabled will also automatically become always responsive horizontally, regardless of the responsive prop setting, if the table is wider than the available horizontal space.

Heading1Heading2Heading3
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
table celltable celltable cell
HTML
vue
<template>
  <div>
    <BTable sticky-header :items="items" head-variant="light" />
  </div>
</template>

<script setup lang="ts">
const items = [
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
  {heading1: 'table cell', heading2: 'table cell', heading3: 'table cell'},
]
</script>

Sticky header notes:

  • The sticky-header prop has no effect if the table has the stacked prop set.
  • Sticky header tables are wrapped inside a vertically scrollable <div> with a maximum height set.
  • BootstrapVue's custom CSS is required in order to support sticky-header.
  • Bootstrap v5 uses the CSS style border-collapse: collapsed on table elements. This prevents the borders on the sticky header from "sticking" to the header, and hence the borders will scroll when the body scrolls. To get around this issue, set the prop no-border-collapse on the table (note that this may cause double width borders when using features such as bordered, etc.).

Sticky columns

Columns can be made sticky, where they stick to the left of the table when the table has a horizontal scrollbar. To make a column a sticky column, set the stickyColumn prop in the field's header definition. Sticky columns will only work when the table has either the sticky-header prop set and/or the responsive prop is set.

Row ID
Heading A
Heading B
Heading
Heading D
Heading E
Heading F
Heading G
Heading H
Heading I
Heading J
Heading K
Heading L
101234567891011
201234567891011
301234567891011
401234567891011
501234567891011
601234567891011
701234567891011
801234567891011
901234567891011
1001234567891011
HTML
vue
<template>
  <div>
    <div class="mb-2">
      <b-form-checkbox v-model="stickyHeader" inline>Sticky header</b-form-checkbox>
      <b-form-checkbox v-model="noCollapse" inline>No border collapse</b-form-checkbox>
    </div>
    <b-table
      :sticky-header="stickyHeader"
      :no-border-collapse="noCollapse"
      responsive
      :items="items"
      :fields="fields"
    >
      <!-- We are using utility class `text-nowrap` to help illustrate horizontal scrolling -->
      <template #head(id)>
        <div class="text-nowrap">Row ID</div>
      </template>
      <template #head()="scope">
        <div class="text-nowrap">Heading {{ scope.label }}</div>
      </template>
    </b-table>
  </div>
</template>

<script setup lang="ts">
import type {TableFieldRaw} from 'bootstrap-vue-next'
import {ref} from 'vue'

const fields: TableFieldRaw[] = [
  {key: 'id', stickyColumn: true, isRowHeader: true, variant: 'primary'},
  'a',
  'b',
  {key: 'c', stickyColumn: true, variant: 'info'},
  'd',
  'e',
  'f',
  'g',
  'h',
  'i',
  'j',
  'k',
  'l',
]
const items = [
  {id: 1, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 2, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 3, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 4, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 5, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 6, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 7, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 8, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 9, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
  {id: 10, a: 0, b: 1, c: 2, d: 3, e: 4, f: 5, g: 6, h: 7, i: 8, j: 9, k: 10, l: 11},
]

const stickyHeader = ref(true)
const noCollapse = ref(false)
</script>

Sticky column notes:

  • Sticky columns has no effect if the table has the stacked prop set.
  • Sticky columns tables require either the sticky-header and/or responsive modes, and are wrapped inside a horizontally scrollable <div>.
  • When you have multiple columns that are set as stickyColumn, the columns will stack over each other visually, and the left-most sticky columns may "peek" out from under the next sticky column. To get around this behaviour, make sure your latter sticky columns are the same width or wider than previous sticky columns.
  • Bootstrap v5 uses the CSS style border-collapse: collapsed on table elements. This prevents any borders on the sticky columns from "sticking" to the column, and hence those borders will scroll when the body scrolls. To get around this issue, set the prop no-border-collapse on the table (note that this may cause double width borders when using features such as bordered, etc.).
  • BootstrapVue's custom CSS is required in order to support sticky columns.
  • The sticky column feature uses CSS style position: sticky to position the column cells. Internet Explorer does not support position: sticky, hence for IE 11 the sticky column will scroll with the table body.

Row details support

If you would optionally like to display additional record information (such as columns not specified in the fields definition array), you can use the scoped slot row-details, in combination with the special item record boolean property _showDetails.

If the record has its _showDetails property set to true, and a row-details scoped slot exists, a new row will be shown just below the item, with the rendered contents of the row-details scoped slot.

In the scoped field slot, you can toggle the visibility of the row's row-details scoped slot by calling the toggleDetails function passed to the field's scoped slot variable. You can use the scoped fields slot variable detailsShowing to determine the visibility of the row-details slot.

NOTE

If manipulating the _showDetails property directly on the item data (i.e. not via the toggleDetails function reference), the _showDetails property must exist in the items data for proper reactive detection of changes to its value. Read more about how reactivity works in Vue.

Available row-details scoped variable properties:

PropertyTypeDescription
itemItemsThe entire row record data object
indexnumberThe current visible row number
fieldsTableField<Items>[]The normalized fields definition array (in the array of objects format)
toggleDetails() => voidFunction to toggle visibility of the row's details slot
rowSelectedbooleanWill be true if the row has been selected. See section Row select support for additional information
selectRow(index?: number) => voidWhen called, selects the current row. See section Row select support for additional information
unselectRow(index?: number) => voidWhen called, unselects the current row. See section Row select support for additional information

NOTE

the row select related scope properties are only available in <BTable>.

In the following example, we show two methods of toggling the visibility of the details: one via a button, and one via a checkbox. We also show the third row details defaulting to have details initially showing.

First nameLast nameShow details
DickersonMacdonald
LarsenShaw
GenevaWilson
Age:
89
Is Active:
false
JamiCarney
HTML
vue
<template>
  <div>
    <BTable :items="items" :fields="fields" striped responsive="sm">
      <template #cell(show_details)="row">
        <BButton size="sm" class="mr-2" @click="row.toggleDetails">
          {{ row.detailsShowing ? 'Hide' : 'Show' }} Details
        </BButton>

        <!-- As `row.showDetails` is one-way, we call the toggleDetails function on @change -->
        <BFormCheckbox v-model="row.detailsShowing" @change="row.toggleDetails">
          Details via check
        </BFormCheckbox>
      </template>

      <template #row-details="row">
        <BCard>
          <BRow class="mb-2">
            <BCol sm="3" class="text-sm-right"><b>Age:</b></BCol>
            <BCol>{{ row.item.age }}</BCol>
          </BRow>

          <BRow class="mb-2">
            <BCol sm="3" class="text-sm-right"><b>Is Active:</b></BCol>
            <BCol>{{ row.item.isActive }}</BCol>
          </BRow>

          <BButton size="sm" @click="row.toggleDetails">Hide Details</BButton>
        </BCard>
      </template>
    </BTable>
  </div>
</template>

<script setup lang="ts">
const fields = ['first_name', 'last_name', 'show_details']
const items = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {
    isActive: false,
    age: 89,
    first_name: 'Geneva',
    last_name: 'Wilson',
    _showDetails: true,
  },
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]
</script>

Row select support

You can make rows selectable, by using the <BTable> prop selectable.

Users can easily change the selecting mode by setting the select-mode prop.

  • 'multi': Each click will select/deselect the row (default mode)
  • 'single': Only a single row can be selected at one time
  • 'range': Any row clicked is selected, any other deselected. Shift + click selects a range of rows, and Ctrl (or Cmd) + click will toggle the selected row.

When a table is selectable and the user clicks on a row, <BTable> will emit the update:selected-items event, passing a single argument which is the complete list of selected items. This argument is read-only. In addition, row-selected or row-unselected events are emitted for each row.

Rows can also be programmatically selected and unselected via the following exposed methods on the <BTable> instance:

MethodDescription
selectRow(index: number)Selects a row with the given index number.
unselectRow(index: number)Unselects a row with the given index number.
selectAllRows()Selects all rows in the table, except in single mode in which case only the first row is selected.
clearSelected()Unselects all rows.
isRowSelected(index: number)Returns true if the row with the given index is selected, otherwise it returns false.

Programmatic row selection notes:

  • index is the zero-based index of the table's visible rows, after filtering, sorting, and pagination have been applied.
  • In single mode, selectRow(index) will unselect any previous selected row.
  • Attempting to selectRow(index) or unselectRow(index) on a non-existent row will be ignored.
  • The table must be selectable for any of these methods to have effect.
  • You can disable selection of rows via click events by setting the no-select-on-click prop. Rows will then only be selectable programmatically.

Row select notes:

  • Sorting, filtering, or paginating the table will clear the active selection. The update:selected-items event will be emitted with an empty array ([]) if needed.
  • When the table is in selectable mode, all data item <tr> elements will be in the document tab sequence (tabindex="0") for accessibility reasons, and will have the attribute aria-selected set to either 'true' or 'false' depending on the selected state of the row.
  • Not yet implementedWhen a table is selectable, the table will have the attribute aria-multiselect set to either'false' for single mode, and 'true' for either multi or range modes.
Not yet implemented

Use the prop selected-variant to apply a Bootstrap theme color to the selected row(s). Note, due to the order that the table variants are defined in Bootstrap's CSS, any row-variant might take precedence over the selected-variant. You can set selected-variant to an empty string if you will be using other means to convey that a row is selected (such as a scoped field slot in the below example).

The selected-variant can be any of the standard (or custom) Bootstrap base color variants, or the special table active variant (the default) which takes precedence over any specific row or cell variants.

For accessibility reasons (specifically for color blind users, or users with color contrast issues), it is highly recommended to always provide some other visual means of conveying that a row is selected, such as a virtual column as shown in the example below.

SelectedIs ActiveAgeFirst nameLast name
Not selectedtrue40DickersonMacdonald
Not selectedfalse21LarsenShaw
Not selectedfalse89GenevaWilson
Not selectedtrue38JamiCarney

Selected Rows:
[]

HTML
vue
<template>
  <div>
    <BFormGroup label="Selection mode:" label-for="table-select-mode-select" label-cols-md="4">
      <BFormSelect
        id="table-select-mode-select"
        v-model="selectMode"
        :options="modes"
        class="mb-3"
      />
    </BFormGroup>

    <BTable
      ref="selectable-table"
      :items="items"
      :fields="fields"
      :select-mode="selectMode"
      responsive="sm"
      selectable
      @update:selected-items="onSelectedItems"
    >
      <!-- Example scoped slot for select state illustrative purposes -->
      <template #cell(selected)="{rowSelected}">
        <template v-if="rowSelected">
          <span aria-hidden="true">&check;</span>
          <span class="sr-only">Selected</span>
        </template>
        <template v-else>
          <span aria-hidden="true">&nbsp;</span>
          <span class="sr-only">Not selected</span>
        </template>
      </template>
    </BTable>
    <p>
      <BButton size="sm" class="me-2" @click="selectAllRows">Select all</BButton>
      <BButton size="sm" class="me-2" @click="clearSelected">Clear selected</BButton>
      <BButton size="sm" class="me-2" @click="selectThirdRow">Select 3rd row</BButton>
      <BButton size="sm" @click="unselectThirdRow">Unselect 3rd row</BButton>
    </p>
    <p>
      Selected Rows:<br />
      {{ selected }}
    </p>
  </div>
</template>

<script setup lang="ts">
import {ref, useTemplateRef} from 'vue'
import {BTable} from 'bootstrap-vue-next'
import type {ComponentExposed} from 'vue-component-type-helpers'

interface Person {
  first_name: string
  last_name: string
  age: number
  isActive: boolean
}

const selectableTable = useTemplateRef<ComponentExposed<typeof BTable>>('selectable-table')

const modes = ['multi', 'single', 'range']
const fields = ['selected', 'isActive', 'age', 'first_name', 'last_name']
const items: Person[] = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]
const selectMode = ref<'multi' | 'single' | 'range'>('multi')

const selected = ref<Person[]>([])

const onSelectedItems = (selectedItems: readonly Person[]) => {
  selected.value = [...selectedItems]
}

const selectAllRows = () => {
  selectableTable?.value?.selectAllRows()
}
const clearSelected = () => {
  selectableTable?.value?.clearSelected()
}
const selectThirdRow = () => {
  // Rows are indexed from 0, so the third row is index 2
  selectableTable?.value?.selectRow(2)
}
const unselectThirdRow = () => {
  // Rows are indexed from 0, so the third row is index 2
  selectableTable?.value?.unselectRow(2)
}
</script>

Table body transition support

Not yet implemented

Sorting

As mentioned in the Fields section above, you can make columns sortable in <BTable>. Clicking on a sortable column header will sort the column in ascending direction (smallest first), while clicking on it again will switch the direction of sorting to descending (largest first). Clicking on it a third time will stop sorting on the column. For single column sorting (e.g. multisort===false) clicking on a differnt sortable column header will sort that column in ascending order and clear the sort order for the previously sorted column.

You can control which column is pre-sorted and the order of sorting (ascending or descending). To pre-specify the column to be sorted use the sortBy model. For single column sorting (e.g. multisort===false) sortBy should be an array containing a single BTableSortBy object with a defined order field.

ts
type BTableSortByOrder = 'desc' | 'asc' | undefined
// Where T is the type of `items` in the table
type BTableSortByComparerFunction<T = unknown> = (a: T, b: T) => number
type BTableSortBy<T = unknown> = {
  order: BTableSortByOrder
  key: string
  comparer?: BTableSortByComparerFunction<T>
}
  • Ascending: Items are sorted lowest to highest (i.e. A to Z) and will be displayed with the lowest value in the first row with progressively higher values in the following rows.
  • Descending: Items are sorted highest to lowest (i.e. Z to A) and will be displayed with the highest value in the first row with progressively lower values in the following rows.

By default the comparer function does a numeric localeCompare. If one wishes to change this, use a custom comparer function with that BTableSortBy element.

To prevent the table from wiping out the comparer function, internally it will set the order key to undefined, instead of just removing the element from the sortBy array. i.e. :sort-by="[]" & :sort-by="[key: 'someKey', order: undefined]" behave identically. Naturally if this value is given to a server, orders of undefined should be handled. See the computed singleSortBy function below as a simple means of retrieving the single sortded column reference from a table that is in single sort mode.

Last NameFirst NameAgeIs Active
MacdonaldZelda45true
ShawLarsen21false
CarneyJami38true
WilsonGeneva89false
WilsonGary89false
MacdonaldDickerson40true
HTML
vue
<template>
  <BTable :sort-by="[{key: 'first_name', order: 'desc'}]" :items="items" :fields="fields" />
</template>

<script setup lang="ts">
import {type TableFieldRaw, type TableItem} from 'bootstrap-vue-next'

interface SortPerson {
  first_name: string
  last_name: string
  age: number
  isActive: boolean
}

const items: TableItem<SortPerson>[] = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: true, age: 45, first_name: 'Zelda', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: false, age: 89, first_name: 'Gary', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]

const fields: Exclude<TableFieldRaw<SortPerson>, string>[] = [
  {key: 'last_name', sortable: true},
  {key: 'first_name', sortable: true},
  {key: 'age', sortable: true},
  {key: 'isActive', sortable: false},
]
</script>

sorbBy is a named model so it can be bound to an object that will be updated with the current sort state when the user changes sorting by clicking the headers.

Last NameFirst NameAgeIs Active
MacdonaldZelda45true
ShawLarsen21false
CarneyJami38true
WilsonGeneva89false
WilsonGary89false
MacdonaldDickerson40true
sortBy = [{"key":"first_name","order":"desc"}]
singleSortBy = {"key":"first_name","order":"desc"}
HTML
vue
<template>
  <BTable v-model:sort-by="sortBy" :items="items" :fields="fields" />
  <div>sortBy = {{ JSON.stringify(sortBy) }}</div>
  <div>singleSortBy = {{ JSON.stringify(singleSortBy) }}</div>
</template>

<script setup lang="ts">
import {type BTableSortBy, type TableFieldRaw, type TableItem} from 'bootstrap-vue-next'
import {computed, ref} from 'vue'

interface SortPerson {
  first_name: string
  last_name: string
  age: number
  isActive: boolean
}

const items: TableItem<SortPerson>[] = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: true, age: 45, first_name: 'Zelda', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: false, age: 89, first_name: 'Gary', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]

const fields: TableFieldRaw<SortPerson>[] = [
  {key: 'last_name', sortable: true},
  {key: 'first_name', sortable: true},
  {key: 'age', sortable: true},
  {key: 'isActive', sortable: false},
]

const sortBy = ref<BTableSortBy[]>([{key: 'first_name', order: 'desc'}])

const singleSortBy = computed(() => sortBy.value.find((sb) => sb.order !== undefined))
</script>

Tables can be sorted by multiple columns. Programmaticly, this can be done by adding more entries to the sortBy array. From the user inteface, multi-sort works as follows:

  • Clicking on a sortable header that isn't currently sorted adds it as ascending to the end of the sortBy list
  • Clicking on a sortable header that is currently sorted as ascending makes it descending, but leaves it in the same order in the sortBy list
  • Clicking on a sortable header that is currently sorted as descending will set the order to undefined. If must-sort is true OR if mustSort is an array that contains that columns key, it will skip to be ascending
Last NameFirst NameAgeIs Active
CarneyJami38true
MacdonaldDickerson40true
MacdonaldZelda45true
ShawLarsen21false
WilsonGary89false
WilsonGeneva89false
sortBy = [{"key":"last_name","order":"asc"},{"key":"first_name","order":"asc"}]
HTML
vue
<template>
  <BTable v-model:sort-by="multiSortBy" :items="sortItems" :fields="sortFields" :multisort="true" />
  <div>sortBy = {{ JSON.stringify(multiSortBy) }}</div>
</template>

<script setup lang="ts">
import {ref} from 'vue'
import {type BTableSortBy, type TableFieldRaw, type TableItem} from 'bootstrap-vue-next'

interface SortPerson {
  first_name: string
  last_name: string
  age: number
  isActive: boolean
}

const sortItems: TableItem<SortPerson>[] = [
  {isActive: true, age: 40, first_name: 'Dickerson', last_name: 'Macdonald'},
  {isActive: true, age: 45, first_name: 'Zelda', last_name: 'Macdonald'},
  {isActive: false, age: 21, first_name: 'Larsen', last_name: 'Shaw'},
  {isActive: false, age: 89, first_name: 'Geneva', last_name: 'Wilson'},
  {isActive: false, age: 89, first_name: 'Gary', last_name: 'Wilson'},
  {isActive: true, age: 38, first_name: 'Jami', last_name: 'Carney'},
]

const sortFields: TableFieldRaw<SortPerson>[] = [
  {key: 'last_name', sortable: true},
  {key: 'first_name', sortable: true},
  {key: 'age', sortable: true},
  {key: 'isActive', sortable: false},
]

const multiSortBy = ref<BTableSortBy[]>([
  {key: 'last_name', order: 'asc'},
  {key: 'first_name', order: 'asc'},
])
</script>

Custom Sort Comparer(s)

Each item in the BSortBy model may include a comparer field of the type BTableSortByComparerFunction<T = any> = (a: T, b: T, key: string) => number. This function takes the items to be compared and the key to compare on. Since the key is passed in, you may use the same function for multiple fields or you can craft a different comparer function for each fied. Leaving the comparer field undefined (or not defining a field in the sortBy array at all) will fall back to using hte default comparer, which looks like this:

ts
const defaultComparer = (a: unknown, b: unknown): number =>
  getStringValue(a).localeCompare(getStringValue(b), undefined, {numeric: true})

where getStringValue retrieves the field value as a string.

If you have a particular field that you want to sort by, you can set up a record of the sortBy model with a custom comparer:

ts
const removeArticles = (str: string) => str.replace(/^(a |the )/i, '')
const sortBy = [
  {
    key: 'titleField',
    comparer: (a: T, b: T, key: string) =>
      removeArticles(a.titleField).localeCompare(removeArticles(b.titleField)),
  },
]

Filtering

Filtering, when used, is applied by default to the original items array data. Btable provides several options for how data is filtered.

It is currently not possible to filter based on result of formatting via scoped field slots.

Built in filtering

The item's row data values are stringified (see the sorting section above for how stringification is done) and the filter searches that stringified data (excluding any of the special properties that begin with an underscore '_'). The stringification also, by default, includes any data not shown in the presented columns.

With the default built-in filter function, the filter prop value can either be a string or a RegExp object (regular expressions should not have the /g global flag set). Not yet implemented Currently the filter prop only supports a string, not a RegExp.

If the stringified row contains the provided string value or matches the RegExp expression then it is included in the displayed results.

Set the filter prop to null or an empty string to clear the current filter.

Built in filtering options

There are several options for controlling what data the filter is applied against.

  • Not yet implementedThe filter-ignored-fields prop accepts an array of top-level (immediate properties of the rowdata) field keys that should be ignored when filtering.
  • Not yet implementedThe filter-included-fields prop accepts an array of top-level (immediate properties of the rowdata) field keys that should used when filtering. All other field keys not included in this array will be ignored. This feature can be handy when you want to filter on specific columns. If the specified array is empty, then all fields are included, except those specified via the prop filter-ignored-fields. If a field key is specified in both filter-ignored-fields and filter-included-fields, then filter-included-fields takes precedence.
  • Normally, <BTable> filters based on the stringified record data. If the field has a formatter function specified, you can optionally filter based on the result of the formatter by setting the field definition property filterByFormatted to true. If the field does not have a formatter function, this option is ignored. Not yet implementedYou can optionally pass a formatter function reference, to be used for filtering only, to the field definition property filterByFormatted.

The props filter-ignored-fields and filter-included-fields, and the field definition property filterByFormatted have no effect when using a custom filter function, or items provider based filtering.

Custom filter function

You can also use a custom filter function, by setting the prop filter-function to a reference of custom filter test function. The filter function signature is (item: Readonly<Items>, filter: string | undefined) => boolean

  • item is the original item row record data object.
  • filter value of the filter prop

The function should return true if the record matches your criteria or false if the record is to be filtered out.

For proper reactive updates to the displayed data, when not filtering you should set the filter prop to null or an empty string (and not an empty object or array). The filter function will not be called when the filter prop is a falsey value.

The display of the empty-filter-text relies on the truthiness of the filter prop.

Filter events

When local filtering is applied, and the resultant number of items change, <BTable> will emit the filtered event with a single argument of type Items[]: which is the complete list of items passing the filter routine. Treat this argument as read-only.

Setting the prop filter to null or an empty string will clear local items filtering.

Pagination

To Be Completed

Using items provider functions

To Be Completed

Light-weight tables

To Be Completed

Simple tables

The BTableSimple component gives the user complete control over the rendering of the table content, while providing basic Bootstrap v5 table styling. BTableSimple is a wrapper component around the <table> element. Inside the component, via the default slot, you can use any or all of the BootstrapVueNext table helper components: BThead, BTfoot, BTbody, BTr, BTh, BTd, and the HTML5 elements <caption>, <colgroup> and <col>. Contrary to the component's name, one can create simple or complex table layouts with BTableSimple.

BTableSimple provides basic styling options via props: striped, bordered, borderless, outlined, small, hover, dark, fixed, responsive and sticky-header. Note that stacked mode is available but requires some additional markup to generate the cell headings, as described in the Simple tables and stacked mode section below. Sticky columns are also supported, but also require a bit of additional markup to specify which columns are to be sticky. See below for more information on using sticky columns.

Since BTableSimple is just a wrapper component, of which you will need to render content inside, it does not provide any of the advanced features of BTable (i.e. row events, head events, sorting, pagination, filtering, foot-clone, items, fields, etc.).

Items sold in August, grouped by Country and City:
RegionClothesAccessories
CountryCityTrousersSkirtsDressesBraceletsRings
BelgiumAntwerp5622437223
Gent4618506115
Brussels5127386928
The NetherlandsAmsterdam8934698538
Utrecht8012433619
Total Rows: 5
HTML
template
<BTableSimple hover small caption-top responsive>
  <caption>
    Items sold in August, grouped by Country and City:
  </caption>
  <colgroup>
    <col />
    <col />
  </colgroup>
  <colgroup>
    <col />
    <col />
    <col />
  </colgroup>
  <colgroup>
    <col />
    <col />
  </colgroup>
  <BThead variant="dark">
    <BTr>
      <BTh colspan="2">Region</BTh>
      <BTh colspan="3">Clothes</BTh>
      <BTh colspan="2">Accessories</BTh>
    </BTr>
    <BTr>
      <BTh>Country</BTh>
      <BTh>City</BTh>
      <BTh>Trousers</BTh>
      <BTh>Skirts</BTh>
      <BTh>Dresses</BTh>
      <BTh>Bracelets</BTh>
      <BTh>Rings</BTh>
    </BTr>
  </BThead>
  <BTbody>
    <BTr>
      <BTh rowspan="3">Belgium</BTh>
      <BTh class="text-end">Antwerp</BTh>
      <BTd>56</BTd>
      <BTd>22</BTd>
      <BTd>43</BTd>
      <BTd variant="success">72</BTd>
      <BTd>23</BTd>
    </BTr>
    <BTr>
      <BTh class="text-end">Gent</BTh>
      <BTd>46</BTd>
      <BTd variant="warning">18</BTd>
      <BTd>50</BTd>
      <BTd>61</BTd>
      <BTd variant="danger">15</BTd>
    </BTr>
    <BTr>
      <BTh class="text-end">Brussels</BTh>
      <BTd>51</BTd>
      <BTd>27</BTd>
      <BTd>38</BTd>
      <BTd>69</BTd>
      <BTd>28</BTd>
    </BTr>
    <BTr>
      <BTh rowspan="2">The Netherlands</BTh>
      <BTh class="text-end">Amsterdam</BTh>
      <BTd variant="success">89</BTd>
      <BTd>34</BTd>
      <BTd>69</BTd>
      <BTd>85</BTd>
      <BTd>38</BTd>
    </BTr>
    <BTr>
      <BTh class="text-end">Utrecht</BTh>
      <BTd>80</BTd>
      <BTd variant="danger">12</BTd>
      <BTd>43</BTd>
      <BTd>36</BTd>
      <BTd variant="warning">19</BTd>
    </BTr>
  </BTbody>
  <BTfoot>
    <BTr>
      <BTd colspan="7" variant="secondary" class="text-end"> Total Rows: <b>5</b> </BTd>
    </BTr>
  </BTfoot>
</BTableSimple>

When in responsive or sticky-header mode, the <table> element is wrapped inside a <div> element. If you need to apply additional classes or attributes to the <table> element, use the table-classes and table-attrs, respectively.

Simple tables and stacked mode

A bit of additional markup is required on your BTableSimple body cells when the table is in stacked mode. Specifically, BootstrapVueNext uses a special data attribute to create the cell's heading, of which you can supply to BTd or BTh via the stacked-heading prop. Only plain strings are supported (not HTML markup), as we use the pseudo element ::before and css content property.

Here is the same table as above, set to be always stacked, which has the extra markup to handle stacked mode (specifically for generating the cell headings):

Items sold in August, grouped by Country and City:
RegionClothesAccessories
CountryCityTrousersSkirtsDressesBraceletsRings
Belgium (3 Cities)
Antwerp
56
22
43
72
23
Gent
46
18
50
61
15
Brussels
51
27
38
69
28
The Netherlands (2 Cities)
Amsterdam
89
34
69
85
38
Utrecht
80
12
43
36
19
Total Rows: 5
HTML
template
<BTableSimple hover small caption-top stacked>
  <caption>
    Items sold in August, grouped by Country and City:
  </caption>
  <colgroup>
    <col />
    <col />
  </colgroup>
  <colgroup>
    <col />
    <col />
    <col />
  </colgroup>
  <colgroup>
    <col />
    <col />
  </colgroup>
  <BThead variant="dark">
    <BTr>
      <BTh colspan="2">Region</BTh>
      <BTh colspan="3">Clothes</BTh>
      <BTh colspan="2">Accessories</BTh>
    </BTr>
    <BTr>
      <BTh>Country</BTh>
      <BTh>City</BTh>
      <BTh>Trousers</BTh>
      <BTh>Skirts</BTh>
      <BTh>Dresses</BTh>
      <BTh>Bracelets</BTh>
      <BTh>Rings</BTh>
    </BTr>
  </BThead>
  <BTbody>
    <BTr>
      <BTh rowspan="3" class="text-center">Belgium (3 Cities)</BTh>
      <BTh stacked-heading="City" class="text-start">Antwerp</BTh>
      <BTd stacked-heading="Clothes: Trousers">56</BTd>
      <BTd stacked-heading="Clothes: Skirts">22</BTd>
      <BTd stacked-heading="Clothes: Dresses">43</BTd>
      <BTd stacked-heading="Accessories: Bracelets" variant="success">72</BTd>
      <BTd stacked-heading="Accessories: Rings">23</BTd>
    </BTr>
    <BTr>
      <BTh stacked-heading="City">Gent</BTh>
      <BTd stacked-heading="Clothes: Trousers">46</BTd>
      <BTd stacked-heading="Clothes: Skirts" variant="warning">18</BTd>
      <BTd stacked-heading="Clothes: Dresses">50</BTd>
      <BTd stacked-heading="Accessories: Bracelets">61</BTd>
      <BTd stacked-heading="Accessories: Rings" variant="danger">15</BTd>
    </BTr>
    <BTr>
      <BTh stacked-heading="City">Brussels</BTh>
      <BTd stacked-heading="Clothes: Trousers">51</BTd>
      <BTd stacked-heading="Clothes: Skirts">27</BTd>
      <BTd stacked-heading="Clothes: Dresses">38</BTd>
      <BTd stacked-heading="Accessories: Bracelets">69</BTd>
      <BTd stacked-heading="Accessories: Rings">28</BTd>
    </BTr>
    <BTr>
      <BTh rowspan="2" class="text-center">The Netherlands (2 Cities)</BTh>
      <BTh stacked-heading="City">Amsterdam</BTh>
      <BTd stacked-heading="Clothes: Trousers" variant="success">89</BTd>
      <BTd stacked-heading="Clothes: Skirts">34</BTd>
      <BTd stacked-heading="Clothes: Dresses">69</BTd>
      <BTd stacked-heading="Accessories: Bracelets">85</BTd>
      <BTd stacked-heading="Accessories: Rings">38</BTd>
    </BTr>
    <BTr>
      <BTh stacked-heading="City">Utrecht</BTh>
      <BTd stacked-heading="Clothes: Trousers">80</BTd>
      <BTd stacked-heading="Clothes: Skirts" variant="danger">12</BTd>
      <BTd stacked-heading="Clothes: Dresses">43</BTd>
      <BTd stacked-heading="Accessories: Bracelets">36</BTd>
      <BTd stacked-heading="Accessories: Rings" variant="warning">19</BTd>
    </BTr>
  </BTbody>
  <BTfoot>
    <BTr>
      <BTd colspan="7" variant="secondary" class="text-end"> Total Rows: <b>5</b> </BTd>
    </BTr>
  </BTfoot>
</BTableSimple>

Like BTable and BTableLite, table headers (<thead>) and footers (<tfoot>) are visually hidden when the table is visually stacked. If you need a header or footer, you can do so by creating an extra BTr inside of the BTbody component (or in a second BTbody component), and set a role of columnheader on the child BTh cells, and use Bootstrap v5 responsive display utility classes to hide the extra row (or BTbody) above a certain breakpoint when the table is no longer visually stacked (the breakpoint should match the stacked table breakpoint you have set), i.e. <BTr class="d-md-none"> would hide the row on medium and wider screens, while <BTbody class="d-md-none"> would hide the row group on medium and wider screens.

NOTE

Note: stacked mode with BTableSimple requires that you use the BootstrapVueNext table helper components. Use of the regular <tbody>, <tr>, <td> and <th> element tags will not work as expected, nor will they automatically apply any of the required accessibility attributes.

Simple tables and sticky columns

Sticky columns are supported with BTableSimple, but you will need to set the sticky-column prop on each table cell (in the thead, tbody, and tfoot row groups) in the column that is to be sticky. For example:

template
<BTableSimple responsive>
  <BThead>
    <BTr>
      <BTh sticky-column>Sticky Column Header</BTh>
      <BTh>Heading 1</BTh>
      <BTh>Heading 2</BTh>
      <BTh>Heading 3</BTh>
      <BTh>Heading 4</BTh>
    </BTr>
  </BThead>
  <BTbody>
    <BTr>
      <BTh sticky-column>Sticky Column Row Header</BTh>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
    </BTr>
    <BTr>
      <BTh sticky-column>Sticky Column Row Header</BTh>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
      <BTd>Cell</BTd>
    </BTr>
  </BTbody>
  <BTfoot>
    <BTr>
      <BTh sticky-column>Sticky Column Footer</BTh>
      <BTh>Heading 1</BTh>
      <BTh>Heading 2</BTh>
      <BTh>Heading 3</BTh>
      <BTh>Heading 4</BTh>
    </BTr>
  </BTfoot>
</BTableSimple>

As with BTable and BTableLite, sticky columns are not supported when the stacked prop is set on BTableSimple.

Table Helper Components

To Be Completed

Accessibility

To Be Completed

Complete Example

Filter On
Leave all unchecked to filter on all data
Person full namePerson sortable namePerson ageIs ActiveActions
Dickerson MacdonaldMacdonald, Dickerson40Yes
Larsen ShawShaw, Larsen21No
Mini NavarroNavarro, Mini9No
Geneva WilsonWilson, Geneva89No
Jami CarneyCarney, Jami38Yes
HTML
vue
<template>
  <BContainer class="py-5">
    <!-- User Interface controls -->
    <BRow>
      <BCol lg="6" class="my-1">
        <BFormGroup
          v-slot="{ariaDescribedby}"
          label="Sort"
          label-for="sort-by-select"
          label-cols-sm="3"
          label-align-sm="right"
          label-size="sm"
          class="mb-0"
        >
          <BButton size="sm" @click="onAddSort">Add Sort...</BButton>
          <BInputGroup v-for="sort in sortBy" :key="sort.key" size="sm">
            <BFormSelect
              id="sort-by-select"
              v-model="sort.key"
              :options="sortOptions"
              :aria-describedby="ariaDescribedby"
              class="w-75"
            >
              <template #first>
                <option value="">-- none --</option>
              </template>
            </BFormSelect>
            <BFormSelect
              v-model="sort.order"
              :disabled="!sortBy"
              :aria-describedby="ariaDescribedby"
              size="sm"
              class="w-25"
            >
              <option value="asc">Asc</option>
              <option value="desc">Desc</option>
            </BFormSelect>
          </BInputGroup>
        </BFormGroup>
      </BCol>
      <BCol lg="6" class="my-1">
        <BFormGroup
          label="Filter"
          label-for="filter-input"
          label-cols-sm="3"
          label-align-sm="right"
          label-size="sm"
          class="mb-0"
        >
          <BInputGroup size="sm">
            <BFormInput
              id="filter-input"
              v-model="filter"
              type="search"
              placeholder="Type to Search"
            />
            <BInputGroupText>
              <BButton :disabled="!filter" @click="filter = ''">Clear</BButton>
            </BInputGroupText>
          </BInputGroup>
        </BFormGroup>
      </BCol>
      <BCol lg="6" class="my-1">
        <BFormGroup
          v-slot="{ariaDescribedby}"
          v-model="sortDirection"
          label="Filter On"
          description="Leave all unchecked to filter on all data"
          label-cols-sm="3"
          label-align-sm="right"
          label-size="sm"
          class="mb-0"
        >
          <div class="d-flex gap-2">
            <BFormCheckbox v-model="filterOn" value="name" :aria-describedby="ariaDescribedby"
              >Name</BFormCheckbox
            >
            <BFormCheckbox v-model="filterOn" value="age" :aria-describedby="ariaDescribedby"
              >Age</BFormCheckbox
            >
            <BFormCheckbox v-model="filterOn" value="isActive" :aria-describedby="ariaDescribedby"
              >Active</BFormCheckbox
            >
          </div>
        </BFormGroup>
      </BCol>
      <BCol sm="5" md="6" class="my-1">
        <BFormGroup
          label="Per page"
          label-for="per-page-select"
          label-cols-sm="6"
          label-cols-md="4"
          label-cols-lg="3"
          label-align-sm="right"
          label-size="sm"
          class="mb-0"
        >
          <BFormSelect id="per-page-select" v-model="perPage" :options="pageOptions" size="sm" />
        </BFormGroup>
      </BCol>
      <BCol sm="7" md="6" class="my-1">
        <BPagination
          v-model="currentPage"
          :total-rows="totalRows"
          :per-page="perPage"
          :align="'fill'"
          size="sm"
          class="my-0"
        />
      </BCol>
    </BRow>
    <!-- Main table element for typed table-->
    <BTable
      v-model:sort-by="sortBy"
      :sort-internal="true"
      :items="items"
      :fields="fields"
      :current-page="currentPage"
      :per-page="perPage"
      :filter="filter"
      :responsive="false"
      :filterable="filterOn"
      :small="true"
      :multisort="true"
      @filtered="onFiltered"
    >
      <template #cell(name)="row">
        {{ (row.value as PersonName).first }}
        {{ (row.value as PersonName).last }}
      </template>
      <template #cell(actions)="row">
        <BButton size="sm" class="me-1" @click="info(row.item, row.index)"> Info modal </BButton>
        <BButton size="sm" @click="row.toggleDetails">
          {{ row.detailsShowing ? 'Hide' : 'Show' }} Details
        </BButton>
      </template>
      <template #row-details="row">
        <BCard>
          <ul>
            <li v-for="(value, key) in row.item" :key="key">{{ key }}: {{ value }}</li>
            <BButton size="sm" @click="row.toggleDetails"> Toggle Details </BButton>
          </ul>
        </BCard>
      </template>
    </BTable>
    <!-- Info modal -->
    <BModal
      :id="infoModal.id"
      v-model="infoModal.open"
      :title="infoModal.title"
      :ok-only="true"
      @hide="resetInfoModal"
    >
      <pre>{{ infoModal.content }}</pre>
    </BModal>
  </BContainer>
</template>

<script setup lang="ts">
import {
  type BTableSortBy,
  type ColorVariant,
  type LiteralUnion,
  type TableFieldRaw,
  type TableItem,
} from 'bootstrap-vue-next'
import {computed, reactive, ref} from 'vue'

interface PersonName {
  first: string
  last: string
}

interface Person {
  name: PersonName
  age: number
  isActive: boolean
}

const items: TableItem<Person>[] = [
  {isActive: true, age: 40, name: {first: 'Dickerson', last: 'Macdonald'}},
  {isActive: false, age: 21, name: {first: 'Larsen', last: 'Shaw'}},
  {
    isActive: false,
    age: 9,
    name: {first: 'Mini', last: 'Navarro'},
    _rowVariant: 'success' as ColorVariant,
  },
  {isActive: false, age: 89, name: {first: 'Geneva', last: 'Wilson'}},
  {isActive: true, age: 38, name: {first: 'Jami', last: 'Carney'}},
  {isActive: false, age: 27, name: {first: 'Essie', last: 'Dunlap'}},
  {isActive: true, age: 40, name: {first: 'Thor', last: 'Macdonald'}},
  {
    isActive: true,
    age: 87,
    name: {first: 'Larsen', last: 'Shaw'},
    _cellVariants: {age: 'danger', isActive: 'warning'},
  },
  {isActive: false, age: 26, name: {first: 'Mitzi', last: 'Navarro'}},
  {isActive: false, age: 22, name: {first: 'Genevieve', last: 'Wilson'}},
  {isActive: true, age: 38, name: {first: 'John', last: 'Carney'}},
  {isActive: false, age: 29, name: {first: 'Dick', last: 'Dunlap'}},
]

const fields: Exclude<TableFieldRaw<Person>, string>[] = [
  {
    key: 'name',
    label: 'Person full name',
    sortable: true,
    sortDirection: 'desc',
  },
  {
    key: 'sortableName',
    label: 'Person sortable name',
    sortable: true,
    sortDirection: 'desc',
    formatter: (_value: unknown, _key?: LiteralUnion<keyof Person>, item?: Person) =>
      item ? `${item.name.last}, ${item.name.first}` : 'Something went wrong',
    sortByFormatted: true,
    filterByFormatted: true,
  },
  {key: 'age', label: 'Person age', sortable: true, class: 'text-center'},
  {
    key: 'isActive',
    label: 'Is Active',
    formatter: (value: unknown) => (value ? 'Yes' : 'No'),
    sortable: true,
    sortByFormatted: true,
    filterByFormatted: true,
  },
  {key: 'actions', label: 'Actions'},
]

const pageOptions = [
  {value: 5, text: '5'},
  {value: 10, text: '10'},
  {value: 15, text: '15'},
  {value: 100, text: 'Show a lot'},
]

const totalRows = ref(items.length)
const currentPage = ref(1)
const perPage = ref(5)
const sortBy = ref<BTableSortBy[]>([])
const sortDirection = ref('asc')
const filter = ref('')
const filterOn = ref([])
const infoModal = reactive({
  open: false,
  id: 'info-modal',
  title: '',
  content: '',
})

// Create an options list from our fields
const sortOptions = computed(() =>
  fields.filter((f) => f.sortable).map((f) => ({text: f.label, value: f.key}))
)

function info(item: TableItem<Person>, index: number) {
  infoModal.title = `Row index: ${index}`
  infoModal.content = JSON.stringify(item, null, 2)
  infoModal.open = true
}

function resetInfoModal() {
  infoModal.title = ''
  infoModal.content = ''
}

function onFiltered(filteredItems: TableItem<Person>[]) {
  // Trigger pagination to update the number of buttons/pages due to filtering
  totalRows.value = filteredItems.length
  currentPage.value = 1
}

function onAddSort() {
  sortBy.value.push({key: '', order: 'asc'})
}
</script>

Component Reference

PropTypeDefaultDescription
busybooleanfalse
busy-loading-textstring'Loading...'
current-pageNumberish1
empty-filtered-textstring'There are no records matching your request' Text to display when no items are present in the `items` array after filtering
empty-textstring'There are no records to show' Text to display when no items are present in the `items` array
filterstringundefined
filter-function(item: Readonly<Items>, filter: string | undefined) => booleanundefined Function called during filtering of items, gets passed the current item being filtered
filterablestring[]undefined
multisortbooleanfalse
must-sortboolean | string[]false
no-local-sortingbooleanfalse
no-providerNoProviderTypes[]undefined
no-provider-filteringbooleanfalse
no-provider-pagingbooleanfalse
no-provider-sortingbooleanfalse
no-select-on-clickbooleanfalse Do not select row when clicked
no-sortable-iconbooleanfalse
per-pageNumberishnull
providerBTableProviderundefined
select-headboolean | stringtrue
select-mode'multi' | 'single' | 'range''multi'
selectablebooleanfalse
selected-itemsTableItem[]undefined
selection-variantColorVariant | null'primary'
show-emptybooleanfalse Show the empty text when no items are present in the `items` array
sort-byBTableSortBy[]undefined Model representing the current sort state
sticky-selectbooleanfalse
Extensions:
EventArgsDescription
headClicked
key: TableField<Record<string, unknown>>.key: LiteralUnion<string, string>
field: TableField
event: MouseEvent
is-footer: boolean
rowClicked
item: TableItem
index: number
event: MouseEvent
rowDblClicked
item: TableItem
index: number
event: MouseEvent
rowHovered
item: TableItem
index: number
event: MouseEvent
rowSelected
row-selected: TableItem
rowUnhovered
item: TableItem
index: number
event: MouseEvent
rowUnselected
row-unselected: TableItem
selection
selection: TableItem[]
sorted
value: BTableSortBy
Updated when the user clicks a sortable column heading and represents the column click and the sort state (`asc`, `desc`, or undefined)
update:sortBy
value: string - BTableSortBy[] | undefined
Emitted when the `sortBy` model is changed and represents the current sort state
NameScopeDescription
custom-foot
default
emptyContent to display when no items are present in the `items` array
empty-filteredContent to display when no items are present in the `items` array after filtering
row-details
select-head
selectCell
table-busy
table-caption
table-caption
thead-sub
thead-top
<BTableLite>
PropTypeDefaultDescription
alignVerticalAlignundefined
captionstringundefined
details-td-classClassValueundefined
field-column-class(field: TableField) => Record<string, any>[] | string | Record<PropertyKey, any> | any[]undefined
fieldsTableFieldRaw[]'() => []'
foot-clonebooleanfalse
foot-row-variantColorVariant | nullundefined
foot-variantColorVariant | nullundefined
head-row-variantColorVariant | nullundefined
head-variantColorVariant | nullundefined
itemsTableItem[]'() => []'
label-stackedbooleanfalse When set, the labels will appear as actual label elements, rather than with the data-label attribute
model-valueanyundefined
primary-keystringundefined
tbody-classClassValueundefined
tbody-tr-attrsClassValueundefined
tbody-tr-class((item: TableItem | null, type: string) => string | any[] | null | undefined) | string | Record<PropertyKey, any> | any[]undefined
tfoot-classClassValueundefined
tfoot-tr-classClassValueundefined
thead-classClassValueundefined
thead-tr-classClassValueundefined
Extensions:
<BTableSimple>
PropTypeDefaultDescription
border-variantColorVariant | nullnull Applies one of the Bootstrap theme color variants to the table border
borderedbooleanfalse Adds borders to all the cells and headers
borderlessbooleanfalse Removes all borders from cells
caption-topbooleanfalse When set, the table caption will appear above the table
darkbooleanfalse Places the table in dark mode
fixedbooleanfalse Makes all columns equal width (fixed layout table). Will speed up rendering for large tables. Column widths can be set via CSS or colgroup
hoverbooleanfalse Enables hover styling on rows
idstringundefined Used to set the `id` attribute on the rendered content, and used as the base to generate any additional element IDs as needed
no-border-collapsebooleanfalse Disable's the collapsing of table borders. Useful when table has sticky headers or columns
outlinedbooleanfalse Adds an outline border to the table element
responsiveboolean | Breakpointfalse Makes the table responsive in width, adding a horizontal scrollbar. Set to true for always responsive or set to one of the breakpoints to switch from responsive to normal: 'sm', 'md', 'lg', 'xl'
smallbooleanfalse Renders the table with smaller cell padding
stackedboolean | Breakpointfalse Place the table in stacked mode. Set to true for always stacked, or set to one of the breakpoints to switch from stacked to normal: 'sm', 'md', 'lg', 'xl'
sticky-headerboolean | Numberishfalse Makes the table header sticky. Set to true for a maximum height 300px tall table, or set to any valid CSS height (including units). Inputting a number type is converted to px height
stripedbooleanfalse Applies striping to the tbody rows
striped-columnsbooleanfalse Applies striping to the table columns
table-attrsAttrsValueundefined Attributes to apply to the table element
table-classClassValueundefined Classes to apply to the table element
variantColorVariant | nullnull Applies one of the Bootstrap theme color variants to the component. When implemented `bg-variant` and `text-variant` will take precedence
NameScopeDescription
defaultContent to place in the table
PropTypeDefaultDescription
variantColorVariantnull
NameScopeDescription
default
PropTypeDefaultDescription
colspanstring | numberundefined
rowspanstring | numberundefined
stacked-headingstringundefined
sticky-columnbooleanfalse
variantColorVariant | nullnull
NameScopeDescription
default
PropTypeDefaultDescription
variantColorVariant | nullnull
NameScopeDescription
default
PropTypeDefaultDescription
colspanstring | numberundefined
rowspanstring | numberundefined
stacked-headingstringundefined
sticky-columnbooleanfalse
variantColorVariant | nullnull
NameScopeDescription
default
PropTypeDefaultDescription
variantColorVariantnull
NameScopeDescription
default
PropTypeDefaultDescription
variantColorVariantnull
NameScopeDescription
default