A theme written for the fli-ginormosia.bearblog.dev site with responsive tables.
This theme was designed for my Fantasy Life i - Ginormosia Data Project. The main goal was to make something as quickly as possible while also being as readable as possible. The main feature, if you can call it that, is semi-responsive tables. Otherwise it reuses some code from my theme here, such as margins with clamp, the tables of contents, and prev/next post buttons. You can find explanations of those on the Bearblog Customization page.
As always if you re-use any of this or have any questions send me an email. Although I don’t know who else is going to be putting large amounts of …
A theme written for the fli-ginormosia.bearblog.dev site with responsive tables.
This theme was designed for my Fantasy Life i - Ginormosia Data Project. The main goal was to make something as quickly as possible while also being as readable as possible. The main feature, if you can call it that, is semi-responsive tables. Otherwise it reuses some code from my theme here, such as margins with clamp, the tables of contents, and prev/next post buttons. You can find explanations of those on the Bearblog Customization page.
As always if you re-use any of this or have any questions send me an email. Although I don’t know who else is going to be putting large amounts of tabular data on a bearblog.
Contents:
Tables
I am making absolutely no claims to this being the best or optimal solution for screenreader-friendly sortable tables, but I’ve done my best to keep the semantic structure of the table correct. It is based off of this W3.org tutorial.
You can see these tables in action on the Monsters page of the Ginormosia site.
HTML
<div class="table-wrap">
<table class="sortable">
<caption>
West Dryridge Desert
</caption>
<thead>
<tr>
<th aria-sort="ascending"><button>Location<span aria-hidden="true"></span></button></th>
<th><button>Rank<span aria-hidden="true"></span></button></th>
<th><button>Type<span aria-hidden="true"></span></button></th>
<th><button>Name<span aria-hidden="true"></span></button></th>
<th><button>Boss<span aria-hidden="true"></span></button></th>
<th><button>Condition<span aria-hidden="true"></span></button></th>
<th class="no-sort"><button>Notes<span aria-hidden="true"></span></button></th>
</tr>
</thead>
<tbody>
<tr>
<td>01 West Dryridge Desert</td>
<td>1</td>
<td>Ore</td>
<td>Ancient Fossil</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>1</td>
<td>Tree</td>
<td>Desert Tree</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>5</td>
<td>Ore</td>
<td>Earth Starcrystal</td>
<td>Gold Crown</td>
<td></td>
<td>Drops Diamond</td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>5</td>
<td>Fish</td>
<td>Golden Swordfish</td>
<td>Gold Crown</td>
<td></td>
<td>Drops Golden Fin</td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>3</td>
<td>Tree</td>
<td>Great Desert Tree</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>3</td>
<td>Ore</td>
<td>Great Haniwa Stone</td>
<td></td>
<td></td>
<td></td>
</tr>
<tr>
<td>01 West Dryridge Desert</td>
<td>2</td>
<td>Tree</td>
<td>Great Palm Tree</td>
<td></td>
<td></td>
<td></td>
</tr>
</table></div>
The wrapping div was what helped allow the table to be scrollable on smaller screens using overflow-x.
The table contains a caption whish describes the content of the table. Then within thead we have the column headings. They all have a button within them to allow the user to click on the heading to sort. And the span is where the sort up/down icon goes. If the column is being sorted the icon is visible, if it isn’t then it’s hidden, hence the aria-hidden. The th has an aria-sort="ascending" to indicate it is the default sort column. It could be added to one of the other columns if you prefer.
Javascript
This javascript is copied wholesale from the W3C so I make no claim to it. It is simply dumped as is in the Head directive. If you only wanted to use it on one page could you embed it just in the top of that page.
<script>/*
* This content is licensed according to the W3C Software License at
* https://www.w3.org/Consortium/Legal/2015/copyright-software-and-document
*
* File: sortable-table.js
*
* Desc: Adds sorting to a HTML data table that implements ARIA Authoring Practices
*/
'use strict';
class SortableTable {
constructor(tableNode) {
this.tableNode = tableNode;
this.columnHeaders = tableNode.querySelectorAll('thead th');
this.sortColumns = [];
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (buttonNode) {
this.sortColumns.push(i);
buttonNode.setAttribute('data-column-index', i);
buttonNode.addEventListener('click', this.handleClick.bind(this));
}
}
this.optionCheckbox = document.querySelector(
'input[type="checkbox"][value="show-unsorted-icon"]'
);
if (this.optionCheckbox) {
this.optionCheckbox.addEventListener(
'change',
this.handleOptionChange.bind(this)
);
if (this.optionCheckbox.checked) {
this.tableNode.classList.add('show-unsorted-icon');
}
}
}
setColumnHeaderSort(columnIndex) {
if (typeof columnIndex === 'string') {
columnIndex = parseInt(columnIndex);
}
for (var i = 0; i < this.columnHeaders.length; i++) {
var ch = this.columnHeaders[i];
var buttonNode = ch.querySelector('button');
if (i === columnIndex) {
var value = ch.getAttribute('aria-sort');
if (value === 'descending') {
ch.setAttribute('aria-sort', 'ascending');
this.sortColumn(
columnIndex,
'ascending',
ch.classList.contains('num')
);
} else {
ch.setAttribute('aria-sort', 'descending');
this.sortColumn(
columnIndex,
'descending',
ch.classList.contains('num')
);
}
} else {
if (ch.hasAttribute('aria-sort') && buttonNode) {
ch.removeAttribute('aria-sort');
}
}
}
}
sortColumn(columnIndex, sortValue, isNumber) {
function compareValues(a, b) {
if (sortValue === 'ascending') {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return a.value - b.value;
} else {
return a.value < b.value ? -1 : 1;
}
}
} else {
if (a.value === b.value) {
return 0;
} else {
if (isNumber) {
return b.value - a.value;
} else {
return a.value > b.value ? -1 : 1;
}
}
}
}
if (typeof isNumber !== 'boolean') {
isNumber = false;
}
var tbodyNode = this.tableNode.querySelector('tbody');
var rowNodes = [];
var dataCells = [];
var rowNode = tbodyNode.firstElementChild;
var index = 0;
while (rowNode) {
rowNodes.push(rowNode);
var rowCells = rowNode.querySelectorAll('th, td');
var dataCell = rowCells[columnIndex];
var data = {};
data.index = index;
data.value = dataCell.textContent.toLowerCase().trim();
if (isNumber) {
data.value = parseFloat(data.value);
}
dataCells.push(data);
rowNode = rowNode.nextElementSibling;
index += 1;
}
dataCells.sort(compareValues);
// remove rows
while (tbodyNode.firstChild) {
tbodyNode.removeChild(tbodyNode.lastChild);
}
// add sorted rows
for (var i = 0; i < dataCells.length; i += 1) {
tbodyNode.appendChild(rowNodes[dataCells[i].index]);
}
}
/* EVENT HANDLERS */
handleClick(event) {
var tgt = event.currentTarget;
this.setColumnHeaderSort(tgt.getAttribute('data-column-index'));
}
handleOptionChange(event) {
var tgt = event.currentTarget;
if (tgt.checked) {
this.tableNode.classList.add('show-unsorted-icon');
} else {
this.tableNode.classList.remove('show-unsorted-icon');
}
}
}
// Initialize sortable table buttons
window.addEventListener('load', function () {
var sortableTables = document.querySelectorAll('table.sortable');
for (var i = 0; i < sortableTables.length; i++) {
new SortableTable(sortableTables[i]);
}
});</script>
CSS
/* tables */
table { /* table should stretch and shrink to the fit the body width until it hits 600px at which point the media query takes over */
table-layout: fixed;
max-width: 900px;
min-width: 600px;
width: 100%;
overflow: auto;
}
@media (max-width: 600px) { /* with the table width fixed at 600px minimum for readability the div wrapper containing the table is allowed to shrink to fit the body width, causing the table within it to overflow horizontally and scroll */
.table-wrap {
display: block;
width: 100%;
overflow-x: scroll;
}
}
caption {
font-size: 1.25em;
font-weight: 600;
text-decoration: underline;
padding: 1em;
}
table.sortable td,
table.sortable th {
padding: 0.1em 0.1em;
}
table.sortable th {
font-weight: bold;
border-bottom: thin solid var(--color-accent);
position: relative;
}
table.sortable th button {
padding: 4px;
margin: 1px;
font-size: 1em;
font-weight: 600;
background: transparent;
border: none;
display: inline;
text-align: left;
outline: none;
cursor: pointer;
}
table.sortable th button span {
position: absolute;
right: 4px;
}
table.sortable th[aria-sort="descending"] span::after {
content: "▼";
color: currentcolor;
font-size: 100%;
top: 0;
}
table.sortable th[aria-sort="ascending"] span::after {
content: "▲";
color: currentcolor;
font-size: 100%;
top: 0;
}
/* alternating row colours */
table.sortable tbody tr:nth-child(odd) {
background-color: light-dark(lightgrey, dimgrey);
}
/* centering number columns */
.monsters-page table.sortable tbody td:nth-child(2),
.monsters-page table.sortable tbody td:nth-child(5) {
text-align: center;
}
.gathering-page table.sortable tbody td:nth-child(2) {
text-align: center;
}
.events-page table.sortable tbody td:nth-child(3),
.events-page table.sortable tbody td:nth-child(6)
.events-page table.sortable tbody td:nth-child(7) {
text-align: center;
}
.rankup-page table.sortable tbody td:nth-child(4) {
text-align: center;
}
The table design started from the W3 tutorial, but I ended up having to change a lot of things, especially to make the tables readable on smaller screens. If you use a purely responsive width e.g. 100% the contents of the table will get squeezed into unreadability on smaller screens. But if you set a fixed width in pixels it might not stretch as far as it could on high-res screens or start overflowing on medium to smaller sized ones when you could have just compressed it a bit. This is something of a compromise derived from trial and error, there may be a more efficient way to do this.
The table-layout: fixed requires setting a fixed width, which I did by setting a min and max width. This ensures that all the tables on the page are the same width rather than the contents of the table setting the width of the table, leaving them all different widths, which looks ugly and makes it hard to scan the same column down the page.
The design itself is very simple with some bold and underline for the column headings and alternating colours for the rows. The arrows to indicate the sort direction of the columns can be changed to another unicode character and their colour can be changed, it’s set to the current text colour by default. The header buttons are made invisible.
The final section for centering the text in columns that are just a number is kind of a hack, I’m sure there’s a better way to do it but this was the fastest.
Other Elements
The nav is broken up into three divs so I could have three rows of links that would flex.
<div class="nav-home"><a href="/">Home</a> • <a href="/links/">More Info</a></div>
<div class="nav-faq">Guide: <a href="/faq/">Start</a> • <a href="/faq-1/">Part 1</a> • <a href="/faq-2/">Part 2</a> • <a href="/faq-3/">Part 3</a></div>
<div class="nav-data">Data: <a href="/monsters/">Monsters</a> • <a href="/gathering/">Gathering</a> • <a href="/events/">Events</a> • <a href="/rankup/">Rank Up Quests</a> • <a href="https://docs.google.com/spreadsheets/d/1nFjzdDheDLNWwo0KfJRtd38Hus-0sd7gJgkm8MEiOJo/edit?usp=sharing">Google Sheet</a></div>
I also made a small plain callout box for the front page for soliciting donations, which is just a div with class="callout".
The footer is a skip to top button, the creative commons CC-BY-NC license icon, and Powered by Bear. The skip to top button requires putting an anchor in the Head directive somewhere.
Full CSS
The colours are all default named colours and a simple light/dark mode is achieved with light-dark() where necessary. The font is the system sans-serif. Upvote buttons are hidden and I arbitrarily picked two colours as accents for links and anything else that might need highlighting.
:root {
color-scheme: light dark;
--color-accent: mediumorchid;
--color-accent2: lavender;
}
body {
display: flex;
flex-direction: column;
margin: 0 clamp(0rem, calc(-5.357rem + 26.786vw), 18.75rem);
font-family: ui-sans-serif;
color: light-dark(black, white);
background: light-dark(white, black);
font-size: 16px;
}
main {
padding: unset;
}
header {
text-align: center;
}
footer {
display: flex;
flex-direction: row;
gap: 1rem;
max-width: 100%;
max-height: 2.5em;
align-content: center;
justify-content: space-between;
}
a {
color: var(--color-accent);
font-weight: 600;
}
a:hover {
color: var(--color-accent2);
}
strong {
font-weight: 600;
}
h2 {
border-bottom: 2px solid light-dark(black, white);
}
/* header */
.title {
flex-grow: 0;
text-decoration: none;
font-size: 1rem;
line-height: 2rem;
color: light-dark(black, white);
}
nav {
line-height: 1.2em;
font-size: 1.2rem;
font-weight: bold;
}
nav a {
font-weight: 400;
}
/* lists and toc */
ol, ul {
list-style-position: outside;
line-height: 1.2em;
padding-inline: clamp(0.625rem, calc(0.339rem + 1.429vw), 1.625rem) 0;
margin: 1rem;
}
li {
margin: 0 clamp(0.25em, calc(0.068em + 0.909vw), 0.75em);
}
ol li::marker {
font-weight: 600;
}
ol li > ol {
padding: unset;
margin: 0 0 0 1.2em;
}
ul li > ul {
padding: unset;
margin: 0 0 0 1.2em;
}
.toc {
display: block;
padding: clamp(0.375em, calc(0.268em + 0.536vw), 0.75em);
margin: 0 clamp(0.5em, calc(-4.768em + 26.339vw), 25em) 0 0;
border: 2px dotted light-dark(dimgrey, lightgrey);
border-radius: 4px;
font-size: smaller;
}
.toc a {
text-decoration: none;
}
.toc h2 {
margin: unset;
padding: unset;
border: none;
line-height: 1em;
}
.toc :is(ol, ul) {
margin: unset;
padding-top: 0.05em;
padding-inline: clamp(1rem, calc(0.821rem + 0.893vw), 1.625rem) 0;
line-height: 1.2;
}
.toc ol li,
.toc ul li,
.toc ol li > ul,
.toc ol li > ol,
.toc ul li > ul {
padding: unset;
}
/* callout box */
body.home {
display: flex;
flex-direction: column;
}
.callout {
background-color: light-dark(lightgrey, dimgrey);
margin: 0 2rem 0 2rem;
padding: 0.5rem 1rem 0.5rem 1rem;
align-content: center;
justify-content: center;
}
.callout p {
padding: unset;
margin: unset;
}
/* post nav buttons */
.post-nav {
display: flex;
flex-direction: row;
align-items: center;
justify-content: space-between;
gap: 1em;
margin: 1em 0 1em 0;
}
.prev-post,
.next-post {
padding: 4px;
text-decoration: none;
margin: 4px;
}
.prev-post:before {
content: '\25C0 '
}
.next-post:after {
content: ' \25B6'
}
/* tables */
@media (max-width: 600px) {
.table-wrap {
display: block;
width: 100%;
overflow-x: scroll;
}
}
table {
table-layout: fixed;
max-width: 900px;
min-width: 600px;
width: 100%;
overflow: auto;
}
caption {
font-size: 1.25em;
font-weight: 600;
text-decoration: underline;
padding: 1em;
}
table.sortable td,
table.sortable th {
padding: 0.1em 0.1em;
}
table.sortable th {
font-weight: bold;
border-bottom: thin solid var(--color-accent);
position: relative;
}
table.sortable th button {
padding: 4px;
margin: 1px;
font-size: 1em;
font-weight: 600;
background: transparent;
border: none;
display: inline;
text-align: left;
outline: none;
cursor: pointer;
}
table.sortable th button span {
position: absolute;
right: 4px;
}
table.sortable th[aria-sort="descending"] span::after {
content: "▼";
color: currentcolor;
font-size: 100%;
top: 0;
}
table.sortable th[aria-sort="ascending"] span::after {
content: "▲";
color: currentcolor;
font-size: 100%;
top: 0;
}
table.sortable tbody tr:nth-child(odd) {
background-color: light-dark(lightgrey, dimgrey);
}
.monsters-page table.sortable tbody td:nth-child(2),
.monsters-page table.sortable tbody td:nth-child(5) {
text-align: center;
}
.gathering-page table.sortable tbody td:nth-child(2) {
text-align: center;
}
.events-page table.sortable tbody td:nth-child(3),
.events-page table.sortable tbody td:nth-child(6)
.events-page table.sortable tbody td:nth-child(7) {
text-align: center;
}
.rankup-page table.sortable tbody td:nth-child(4) {
text-align: center;
}
/* upvote button hidden */
.upvote-button svg {
display: none;
}
.upvote-button .upvote-count {
display: none;
}