Skip to content

Instantly share code, notes, and snippets.

@sam
Last active July 21, 2023 13:30
Show Gist options
  • Save sam/64fa260196ab3991f1d88c766b082497 to your computer and use it in GitHub Desktop.
Save sam/64fa260196ab3991f1d88c766b082497 to your computer and use it in GitHub Desktop.
Pure CSS tabs example with optional JavaScript to support direct-linking to tab URLs.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>CSS Tabs Example</title>
<script>
/*
This script enables direct-linking to a tab.
*/
document.addEventListener('DOMContentLoaded', function() {
const params = new URLSearchParams(window.location.search);
document.querySelectorAll(".tabs").forEach((tab) => {
const radios = tab.querySelectorAll("input[type='radio']");
// Return early and continue to the next tab if there are no radios.
if(radios.length === 0)
return;
// If the document defines a checked radio button, store that for later in case
// our querystring param is invalid and we've overwritten the default selection.
// If there is no checked radio button, use the first one for this purpose.
const defaultRadio = tab.querySelector("input[type='radio']:checked") || radios[0];
let checked; // This will be the id of the checked radio-button.
let set = false; // This will be true if we set any radio-button to checked.
radios.forEach((radio) => {
// For a given name, check for a querystring param.
if(!checked)
checked = params.get(radio.name);
// Only reassign from default if the param is set.
if(checked)
radio.checked = radio.id === checked;
set = set || radio.checked;
// When a radio button is checked by the user, update the URL.
radio.addEventListener("change", (e) => {
const radio = e.currentTarget;
if(radio.checked) {
// Other events may have modified window.location.search while
// this page was loaded, so we need to get the current params at
// the time the event is fired.
const params = new URLSearchParams(window.location.search);
params.set(radio.name, radio.id);
history.pushState({}, '', window.location.pathname + '?' + params.toString());
}
});
});
// If there is an id specified in the querystring, but no radio-button
// has been checked, select the default and replace the invalid selection.
if(checked && !set) {
defaultRadio.checked = true;
// Another tab iteration may have already modified the URL, so we
// need to get the current version of the params again at the time
// of writing to the history.
const params = new URLSearchParams(window.location.search);
params.set(defaultRadio.name, defaultRadio.id);
history.replaceState({}, '', window.location.pathname + '?' + params.toString());
}
});
});
</script>
<style>
body {
margin: 0;
font-family: sans-serif;
}
header {
background-color: #333;
color: #fff;
padding: 1rem;
}
header a {
color: #fff;
margin-right: 1rem;
}
.tabs {
/*
These two properties are important; they work in
conjunction with the width 100% of the sections to
force our labels and our tab content into two rows.
*/
display: flex;
flex-wrap: wrap;
min-height: 200px;
margin-top: 50px;
}
.tabs input {
/*
Since we can select the radio buttons by their label,
and we don't want a visible radio-button, we'll hide
them visually. This is more complex than it seems like
it should be since we want to keep keyboard navigation
intact for accessibility. So we restrict the radio
button size to a 1px box and then use a few different
techniques to "erase" that pixel visually.
*/
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0,0,0,0);
border: 0;
}
.tabs label {
/*
Here we're not really doing anything significant,
just styling the label a bit to appear as a "tab".
*/
margin: 0 0 -1px;
padding: 15px 25px;
font-weight: 600;
text-align: center;
color: #ddd;
border: 1px solid transparent;
cursor: pointer;
}
.tabs input:focus + label {
/*
A bit of a hack since we can't "focus" a label,
but we _can_ apply styles to the label that appears
next to our focused radio button.
*/
outline: 2px solid #0097A7;
}
.tabs input:checked + label {
/*
This selector will target the label that is
immediately after a checked radio button.
This is the tab that is currently "active".
*/
color: #fff;
border: 1px solid #ccc;
border-top: 2px solid #0097A7;
border-bottom: 1px solid #fff;
background-color: magenta;
}
.tabs section {
/*
The three properties here work in conjunction with
our flex-wrap: wrap property on the .tabs container.
First we want to hide all tab content by default.
Then we set a width of 100%, allowing the flex-wrap to
create a new row. Finally, we set the order to 1
instead of the default 0 so that the content is visually
restructured to appear below the labels.
The last point is where all the magic happens!
*/
display: none;
width: 100vw;
order: 1;
padding: 20px;
}
.tabs input:checked + label + section {
/*
This selector will target the section that is
immediately after a checked radio button.
This is the tab-content that is currently "active".
Note that we're able to use a selector like this
because that's our document structure as-written,
even though the "order: 1" above visually rewrites
the document structure!
*/
display: block;
}
/*
Now we want to demonstrate some styling and responsive
behavior. Our first goal is to place the labels on a
"tab bar" with a grey background.
*/
main {
background-color: #bbb;
}
/*
By setting our section backgrounds to white, and a width
of 100vw (or 100% if you prefer inside a container), we
cover our grey background, achieving the desired
"tab bar" effect.
*/
section {
width: 100vw;
background-color: white;
}
/*
Here's where it gets a little tricky. We have to have an
element within our sections to control the actual visual "tab"
width. Our release uses an article, and our photos uses a div.
We want the release to be a comfortable 60vw for reading width:
*/
section#release article {
width: 60vw;
margin: auto;
background-color: yellow;
}
/*
For the photos grid, we want a more visual media layout, so
we'll use a width of 80vw:
*/
section#photos > div {
width: 80vw;
margin: auto;
background-color: lime;
}
/*
The only problem now is that our tab labels are off on the left
of our 100vw tab-bar. Since the release tab is the first displayed,
we want to align the tabs with the left edge of the article.
This is actually easier than it sounds, since we know the width
of the article, and we know the width of the tab-bar, so we can
just use some simple math to calculate the margin-left we need on the
first label to align it with the article.
*/
.tabs input:first-child + label {
/* Need to account for default body margin if present... */
margin-left: calc(20vw - 15px);
/*
Since we've set body margin to 0, we can just use
half the margin of the element we want to align with,
section#release article in this case, with a width
of 60vw.
100 - 60 = 40, 40 / 2 = 20.
*/
margin-left: calc((100vw - 60vw) / 2);
/* The math is easy, so no need to use calc. */
margin-left: 20vw;
}
</style>
</head>
<body>
<header>
<h1>CSS Tabs Example</h1>
<nav>
<a href="#">Home</a>
<a href="#">About</a>
<a href="#">Contact</a>
</nav>
</header>
<main class="tabs">
<input id="release-tab" type="radio" name="content-tab" checked>
<label for="release-tab">Article</label>
<section id="release">
<article>
<header>
<h1>The Benefits of Regular Exercise</h1>
<p class="author">By John Doe</p>
<p class="date">Published on July 20, 2023</p>
</header>
<img src="exercise.jpg" alt="People exercising in a park" />
<p>Regular exercise is not only important for maintaining a healthy weight, but it also offers numerous other benefits that contribute to overall well-being.</p>
<h2>Physical Health</h2>
<p>Engaging in regular physical activity helps strengthen the cardiovascular system, improves lung capacity, and increases muscle tone and flexibility. It reduces the risk of chronic diseases such as heart disease, diabetes, and certain types of cancer. Regular exercise also helps maintain a healthy weight, boosts metabolism, and improves overall energy levels.</p>
<h2>Mental Health</h2>
<p>Exercise is not just beneficial for the body but also for the mind. Physical activity stimulates the release of endorphins, which are known as "feel-good" hormones. These endorphins help reduce stress, anxiety, and symptoms of depression. Regular exercise has been linked to improved sleep quality, enhanced cognitive function, and increased self-esteem.</p>
<h2>Social Interaction</h2>
<p>Participating in group activities or team sports can provide opportunities for social interaction and foster a sense of belonging. Joining a fitness class, sports league, or exercising with friends allows you to meet new people with similar interests, which can contribute to improved mental well-being and overall happiness.</p>
<h2>Longevity</h2>
<p>Research consistently shows that individuals who engage in regular exercise tend to live longer than those who lead sedentary lifestyles. Exercise helps to strengthen the immune system, improve organ function, and reduce the risk of age-related diseases. By adopting an active lifestyle, you increase your chances of enjoying a longer, healthier, and more fulfilling life.</p>
<h2>Getting Started</h2>
<p>If you're new to exercise, it's essential to start slowly and gradually increase the intensity and duration of your workouts. Choose activities that you enjoy, whether it's walking, cycling, swimming, or dancing. Aim for at least 150 minutes of moderate aerobic activity or 75 minutes of vigorous activity each week, along with strength training exercises twice a week.</p>
<p>In conclusion, incorporating regular exercise into your routine brings a multitude of benefits for both your physical and mental well-being. Make it a priority to prioritize your health and start reaping the rewards of an active lifestyle today.</p>
<footer>
<p>Disclaimer: The information provided in this article is for educational purposes only and should not be considered as medical advice. Consult with a healthcare professional before starting any exercise program or making significant changes to your current routine.</p>
</footer>
</article>
</section>
<input id="photos-tab" type="radio" name="content-tab">
<label for="photos-tab">Photos</label>
<section id="photos">
<div>
<img src="https://picsum.photos/seed/first/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/second/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/third/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/fourth/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/fifth/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/sixth/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/seventh/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/eighth/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/ninth/200/300" alt="Random photo">
<img src="https://picsum.photos/seed/tenth/200/300" alt="Random photo">
</div>
</section>
</main>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment