<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>&nbsp;</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
}
  • Content:
    @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();
    }
    
  • URL: /components/raw/table/_table.scss
  • Filesystem Path: src/molecules/table/_table.scss
  • Size: 7.7 KB
  • Content:
    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);
    });
    
  • URL: /components/raw/table/table.js
  • Filesystem Path: src/molecules/table/table.js
  • Size: 2.1 KB

Tables

The basic scrollable table starts scrolling below the medium breakpoint (769px).

Responsive variants to compare:

  • Scrollable: keeps the full table and adds directional overflow hints only when horizontal scrolling is actually needed.
  • Stacked: turns each row into a card with column labels above each value.
  • Sticky first column: pins the first column while the rest of the table scrolls horizontally.