Building an HTML Tab System

Sometimes you'll want to display a lot of information on a single page and be able to switch between sections, for which you'd like to implement a tab system. The trouble is, implementing these can be difficult and inflexible, especially considering differing amounts of content in each tab, making the resultant size difficulty to predict. Enter the following:

  • Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
  • Account information will appear here There are two lines of content here
  • Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit, sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
  • Invoicing information will appear here
    There are two lines of content here

The tab switching (not quite the tab heading yet; see below) is accomplished in native HTML / CSS, shown below:

<div class="tabs">
	<ul>
		<li class="on"><label for="tabs_01_01">General</label></li>
		<li><label for="tabs_01_02">Accounts</label></li>
		<li><label for="tabs_01_03">Customers</label></li>
		<li><label for="tabs_01_04">Invoices</label></li>
	</ul>
	<ul>
		<li><input type="radio" name="tabs_01[]" id="tabs_01_01" checked="checked"><div>
			General content will appear here
		</div></li>
		<li><input type="radio" name="tabs_01[]" id="tabs_01_02"><div>
			Account information will appear here
		</div></li>
		<li><input type="radio" name="tabs_01[]" id="tabs_01_03"><div>
			Customer listings will appear here
		</div></li>
		<li><input type="radio" name="tabs_01[]" id="tabs_01_04"><div>
			Invoicing information will appear here
		</div></li>
	</ul>
</div>
/* Tab system */
div.tabs {
	position: relative;
	font-size: 0;
	z-index: 0;
}

div.tabs > ul {
	position: relative;
	list-style-type: none;
	margin: 0;
	padding: 0;
}

div.tabs > ul > li {
	margin: 0;
	padding: 0;
	font-size: 14px;
}

/* Make the tab headings appear on top of the tab contents; allows blending */
div.tabs > ul:nth-of-type(1) {
	z-index: 1;
}

/* Make the tab heading containers line up horizontally */
div.tabs > ul:nth-of-type(1) > li {
	display: inline-block;
}

/* Clickable part of the tab headings */
div.tabs > ul:nth-of-type(1) > li > label {
	position: relative;
	display: block;
	padding: 5px 10px;
	border: 1px solid #333333;
	border-width: 1px 1px 0 1px;
	color: #333333;
	background-color: #EEEEEE;
	cursor: pointer;
}

/* Hide left border of all non-first tab headings */
div.tabs > ul:nth-of-type(1) > li:not(:first-child) > label {
	border-left: none;
}

/* The active tab heading */
div.tabs > ul:nth-of-type(1) > li.on > label {
	background-color: #FFFFFF;
}

/* For the active tab, add a 1px white line over the bottom of the tab to hide the content border */
div.tabs > ul:nth-of-type(1) > li.on > label::after {
	position: absolute;
	bottom: -1px;
	left: 1px;
	width: calc(100% - 2px);
	height: 1px;
	background-color: #FFFFFF;
	content: '';
}

/* Tab content */
div.tabs > ul:nth-of-type(2) {
	z-index: 0;
}

/* Hide all tab contents and radio elements */
div.tabs > ul:nth-of-type(2) > li > * {
	display: none;
}

/* Tab contents formatting */
div.tabs > ul:nth-of-type(2) > li > div {
	padding: 1em;
	border: 1px solid #333333;
}

/* Make the selected tab visible */
div.tabs > ul:nth-of-type(2) > li > input:checked + div {
	display: block;
}

To make this work, and permit multiple sets of tabs to appear on a single page, we give each tab its own unique ID on the page, and have each tab heading tied to its individual tab. The <radio> tag's name value should be unique per tab set to be sure that multiple tab sets don't interfere with each other.

The caveat here is that in native HTML/CSS, one click can't activate two items, namely the tab heading and the tab content. To accomplish the changing of the active tab header requires a reasonable amount of JavaScript, but is still very lightweight:

function page_load(item) {
	// If onload is empty, direct it at the supplied function
	// If onload is not empty, take its content and append the supplied function
	let load = window.onload;
	if (typeof load != 'function') { window.onload = item; } else {
		window.onload = () => { if (load) { load(); } item(); };
	}
}

page_load(() => {
	if (document.addEventListener) {
		// Add event listener to each tab heading's <label> tag
		let tab_headers = document.querySelectorAll("div.tabs > ul:nth-of-type(1) > li > label");
		for (let x = 0; x < tab_headers.length; x++) {
			tab_headers[x].addEventListener('click',(e) => { select_tab(e); });
		}
	}
});

function select_tab(e) {
	let target = e.target;
	let list = target.parentNode.parentNode;
	for (let x = 0; x < list.childNodes.length; x++) {
		// Unselect every tab heading
		if (list.childNodes[x].classList) { list.childNodes[x].classList.remove('on'); }
	}
	target.parentNode.classList.add('on'); // Set the clicked tab heading as active
}

Of course, both the tab header and content switching can be done in JavaScript, although I find it's best to keep as much native behaviour as possible, to keep overheads down and avoid breakages where JavaScript execution is limited or blocked. In that scenario, the tab switching would still work, albeit without switching tab headers, thereby giving the best behaviour possible under the circumstances.