Table (TTable)

VueJs reactive HTML Table component with configurable classes, variants, and most common events. Friendly with utility-first frameworks like TailwindCSS..

Playground:


Basic example

<t-table
  :headers="['Name', 'Email', 'Age', 'Sales']"
  :data="[
    ['Alfonso Bribiesca', 'alfonso@vexilo.com', '31', '$9,999.00'],
    ['Saida Redondo', 'saida@gmail.com', 27, '$124.00'],
  ]"
></t-table>
NameEmailAgeSales
Alfonso Bribiescaalfonso@vexilo.com31$9,999.00
Saida Redondosaida@gmail.com27$124.00

Props

PropertyTypeDefault valueDescription
dataArray[]A multidimensional array with rows and columns or an array of objects
headersArray[]An array of strings or an array of objects with different attributes (explained above)
footerDataArray[]An array of strings or an array of objects with different attributes (explained above)
responsive (experimental)BooleanfalseWhen set the header will be hidden and the slots will have a renderResponsive prop variable with the value of true when the screen is smaller than the responsiveBreakpoint option, you can use that variable to define a custom layout in smaller screens. Check the example below
responsiveBreakpoint (experimental)Number768When the screen is smaller to the value the responsiveBreakpoint slot prop will set to true

Classes and variants format

This component expects an object with classes named after every child element.

The properties in that object are the following:

PropertyDescription
tableTable
theadthead tag
theadTrTr tag inside thead
theadThth tag inside thead
tbodytbody tag
trtr tag
tdtd tag
tfoottfoot tag
tfootTrtr tag inside the tfoot
tfootTdtd tag inside tfoot

Default classes

{
  table: 'min-w-full divide-y divide-gray-100 shadow-sm border-gray-200 border',
  thead: '',
  theadTr: '',
  theadTh: 'px-3 py-2 font-semibold text-left bg-gray-100 border-b',
  tbody: 'bg-white divide-y divide-gray-100',
  tr: '',
  td: 'px-3 py-2 whitespace-no-wrap',
  tfoot: '',
  tfootTr: '',
  tfootTd: '',
}

Example theme

{
  classes: {
    table: 'min-w-full divide-y divide-gray-100 shadow-sm border-gray-200 border',
    thead: '',
    theadTr: '',
    theadTh: 'px-3 py-2 font-semibold text-left bg-gray-100 border-b',
    tbody: 'bg-white divide-y divide-gray-100',
    tr: '',
    td: 'px-3 py-2 whitespace-no-wrap',
    tfoot: '',
    tfootTr: '',
    tfootTd: ''
  },
  variants: {
    thin: {
      td: 'p-1 whitespace-no-wrap text-sm',
      theadTh: 'p-1 font-semibold text-left bg-gray-100 border-b text-sm'
    }
  }
}

Headers data

Array of strings

<t-table :headers="['Name', 'Email', 'Sales']" />
<table>
  <thead>
    <tr><th>Name</th></tr>
    <tr><th>Email</th></tr>
    <tr><th>Sales</th></tr>
  </thead>
  ...
</table>

Array of objects

<t-table :headers="[
    {
      id: 'name-id',
      value: 'name',
      text: 'The name',
      className: 'bg-red-200',
    },
    {
      id: 'email-id',
      value: 'email',
      text: 'E-mail',
      className: 'bg-blue-700',
    }
  ]"
/>
<table>
  <thead>
    <tr><th id="name-id" class="bg-red-200">The name</th></tr>
    <tr><th id="email-id" class="bg-blue-700">E-mail</th></tr>
  </thead>
  ...
</table>

*Note: The className attribute will be merged to the theme theadTh className.

Table Data

Multidimensional array of items

<t-table :data="[
  ['Alfonso Bribiesca', 'alfonso@vexilo.com', '$9,999.00'],
  ['Saida Redondo', 'saida@gmail.com', '$124.00'],
  ['Regina Bribiesca', 'regina@gmail.com', '$0.00']
]" />
<table>
  ...
  <tbody>
    <tr><td>Alfonso Bribiesca</td><td>alfonso@vexilo.com</td><td>$9,999.00</td></tr>
    <tr><td>Saida Redondo</td><td>saida@gmail.com</td><td>$124.00</td></tr>
    <tr><td>Regina Bribiesca</td><td>regina@gmail.com</td><td>$0.00</td></tr>
  </tbody>
  ...
</table>

Array of objects

<t-table :data="[
    {
      id: 1,
      name: 'Alfonso Bribiesca',
      email: 'alfonso@vexilo.com',
      sales: '$9,999.00',
    },
    {
      id: 2,
      name: 'Saida Redondo',
      email: 'saida@gmail.com',
      sales: '$124.00',
    },
  ]"
/>
<table>
  ...
  <tbody>
    <tr><td>1</td><td>Alfonso Bribiesca</td><td>alfonso@vexilo.com</td><td>$9,999.00</td></tr>
    <tr><td>2</td><td>Saida Redondo</td><td>saida@gmail.com</td><td>$124.00</td></tr>
  </tbody>
  ...
</table>

Headers value attribute

When you use the value attribute in the headers items together with data objects the datatable will render only the attributes that are in the headers.

Example:

With value attribute:

<t-table
  :headers="[{value: 'name', text: 'Name'}, {value: 'email', text: 'E-mail'}]"
  :data="[
    {id: 1, name: 'Alfonso', email: 'alfonso@vexilo.com'},
    {id: 2, name: 'Saida', email: 'saida@gmail.com'},
  ]"
/>
NameE-mail
Alfonsoalfonso@vexilo.com
Saidasaida@gmail.com

Without value attribute:

<t-table
  :headers="['Name', 'E-mail']"
  :data="[
    {id: 1, name: 'Alfonso', email: 'alfonso@vexilo.com'},
    {id: 2, name: 'Saida', email: 'saida@gmail.com'},
  ]"
/>
NameE-mail
1Alfonsoalfonso@vexilo.com
2Saidasaida@gmail.com

Column slot

When rendering the table you can use the column scoped slot to render custom HTML per column, this is useful if you want to wrap the items in an HTML tag or you want to add a custom attribute to every cell.

Use:

<t-table>
  <template slot="column" slot-scope="props">
    <td :class="props.tdClass">{{ props.text }}</td>
  </template>
</t-table>

The slot props contain the following data:

PropDescription
props.textThe text of the cell
props.rowIndexThe current row index
props.tdClassThe tbody > td theme class in case you want to re-apply it

Example

<t-table
  :headers="['name', 'email']"
  :data="[
    {
      name: 'Alfonso Bribiesca',
      email: 'alfonso@vexilo.com',
    },
    {
      name: 'Saida Redondo',
      email: 'saida@gmail.com',
    },
]"
>
  <template slot="column" slot-scope="props">
    <td :class="[props.tdClass, 'bg-yellow-100 text-sm text-center']"><strong>{{ props.text }}</strong></td>
  </template>
</t-table>

nameemail
Alfonso Bribiescaalfonso@vexilo.com
Saida Redondosaida@gmail.com

Row slot

When rendering the table you can use the row scoped slot to render custom HTML per row. This is useful if you want to control all the HTML inside every row, define your custom columns, add striped classes, etc.

Use:

<t-table>
  <template v-slot:row="props">
    <tr :class="[props.trClass, props.rowIndex % 2 === 0 ? 'bg-gray-100' : '']">
      <td :class="props.tdClass">{{ props.row.name }}</td>
      <td :class="props.tdClass">{{ props.row.email }}</td>
      <td :class="props.tdClass">{{ props.row.etc }}</td>
    </tr>
  </template>
</t-table>

The slot props contain the following data:

PropDescription
props.rowThe full row Object or Array
props.rowIndexThe current row index
props.trClassThe tbody > tr theme class in case you want to re-apply it
props.tdClassThe tbody > td theme class in case you want to re-apply it

Example

<t-table
  :headers="['Name', 'Email', 'Sales', 'Actions']"
  :data="[
    {
      name: 'Alfonso Bribiesca',
      email: 'alfonso@vexilo.com',
      sales: 9999,
    },
    {
      name: 'Saida Redondo',
      email: 'saida@gmail.com',
      sales: 1500
    },
    {
      name: 'Regina Bribiesca',
      email: 'regina@gmail.com',
      sales: -200.50
    },
    {
      name: 'Ricardo Martinez',
      email: 'rickyrickky@gmail.com',
      sales: 0.0
    },
]"
>
  <template slot="row" slot-scope="props">
    <tr :class="[props.trClass, props.rowIndex % 2 === 0 ? 'bg-gray-100' : '']">
      <td :class="props.tdClass">
        {{ props.row.name }}
      </td>
      <td :class="props.tdClass">
        <a :href="`mailto: ${props.row.email}`">{{ props.row.email }}</a>
      </td>
      <td :class="props.tdClass">
        <span :class="{'text-green-500': props.row.sales >= 0, 'text-red-500': props.row.sales < 0 }">
        ${{ props.row.sales.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,') }}
        </span>
      </td>
      <td :class="props.tdClass">
        <t-button variant="secondary">Edit</t-button>
      </td>
    </tr>
  </template>
</t-table>
NameEmailSalesActions
Alfonso Bribiesca alfonso@vexilo.com $9,999.00
Saida Redondo saida@gmail.com $1,500.00
Regina Bribiesca regina@gmail.com $-200.50
Ricardo Martinez rickyrickky@gmail.com $0.00

Tbody slot

You can use the tbody scoped slot to override the default tbody element with your custom HTML. This is useful if you want to add a message when no data, a busy message when you are fetching the info or any custom HTML.

Use:

<t-table>
  <template slot="tbody" slot-scope="props">
    <tbody :class="props.tbodyClass">
      <tr :class="props.trClass">
        <td :class="props.tdClass" colspan="3">
          <p>My custom HTML</p>
        </td>
      </tr>
    </tbody>
  </template>
</t-table>

The slot props contain the following data:

PropDescription
props.dataThe data of the component (normalized)
props.headersThe data of the headers
props.tbodyClassThe tbody theme class in case you want to re-apply it
props.trClassThe tbody > tr theme class in case you want to re-apply it
props.tdClassThe tbody > td theme class in case you want to re-apply it

Example

<!-- In the practice `[]` could be a variable with your data -->
<t-table
  :headers="['Name', 'Email', 'Sales']"
  :data="[]"
>
  <template v-if="![].length" v-slot:tbody="props">
    <tbody :class="props.tbodyClass">
      <tr :class="[props.trClass, 'text-center']">
        <td :class="props.tdClass" colspan="3">
          <t-alert show :dismissible="false" variant="error">
            No data was found! <button class="underline">
              Create your first item
            </button>
          </t-alert>
        </td>
      </tr>
    </tbody>
  </template>
</t-table>
NameEmailSales
No data was found!

Thead slot

You can use the thead scoped slot to override the default thead element with your custom HTML.

Use:

<t-table>
  <template slot="thead" slot-scope="props">
    <thead :class="props.theadClass">
      <tr :class="props.trClass">
        <th 
          v-for="(item, index) in props.data" 
          :class="props.thClass"
        >{{ item.text }}</th>
      </tr>
    </thead>
  </template>
</t-table>

The slot props contains the following data:

PropDescription
props.dataThe data of the headers
props.tbodyClassThe thead theme class in case you want to re-apply it
props.trClassThe thead > tr theme class in case you want to re-apply it
props.thClassThe thead > td theme class in case you want to re-apply it

Example

<t-table
  :headers="['Name', 'Email', 'Age', 'Sales']"
  :data="[
    ['Alfonso Bribiesca', 'alfonso@vexilo.com', '31', '$9,999.00'],
    ['Saida Redondo', 'saida@gmail.com', 27, '$124.00'],
  ]"
>
  <template v-slot:thead="props">
    <thead :class="props.theadClass">
      <tr :class="props.trClass">
        <th
          v-for="(item, index) in props.data"
          :key="index"
          :class="[props.thClass, `bg-yellow-${index+1}00`]"
        >
          {{ item.text }}
        </th>
      </tr>
    </thead>
  </template>
</t-table>
Name Email Age Sales
Alfonso Bribiescaalfonso@vexilo.com31$9,999.00
Saida Redondosaida@gmail.com27$124.00

Tfoot slot

You can use the tfoot scoped slot to override the default tbody element with your custom HTML.

Use:

<t-table>
  <template v-slot:tfoot="props">
    <tfoot :class="props.tfootClass">
      <tr :class="props.trClass">
        <th 
          v-for="(item, index) in props.data" 
          :class="props.tdClass"
        >{{ item.text }}</th>
      </tr>
    </tfoot>
  </template>
</t-table>

The slot props contain the following data:

PropDescription
props.dataThe data of the footer
props.headersThe data of the headers
props.tbodyClassThe tfoot theme class in case you want to re-apply it
props.trClassThe tfoot > tr theme class in case you want to re-apply it
props.tdClassThe tfoot > td theme class in case you want to re-apply it

Example

<t-table
  :headers="['Name', 'Email', 'Age', 'Sales']"
  :footer-data="['', '', 29, '$10,123']"
  :data="[
    ['Alfonso Bribiesca', 'alfonso@vexilo.com', '31', '$9,999.00'],
    ['Saida Redondo', 'saida@gmail.com', 27, '$124.00'],
  ]"
>
  <template slot="tfoot" slot-scope="props">
    <tfoot :class="props.tfootClass">
      <tr :class="[props.trClass, 'bg-gray-200']">
        <td colspan="3" :class="[props.tdClass, 'text-right']">
          <strong>Total:</strong>
        </td>
        <td>
          <strong class="text-lg">{{ props.data[3].text }}</strong>
        </td>
      </tr>
    </tfoot>
  </template>
</t-table>
NameEmailAgeSales
Alfonso Bribiescaalfonso@vexilo.com31$9,999.00
Saida Redondosaida@gmail.com27$124.00
Total: $10,123

Responsive table (experimental)

When you set the responsive option and the screen is smaller than the responsiveBreakpoint option (default to 768) the header will be hidden and the rest of the slots will have a renderResponsive prop variable with the value of true.

You can use that variable to render custom layouts for mobile devices.

Check the following full working example:

Example:

Resize the view

<t-table
  :headers="['Name', 'E-mail', 'Status', '']"
  :data="[
    {
      id: 1,
      name: 'Alfonso Bribiesca',
      email: 'alfonso@vexilo.com',
      is_approved: true,
    },
    {
      id: 2,
      name: 'Saida Redondo',
      email: 'saida@gmail.com',
      is_approved: false,
    },
  ]"
  :responsive="true"
  :responsive-breakpoint="520"
>
  <template slot="tbody" slot-scope="{ tbodyClass, trClass, tdClass, thClass, renderResponsive, data }">
    <template v-if="renderResponsive">
      <tbody
        v-for="(row, rowIndex) in data"
        :key="rowIndex"
        :class="[tbodyClass, rowIndex % 2 === 0 ? 'bg-gray-100' : '']"
      >
        <tr :class="trClass">
          <th class="text-sm font-semibold text-gray-600 uppercase">
            Name
          </th>
          <td :class="[tdClass, 'relative']">
            <t-dropdown class="absolute top-0 right-0">
              <template slot="button">
                <svg version="1.1" viewBox="0 0 16 16" class="text-gray-600 fill-current svg-icon svg-fill" heigth="20" style="width: 20px;"><path pid="0" d="M13 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 13 7zM8 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 8 7zM3 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 3 7z" /></svg>
              </template>
              <button
                class="block w-full px-4 py-2 text-left text-gray-800 hover:text-white hover:bg-blue-500"
              >
                Edit
              </button>
              <button
                class="block w-full px-4 py-2 text-left text-gray-800 hover:text-white hover:bg-blue-500"
              >
                Delete
              </button>
            </t-dropdown>
            {{ row.name }}
          </td>
        </tr>
        <tr :class="trClass">
          <th class="text-sm font-semibold text-gray-600 uppercase">
            Email
          </th>
          <td :class="[tdClass, 'td-overflow']">
            <a
              :href="`mailto: ${row.email}`"
              class="text-gray-600 hover:underline"
            >{{ row.email }}</a>
          </td>
        </tr>
        <tr :class="trClass">
          <th class="text-sm font-semibold text-gray-600 uppercase">
            Status
          </th>
          <td :class="[tdClass]">
            <span
              v-if="row.is_approved"
              class="px-5 py-1 text-sm font-bold text-green-900 bg-green-200 rounded-full d-flex"
            >
              Active
            </span>
            <span
              v-else
              class="px-5 py-1 text-sm font-bold text-gray-900 bg-gray-200 rounded-full d-flex"
            >
              Inactive
            </span>
          </td>
        </tr>
      </tbody>
    </template>
  </template>
  <template slot="row" slot-scope="{ trClass, tdClass, rowIndex, row }">
    <tr :class="[trClass, rowIndex % 2 === 0 ? 'bg-gray-100' : '']">
      <td :class="[tdClass, 'w-full']">
        {{ row.name }}
      </td>
      <td :class="tdClass">
        <a
          :href="`mailto: ${row.email}`"
          class="text-gray-600 hover:underline"
        >{{ row.email }}</a>
      </td>
      <td :class="[tdClass, 'text-center']">
        <span
          v-if="row.is_approved"
          class="px-5 py-2 text-sm font-bold text-green-900 bg-green-200 rounded-full d-flex"
        >
          Active
        </span>
        <span
          v-else
          class="px-5 py-2 text-sm font-bold text-gray-900 bg-gray-200 rounded-full d-flex"
        >
          Inactive
        </span>
      </td>
      <td :class="[tdClass, 'text-right']">
        <t-dropdown>
          <template slot="button">
            <svg version="1.1" viewBox="0 0 16 16" class="text-gray-600 fill-current svg-icon svg-fill" heigth="20" style="width: 20px;"><path pid="0" d="M13 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 13 7zM8 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 8 7zM3 7a2 2 0 1 1 .001 3.999A2 2 0 0 1 3 7z" /></svg>
          </template>
          <button
            class="block w-full px-4 py-2 text-left text-gray-800 hover:text-white hover:bg-blue-500"
          >
            Edit
          </button>
          <button
            class="block w-full px-4 py-2 text-left text-gray-800 hover:text-white hover:bg-blue-500"
          >
            Delete
          </button>
        </t-dropdown>
      </td>
    </tr>
  </template>
  <template slot="tfoot" slot-scope="{ tfootClass, trClass, tdClass, renderResponsive }">
    <tfoot :class="tfootClass">
      <tr :class="trClass">
        <td
          :class="tdClass"
          :colspan="renderResponsive ? 2 : 4"
        >
          <t-pagination
            :hide-prev-next-controls="renderResponsive"
            :total-items="100"
            :per-page="renderResponsive ? 3 : 5"
            :class="{'ml-auto': !renderResponsive, 'mx-auto': renderResponsive}"
          />
        </td>
      </tr>
    </tfoot>
  </template>
</t-table>

Sign up for our newsletter

Stay up-to-date on news and updates about this project by email.

I will never spam or share your email under any circustance.