<div class="row no-gutters">
<div class="grid">
<div class="table-container js-table-container">
<table class="m-table js-table m-table--columns">
<caption>Pricing plans overview</caption>
<thead>
<tr>
<th>Plan</th>
<th>Monthly cost</th>
<th>Bandwidth</th>
<th>Domains included</th>
<th>Contract</th>
</tr>
</thead>
<tbody>
<tr>
<td data-row-header="true">
<a href="#">Starter</a>
</td>
<td>99 <small>kr/mån</small></td>
<td>100 <small>Mbit</small></td>
<td>1 <small>domain</small></td>
<td>Rolling</td>
</tr>
<tr>
<td data-row-header="true">
<a href="#">Growth</a>
</td>
<td>149 <small>kr/mån</small></td>
<td>1 000 <small>Mbit</small></td>
<td>10 <small>domains</small></td>
<td>12 <small>months</small></td>
</tr>
<tr>
<td data-row-header="true">
<a href="#">Business</a>
</td>
<td>349 <small>kr/mån</small></td>
<td>10 000 <small>Mbit</small></td>
<td>100 <small>domains</small></td>
<td>12 <small>months</small></td>
</tr>
<tr>
<td data-row-header="true">
<a href="#">Enterprise</a>
</td>
<td>Custom <small>pricing</small></td>
<td>Tailored <small>capacity</small></td>
<td>Unlimited <small>domains</small></td>
<td>By agreement</td>
</tr>
</tbody>
<tfoot>
<tr>
<td>Support</td>
<td>Email</td>
<td>Priority email</td>
<td>Dedicated manager</td>
<td>SLA included</td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
<div class="row no-gutters">
<div class="grid">
{{#if scrollable}}
<div class="table-scroll-wrapper js-table-scroll-wrapper scroll-on-large u-m-b-double">
<div class="scroll-indicator"></div>
{{/if}}
<div class="table-container js-table-container">
<table class="m-table js-table{{#if modifier}} {{modifier}}{{/if}}">
{{#unless increment}}
{{#if caption}}
<caption>{{caption}}</caption>
{{/if}}
<thead>
<tr>
{{#each headers}}
<th>{{this}}</th>
{{/each}}
</tr>
</thead>
{{/unless}}
<tbody>
{{#each rows}}
<tr>
{{#if ../increment}}<th> </th>{{/if}}
<td data-row-header="true">
{{#if href}}
<a href="{{href}}">{{title}}</a>
{{else}}
{{title}}
{{/if}}
</td>
{{#each cells}}
<td>{{{this}}}</td>
{{/each}}
</tr>
{{/each}}
</tbody>
{{#unless increment}}
<tfoot>
<tr>
{{#each footer}}
<td>{{this}}</td>
{{/each}}
</tr>
</tfoot>
{{/unless}}
</table>
</div>
{{#if scrollable}}
</div>
</div>
{{/if}}
</div>
</div>
{
"headers": [
"Plan",
"Monthly cost",
"Bandwidth",
"Domains included",
"Contract"
],
"rows": [
{
"title": "Starter",
"href": "#",
"cells": [
"99 <small>kr/mån</small>",
"100 <small>Mbit</small>",
"1 <small>domain</small>",
"Rolling"
]
},
{
"title": "Growth",
"href": "#",
"cells": [
"149 <small>kr/mån</small>",
"1 000 <small>Mbit</small>",
"10 <small>domains</small>",
"12 <small>months</small>"
]
},
{
"title": "Business",
"href": "#",
"cells": [
"349 <small>kr/mån</small>",
"10 000 <small>Mbit</small>",
"100 <small>domains</small>",
"12 <small>months</small>"
]
},
{
"title": "Enterprise",
"href": "#",
"cells": [
"Custom <small>pricing</small>",
"Tailored <small>capacity</small>",
"Unlimited <small>domains</small>",
"By agreement"
]
}
],
"footer": [
"Support",
"Email",
"Priority email",
"Dedicated manager",
"SLA included"
],
"modifier": "m-table--columns",
"caption": "Pricing plans overview",
"scrollable": false
}
@charset "UTF-8";
@use '../../configurations/mixins' as mixin;
@use '../../configurations/bem' as bem;
@use '../../configurations/config' as config;
@use '../../configurations/variables' as var;
@use '../../configurations/functions' as func;
@use '../../configurations/colors/colors' as colors;
@use '../../configurations/colors/colors-functions' as colorFunc;
@use '../../vendor/grid/breakpoints' as breakpoint;
@mixin table() {
width: 100%;
border-collapse: collapse;
border: 0;
counter-reset: table-counter;
&[aria-hidden="true"] {
display: none;
}
caption {
font-family: var.$font-family-headings;
padding-top: func.rhythm(1);
padding-bottom: func.rhythm(1);
}
thead,
tfoot {
font-family: var.$font-family-mono;
font-size: 85%;
text-transform: uppercase;
}
tfoot {
th,
td {
border-top: 2px solid colors.$color-granit;
font-family: var.$font-family-headings;
font-size: 90%;
&::before {
display: none;
}
@include breakpoint.bp-up(lg) {
font-size: 100%;
}
}
td {
border-top-width: 1px;
}
}
tbody {
font-weight: normal;
th {
border-bottom-width: 1px;
}
}
th {
font-weight: normal;
}
th,
td {
padding-top: func.rhythm(1);
padding-right: func.rhythm(2);
padding-bottom: func.rhythm(1);
padding-left: func.rhythm(2);
border-bottom: 2px solid colors.$color-granit;
color: var(--cyberspace-color);
font-size: 90%;
text-align: left;
&:first-child {
text-align: left;
}
span:not(.block-icon) {
display: block;
font-weight: 400;
}
a {
white-space: nowrap;
}
p {
display: inline;
}
@include breakpoint.bp-up(lg) {
font-size: 100%;
}
}
td {
border-bottom-width: 1px;
}
}
@include bem.b(table-container) {
position: relative;
width: 100%;
max-width: 1320px;
margin-right: auto;
margin-left: auto;
&:focus {
outline: none;
}
}
@keyframes xPulse {
0% {
transform: translateX(0);
opacity: 1;
}
50% {
transform: translateX(#{func.to_rem(6px)});
opacity: 0.55;
}
100% {
transform: translateX(0);
opacity: 1;
}
}
@include mixin.molecule(table) {
@include table();
@include bem.m(columns) {
th {
border-bottom: 2px solid colors.$color-granit;
}
td {
border-bottom: 1px solid colors.$color-granit;
}
th,
td {
&:nth-child(even) {
background-color: var(--snow-color);
}
}
}
@include bem.m(rows) {
thead {
th {
background-color: var(--ash-color);
}
}
tbody {
tr:nth-child(odd) {
th,
td {
background-color: var(--snow-color);
}
}
tr:nth-child(even) {
th,
td {
background-color: var(--ash-color);
}
}
}
}
@include bem.m(align-text-center) {
th,
td {
text-align: center;
&:first-child {
text-align: left;
}
}
}
@include bem.m(increment) {
tbody {
tr:nth-child(odd) {
th,
td {
background-color: var(--ash-color);
}
}
tr:nth-child(even) {
th,
td {
background-color: var(--snow-color);
}
}
}
th {
width: func.rhythm(4);
padding-right: func.rhythm(3);
&::before {
content: counter(table-counter);
counter-increment: table-counter;
}
}
}
@include bem.m(lines) {
tbody {
tr {
background-image:
linear-gradient(
to right,
colors.$color-cyberspace,
colors.$color-cyberspace 2px,
transparent 2px,
transparent 6px
);
background-repeat: repeat-x;
background-position: bottom left;
background-size: 6px 1px;
}
td,
th {
padding-top: func.rhythm(2);
padding-bottom: func.rhythm(2);
border: 0;
}
}
tfoot {
tr {
background-image: none;
th,
td {
border: 0;
}
}
}
}
@include bem.m(stacked) {
@include breakpoint.bp-down(sm) {
thead,
tfoot {
display: none;
}
tbody {
display: grid;
}
tr {
display: block;
background-color: var(--snow-color);
}
td,
th {
display: block;
padding-top: func.rhythm(1.25);
padding-right: func.rhythm(1.5);
padding-bottom: func.rhythm(1.25);
padding-left: func.rhythm(1.5);
border-bottom-width: 1px;
}
td::before,
th::before {
content: attr(data-label);
display: block;
margin-bottom: func.to_rem(4px);
font-family: var.$font-family-mono;
font-size: 75%;
letter-spacing: 0.04em;
text-transform: uppercase;
opacity: 0.75;
}
[data-row-header="true"] {
padding-top: func.rhythm(1.5);
padding-bottom: func.rhythm(1.5);
border-bottom-width: 2px;
background-color: var(--ash-color);
font-family: var.$font-family-headings;
font-size: 100%;
&::before {
margin-bottom: func.to_rem(6px);
}
}
a {
white-space: normal;
overflow-wrap: anywhere;
}
span:not(.block-icon) {
display: inline;
}
}
}
@include bem.m(sticky-first) {
border-collapse: separate;
border-spacing: 0;
thead,
tbody,
tfoot {
> tr > :first-child {
position: sticky;
left: 0;
z-index: func.z_index(background);
background-color: var(--snow-color);
}
}
thead {
> tr > :first-child {
z-index: func.z_index(foreground);
background-color: var(--ash-color);
}
}
tbody {
tr:nth-child(odd) > :first-child {
background-color: var(--snow-color);
}
tr:nth-child(even) > :first-child {
background-color: var(--ash-color);
}
}
}
}
@include bem.b(scroll-indicator) {
display: none;
position: absolute;
z-index: func.z_index(foreground);
top: func.to_rem(4px);
right: func.to_rem(4px);
align-items: center;
gap: func.to_rem(4px);
padding: func.to_rem(4px) func.to_rem(8px);
border-radius: func.rhythm(2);
background-color: rgba(colors.$color-cyberspace, 0.92);
color: colors.$color-snow;
pointer-events: none;
white-space: nowrap;
&::before {
content: 'Scroll';
font-family: var.$font-family-mono;
font-size: 70%;
text-transform: uppercase;
}
&::after {
content: '›';
animation: xPulse 2s infinite;
font-family: var.$font-family-mono;
font-size: 100%;
line-height: 1;
position: relative;
top: -2px;
}
}
@include bem.b(table-scroll-wrapper) {
position: relative;
max-width: 100%;
&::before,
&::after {
content: '';
position: absolute;
z-index: func.z_index(background);
top: 0;
bottom: 0;
width: func.rhythm(2.5);
opacity: 0;
pointer-events: none;
transition: opacity 0.2s ease;
}
&::before {
left: 0;
background: linear-gradient(to right, rgba(colors.$color-cyberspace, 0.12), transparent);
}
&::after {
right: 0;
background: linear-gradient(to left, rgba(colors.$color-cyberspace, 0.12), transparent);
}
@include bem.b(table-container) {
overflow-x: auto;
overflow-y: hidden;
-webkit-overflow-scrolling: touch;
overscroll-behavior-x: contain;
scrollbar-width: thin;
}
.m-table {
min-width: 100%;
width: max-content;
}
.m-table--sticky-first {
min-width: 48rem;
}
&[data-overflowing="true"][data-scroll-start="false"] {
&::before {
opacity: 1;
}
.m-table--sticky-first {
thead,
tbody,
tfoot {
> tr > :first-child {
box-shadow: func.to_rem(6px) 0 func.to_rem(10px) rgba(colors.$color-cyberspace, 0.06);
}
}
}
}
&[data-overflowing="true"][data-scroll-end="false"] {
&::after {
opacity: 1;
}
}
&[data-overflowing="true"][data-scroll-start="true"] {
@include bem.b(scroll-indicator) {
display: inline-flex;
}
}
}
// Default styling for tables without classes
table {
@include table();
}
// Styling for default WP Table Block
.wp-block-table {
@include table();
}
const TABLE_SELECTOR = '.js-table';
const TABLE_SCROLL_WRAPPER_SELECTOR = '.js-table-scroll-wrapper';
const TABLE_CONTAINER_SELECTOR = '.js-table-container';
function getHeaderLabels(table) {
return Array.from(table.querySelectorAll('thead th'))
.map((cell) => cell.textContent.replace(/\s+/g, ' ').trim())
.filter(Boolean);
}
function syncLabels(table) {
const labels = getHeaderLabels(table);
if (!labels.length) {
return;
}
table.querySelectorAll('tbody tr').forEach((row) => {
Array.from(row.children).forEach((cell, index) => {
if (!cell.dataset.label && labels[index]) {
cell.dataset.label = labels[index];
}
});
});
}
function updateScrollState(wrapper) {
const container = wrapper.querySelector(TABLE_CONTAINER_SELECTOR);
if (!container) {
return;
}
const maxScrollLeft = Math.max(container.scrollWidth - container.clientWidth, 0);
const hasOverflow = maxScrollLeft > 8;
const scrollLeft = container.scrollLeft;
wrapper.dataset.overflowing = hasOverflow ? 'true' : 'false';
wrapper.dataset.scrollStart = !hasOverflow || scrollLeft <= 4 ? 'true' : 'false';
wrapper.dataset.scrollEnd = !hasOverflow || scrollLeft >= maxScrollLeft - 4 ? 'true' : 'false';
}
function enhanceScrollableTable(wrapper) {
if (wrapper.dataset.tableReady === 'true') {
updateScrollState(wrapper);
return;
}
const container = wrapper.querySelector(TABLE_CONTAINER_SELECTOR);
const table = container?.querySelector(TABLE_SELECTOR);
if (!container) {
return;
}
wrapper.dataset.tableReady = 'true';
const update = () => updateScrollState(wrapper);
container.addEventListener('scroll', update, { passive: true });
if ('ResizeObserver' in window) {
const observer = new ResizeObserver(update);
observer.observe(container);
if (table) {
observer.observe(table);
}
}
update();
}
document.querySelectorAll(TABLE_SELECTOR).forEach(syncLabels);
document.querySelectorAll(TABLE_SCROLL_WRAPPER_SELECTOR).forEach(enhanceScrollableTable);
window.addEventListener('load', () => {
document.querySelectorAll(TABLE_SCROLL_WRAPPER_SELECTOR).forEach(updateScrollState);
});
The basic scrollable table starts scrolling below the medium breakpoint (769px).
Responsive variants to compare: