The trick is defaulting to the red box when there's no hash fragment. I'm not sure you can do that with a pure CSS solution. (Edit: it turns out you can, as shown by Kaiido, provided you can put the default at the end; I should have thought of the general sibling combinator! But the CSS custom properties [aka "variables"] aren't actually doing anything in that AFAICS, see this fiddle.)
I don't know if it's possible to do this with CSS custom properties (aka "variables"); I tend to think not, since setting a custom property in the selector rule for one sibling (say, .box--blue:target
) doesn't change its value for any elements not inside that .box--blue
element (doesn't change it for the .box--red
sibling, for instance). (Fiddle.) But you can definitely show/hide boxes with the old checkbox/radio button trick:
Define the boxes as display: none
.
Have invisible radio buttons immediately prior to the box the radio button will relate to in the same parent:
<input type="radio" name="box-controller" id="chk-box--red" checked>
<div id="boxRed" class="box box--red"></div>
Have label
elements that tick the radio button for that box (via id
/for
) instead of links.
<label tab-index="0" for="chk-box--red">Show red box</label>
In the above, note that I've added tab-index
to the label
so it show sup in the tabbing order. We probably also want CSS that underlines it or similar.
Have a CSS rule that says the .box
immediately after a checked radio button should be display: block
:
input[name=box-controller]:checked + .box {
display: block;
}
Live example:
.box {
height: 100px;
width: 100px;
}
/* Hide boxes inside the outer box until/unless shown */
.box .box {
display: none;
}
label {
text-decoration: underline;
}
input[name=box-controller] {
display: none;
}
input[name=box-controller]:checked + .box {
display: block;
}
.box--red {
background-color: #ff0000;
}
.box--green {
background-color: #00ff00;
}
.box--blue {
background-color: #0000ff;
}
<div>
<div class="box">
<input type="radio" name="box-controller" id="chk-box--red" checked>
<div id="boxRed" class="box box--red"></div>
<input type="radio" name="box-controller" id="chk-box--green">
<div id="boxGreen" class="box box--green"></div>
<input type="radio" name="box-controller" id="chk-box--blue">
<div id="boxBlue" class="box box--blue"></div>
</div>
<nav>
<ul>
<li><label tab-index"0" for="chk-box--red">Show red box</label></li>
<li><label tab-index"0" for="chk-box--green">Show green box</label></li>
<li><label tab-index"0" for="chk-box--blue">Show blue box</label></li>
</ul>
</nav>
</div>
Important caveat: With the above, the URL is no longer driving the process (it doesn't change as you choose boxes). That's because you can't have a label
inside an a
, and if you put the a
inside the label
because it prevents the label
doing its job. If having the state in the URL is important to you, look at the JavaScript solution (which doesn't repeat itself) below.
You indicated that your concern with the JavaScript solution was that you were repeating a lot of code. There's no need to do that. Just for what it's worth, here's a JavaScript solution driven the hash fragment that works regardless of how many boxes you have; comments within:
// The function that shows the current box, hiding others
function showCurrentBox() {
// Get the box to show, defaulting to `boxRed`
const hash = location.hash.replace(/^#/, "") || "boxRed";
// Hide any box we've previously shown
// (Note the use of optional chaining, since `querySelector` may return `null`)
document.querySelector(".box.showing")?.classList.remove("showing");
// Show the box (again with optional chaining in case the fragment doesn't identify a box)
document.getElementById(hash)?.classList.add("showing");
}
// Run on startup
showCurrentBox();
// Run whenver the hash changes
window.addEventListener("hashchange", showCurrentBox);
.box {
height: 100px;
width: 100px;
}
/* Hide boxes inside the outer box until/unless shown */
.box .box {
display: none;
}
.box.showing {
display: block;
}
.box--red {
background-color: #ff0000;
}
.box--green {
background-color: #00ff00;
}
.box--blue {
background-color: #0000ff;
}
<div>
<div class="box">
<div id="boxRed" class="box box--red showing"></div>
<div id="boxGreen" class="box box--green"></div>
<div id="boxBlue" class="box box--blue"></div>
</div>
<nav>
<ul>
<li><a href="#boxRed">Show red box</a></li>
<li><a href="#boxGreen">Show green box</a></li>
<li><a href="#boxBlue">Show blue box</a></li>
</ul>
</nav>
</div>
Adding boxes to that is just a matter of defining their CSS and giving them an ID; the JavaScript code doesn't change.
With this solution, the URL does drive the boxes. Bookmark the URL showing the green box, and that's what shows when you come back to it.