Yeah. We live in a world where JavaScript is a given, and we all like to use and abuse it for even the simplest things. We even import jQuery and other frameworks for the simplest of things, which leads to bloat (a whole factory just for one UI function) and privacy issues (the fact that most people choose to just use a CDN, because cached scripts or something).
But what if I told you that you can leave out the script for these cool flashy elements, and stick to good ol' HTML+CSS? NoScript users will thank you <3 But screen reader users might not. :(
Use with care!
Here's a quick rundown of the stuff we'll be using:
You can abuse form elements here to make your memes come true... provided you don't have any actual forms, that might be confusing to have one functional form and the other a presentational form.
We're gonna use type
attributes with this thing. Let's say, use type="radio"
if you want toggleables,
and use type="checkbox"
if you want multiple things to be toggled at the same time. If you
want the former, you'll have to
group things together using a name
attribute. Don't forget to set the hidden
attribute here! And set your id
s, which
will allow your inputs to be decked out using...
Don't be scared; pretend it's a <div>
. Rather, treat it like a <div>
! Because you can pretty much do anything with it.
And you can put stuff inside it. Like a <div>
. Which is nice to have.
But here's the coolest thing about it: you can interact with it! You just need to attach it to
an actual input using the for
attribute. Point it to your input id
, and when the label is clicked,
it will activate the input you pointed to.
Example: you have an input with an id
of "dave". You have a label, for
the "dave" input.
When you click the label, it activates the "dave" input. I'll explain...
CSS has a bunch of selectors that are a bit more advanced than things inside of things, but still pretty easy. The following are the ones that we can use for some sick tricks.
In the code examples, the highlighted code is the elements that are being styled, or affected by the CSS selector.
Styling: p, but if there's a hr immediately before it.
How:
<hr>
<p> Hello </p>
<p> Hi </p>
Styling: span, but if there's a div before it (immediately or not, doesn't matter). It also needs to have the same parent element as that div.
How:
<section>
<span> Somebody </span>
<div> Once </div>
<span> Told </span>
<span> Me </span>
</section>
Styling: any input with the name attribute set to "temmie". And is selected.
How:
<input name="temmie" type="radio" checked>
<input name="temmie" type="radio">
<input name="temmie" type="radio">
<input name="temmie" type="radio">
<input name="bob" type="checkbox" checked>
Styling: anything which its ID's begin with "sudo".
How:
<div id="sudocreme"> ... </div>
<div id="sudowhich"> ... </div>
<div id="notasudo"> ... </div>
<div id="visudovis"> ... </div>
Alright, now let's get to business.
You should have your box-sizing
set to border-box
for this...
Add this to your stylesheet:
html{
box-sizing: border-box;
}
*,
*:before,
*:after{
box-sizing: inherit;
}
Accordions can only toggle one at a time, right? We can (ab)use the radio-button input for this. Consider this:
Have your text here.
Thing with image.
Lorem ipsum dolor amet adaptogen cornhole vinyl chillwave retro, fam lo-fi man bun cred swag. Edison bulb ugh whatever live-edge celiac, schlitz. Freegan artisan cornhole, kitsch literally typewriter organic cliche cardigan migas swag taiyaki dreamcatcher. Man braid messenger bag neutra before they sold out, ennui XOXO brooklyn.
You can even put forms here too. oooooh
We've ran a few tests, and here's what we got...
Amount of Drivel Spewed per Year | |
---|---|
2010 | 49 |
2011 | 60 |
2012 | 80 |
2013 | 90 |
2014 | 112 |
2015 | 145 |
2016 | 167 |
2017 | 260 |
2018 | 400 |
2019 | 506 |
2020 (proj.) | 908.24058 | Source: the cloud. |
<div class="accordion-container">
<input id="accordion1-1" name="acg" type="radio" checked hidden>
<label for="accordion1-1">
<b>Thing 1</b>
</label>
<section class="ac-contents">
<p>Have your text here.</p>
</section>
<input id="accordion1-2" name="acg" type="radio" hidden>
<label for="accordion1-2">
<b>Thing 2</b>
</label>
<section class="ac-contents">
<p>Thing with image.</p>
<img align="right" src="https://placekitten.com/100/100">
<p>Lorem ipsum dolor amet adaptogen cornhole vinyl chillwave retro,
fam lo-fi man bun cred swag. Edison bulb ugh whatever live-edge celiac,
schlitz. Freegan artisan cornhole, kitsch literally typewriter organic
cliche cardigan migas swag taiyaki dreamcatcher. Man braid messenger
bag neutra before they sold out, ennui XOXO brooklyn.</p>
</section>
<input id="accordion1-3" name="acg" type="radio" hidden>
<label for="accordion1-3">
<b>Thing 3</b>
</label>
<section class="ac-contents">
<p>You can even put forms here too. oooooh</p>
<form>
<h1>Get our daily spam</h1>
<p>We promise not to send you ads for vitality medicines
you don't need. Pinky promise.</p>
<label for="email">Email</label><br>
<input type="text" placeholder="email plox n thnk" name="email"><br><br>
<label for="pet">Pet?</label><br>
<select name="pet">
<option value="dog">Doggo</option>
<option value="cat">Neko</option>
<option value="parrot">Peedy</option>
<option value="bonzi">BonziBuddy</option>
<option value="fish">Go Fish</option>
</select><br><br>
<button type="button">Send me that good stuff</button>
</form>
</section>
<input id="accordion1-4" name="acg" type="radio" hidden>
<label for="accordion1-4">
<b>Thing 4</b>
</label>
<section class="ac-contents">
<h1>Unscrupulous Nonsense</h1>
<p>We've ran a few tests, and here's what we got...</p>
<table border="1">
<thead>
<th colspan="2">Amount of Drivel Spewed per Year</th>
</thead>
<tr><td width="10%">2010</td><td>49</td></tr>
<tr><td>2011</td><td>60</td></tr>
<tr><td>2012</td><td>80</td></tr>
<tr><td>2013</td><td>90</td></tr>
<tr><td>2014</td><td>112</td></tr>
<tr><td>2015</td><td>145</td></tr>
<tr><td>2016</td><td>167</td></tr>
<tr><td>2017</td><td>260</td></tr>
<tr><td>2018</td><td>400</td></tr>
<tr><td>2019</td><td>506</td></tr>
<tr><td>2020 (proj.)</td><td>908.24058</td></tr>
<tfoot>
<td colspan="2">Source: the cloud.</td>
</tfoot>
</table>
</section>
</div>
[id^="accordion"] {
/* Hide all input elements which id starts with "accordion" */
display: none;
}
.accordion-container{
/* The background for our whole accordion group. */
background-color: #0083E9;
background-image: linear-gradient(160deg, #0083E9 0%, #80c0b7 100%);
color: white;
box-shadow:0 7px 18px rgba(0,0,0,.4);
}
[for^="accordion"] {
/* The accordion header, because labels that all point to "accordion" */
display: block;
padding: 1rem 1.5rem;
border-top:1px solid rgba(0,0,0,.2);
/* Down button */
background-image: url("dn.svg");
background-size:1.5em;
background-repeat: no-repeat;
background-position: right 1em center;
}
[id^="accordion"]:checked + [for^="accordion"]{
/* There's an up button; you can add more styles if you want*/
background-image: url("up.svg");
background-size:1.5em;
background-repeat: no-repeat;
background-position: right 1em center;
}
[for^="accordion"]:hover,
[id^="accordion"]:checked + [for^="accordion"]{
/* Light accordion header on hover and expanded */
background-color: rgba(255,255,255,.2);
}
.ac-contents {
/* The accordions' contents */
padding: 0 1.5rem;
background: rgba(80,80,80,.3);
overflow-y: scroll;
transition: height .3s;
height: 0;
}
[id^="accordion"]:checked + [for^="accordion"] + .ac-contents {
/* Expand the accordion to a fixed size. */
height: 15em;
}
...Okay, yeah, that was a bit overwhelming. So let's focus on this single accordion element...
(1) <input id="accordion1-1" name="acg" type="radio" checked hidden>
(2) <label for="accordion1-1">
<b>Thing 1</b>
</label>
(3) <section class="ac-contents">
<p>Have your text here.</p>
</section>
From the top, we have the input that we want to use. In our case,
we'd like to make a single-toggle accordion, so we choose the
"radio" type for our input. This would let us view only one of
the entries at a time. Because we picked a radio type, we must
choose a name
to use, to link these radio buttons together.
I recommend choosing something really short but unique... like
"acg" here, for "accordion group".
We have to set an ID for this input so we can refer to it in our label, which will be our accordion's heading. Here, we'll set a convention that accordion IDs must begin with "accordion". This will help us write our CSS selectors later.
Now we have our label. This label is the heading of our
accordion, and it contains the title of the accordion.
You can choose a heading level here (h1
, h2
etc.), but I chose
to use a b
tag here, for no good reason.
We need to make sure that our label here points to the
correct input ID, so make sure that the for
in
our label matches the id
in our input.
Finally, we have the actual contents of our accordion, under
a section with the class of ac-contents
. Really, just
go crazy here. That's why the HTML looks whack and complicated.
Now repeat that a bunch of times, with your contents being wack-er with every iteration, and you're pretty much golden.
Now the real magic here is where you use CSS to your advantage. You may look at the CSS code; I commented the code as needed, although I may need to explain some of these still.
You may notice a severe lack of class-happenings. Instead, we have
the [id^="accordion"]
operator, which affects
IDs starting with "accordion". There is also the similar
[for^="accordion"]
operator. As far as I know,
the for
attribute is only used in label
,
so I feel that it's specific enough. I may make the thing
look cleaner by adding classes, but the HTML already feels dirty,
so I don't feel like doing that. You can do that if you want,
though.
The open and expand icons are provided in the CSS, these link to an upward arrow and a downward arrow. You will need to download these or change the URL if you want to use this.
You will notice that our accordion only goes up to a certain size. The accordions I've seen at least try to maintain the overall page scroll width by having everything the same size. But if you want variable-width accordions, it's a different story...
Since we can't animate height as freely as we'd like, we have to
resort to animating max-height
through CSS transitions.
Unfortunately, this causes a side effect: Collpasing the accordion
will be delayed. Think of max-height
as a sort of
"mask" as to how high your content can reach. When you expand the
accordion, your content will be clipped to its original size;
but there is additional animation happening that you cannot see.
When closing, the "mask" has to travel all the way up to your
content's bottom boundary before it actually closes your content.
One way to solve this is to limit the max-height
itself;
but a more elegant way to solve this is to
change how
the transitions are animated between the expanded and collapsed state.
When the accordion is collapsed, it has the transition property of
cubic-bezier(0,1,0,1)
which is a very, very sharp ease out. This will ensure that a value
close to max-height is reached quickly before slowing down, upon animating the accordion
collapsing. When in the expanded state, it has the transition property
of ease-in-out
,
which will animate the expand smoothly. By no means will it eliminate the
delay, but at least it will make the delay slightly tolerable.
To do that, you will have to change a few things in the CSS. Change the last two entries in the CSS to:
.ac-contents {
/* The accordions' contents */
padding: 0 1.5rem;
background: rgba(80,80,80,.3);
transition: max-height .5s cubic-bezier(0,1,0,1);
overflow-y: hidden;
max-height: 0;
}
[id^="accordion"]:checked + [for^="accordion"] + .ac-contents {
/* Expand the accordion to a variable size. */
transition: max-height 1.5s ease-in-out;
max-height: 999em;
}
Unlike JS-powered accordions, you can't collapse an expanded accordion, since they're radio buttons. If you want that functionality, you could add a tiny bit of JavaScript... but that's besides the point. Anyway!
Wanna open multiple things at once? Change the "radio" type to "checkbox"! This should also give you the ability to collapse an expanded accordion.
Have your text here.
Lorem ipsum dolor sit amet.
Jackdaws... something.
<div class="accordion-container">
<input id="accordion21" type="checkbox" hidden>
<label for="accordion21">
<b>Thing 1</b>
</label>
<section class="ac-contents">
<p>Have your text here.</p>
</section>
<input id="accordion22" type="checkbox" hidden>
<label for="accordion22">
<b>Thing 2</b>
</label>
<section class="ac-contents">
<p>Lorem ipsum dolor sit amet.</p>
</section>
<input id="accordion23" type="checkbox" hidden>
<label for="accordion23">
<b>Thing 3</b>
</label>
<section class="ac-contents">
<p>Jackdaws... something.</p>
</section>
</div>
You'll notice that the code doesn't make your eyes bleed as much this time.
That's because when using a checkbox, you don't need to assign a
name
to the input to link everything up. And also because
I put in less content than the one above.
Tabs are just about the same thing... except you use radios for this.
Eu copper mug gastropub kale chips master cleanse dolor beard shabby chic selvage adipisicing hammock. Commodo heirloom XOXO typewriter, consequat jianbing do esse adaptogen 3 wolf moon lo-fi fugiat vaporware est. Offal celiac elit, la croix pork belly listicle banjo id franzen messenger bag heirloom coloring book. Pitchfork lo-fi umami aliquip skateboard intelligentsia. Vinyl lumbersexual hot chicken green juice 8-bit, do affogato culpa duis fugiat cupidatat trust fund flannel normcore. Dolor bitters craft beer jianbing literally, distillery asymmetrical chambray incididunt. Mlkshk seitan consequat retro skateboard chicharrones lorem letterpress 90's.
Qui intelligentsia fam lumbersexual snackwave adaptogen banjo. Raclette do humblebrag adaptogen wolf chambray hell of cloud bread ad quis. Butcher consequat roof party anim lo-fi four loko marfa. Lomo meggings 3 wolf moon, portland dolore church-key aliquip eiusmod chicharrones est kogi man braid.
Selfies kinfolk glossier before they sold out food truck try-hard raclette street art etsy banjo keffiyeh hoodie jianbing locavore bushwick. Tousled try-hard brunch, duis nostrud dolore taxidermy. Copper mug cold-pressed gluten-free literally dreamcatcher. Shoreditch live-edge 3 wolf moon prism, laborum synth la croix incididunt gastropub enim pug try-hard yr letterpress offal. Cupidatat et typewriter leggings normcore cillum. Hammock consectetur vinyl, cred lo-fi eiusmod hella messenger bag beard photo booth. Hoodie trust fund cupidatat, vinyl prism crucifix leggings neutra messenger bag la croix copper mug esse whatever.
Distillery cray gochujang commodo art party, fashion axe heirloom seitan subway tile cardigan vaporware hell of try-hard. Helvetica mlkshk in kinfolk ullamco live-edge blue bottle. Man braid wayfarers meditation adipisicing fashion axe pork belly swag ut shaman street art. Aliquip coloring book you probably haven't heard of them craft beer. Laborum slow-carb copper mug taiyaki offal enamel pin tote bag cardigan tbh truffaut mustache man bun direct trade eu snackwave.
<input name="tgroup1" id="tab1" type="radio" hidden checked>
<label for="tab1"> Tab 1 </label>
<input name="tgroup1" id="tab2" type="radio" hidden>
<label for="tab2"> Tab 2 </label>
<input name="tgroup1" id="tab3" type="radio" hidden>
<label for="tab3"> Tab 3 </label>
<input name="tgroup1" id="tab4" type="radio" hidden>
<label for="tab4"> Tab 4 </label>
<section id="tab-content-1">
<p>
Eu copper mug gastropub kale chips master cleanse dolor beard shabby chic selvage adipisicing hammock. Commodo heirloom XOXO typewriter, consequat jianbing do esse adaptogen 3 wolf moon lo-fi fugiat vaporware est. Offal celiac elit, la croix pork belly listicle banjo id franzen messenger bag heirloom coloring book. Pitchfork lo-fi umami aliquip skateboard intelligentsia. Vinyl lumbersexual hot chicken green juice 8-bit, do affogato culpa duis fugiat cupidatat trust fund flannel normcore. Dolor bitters craft beer jianbing literally, distillery asymmetrical chambray incididunt. Mlkshk seitan consequat retro skateboard chicharrones lorem letterpress 90's.
</p>
</section>
<section id="tab-content-2">
<p>
Qui intelligentsia fam lumbersexual snackwave adaptogen banjo. Raclette do humblebrag adaptogen wolf chambray hell of cloud bread ad quis. Butcher consequat roof party anim lo-fi four loko marfa. Lomo meggings 3 wolf moon, portland dolore church-key aliquip eiusmod chicharrones est kogi man braid.
</p>
</section>
<section id="tab-content-3">
<p>
Selfies kinfolk glossier before they sold out food truck try-hard raclette street art etsy banjo keffiyeh hoodie jianbing locavore bushwick. Tousled try-hard brunch, duis nostrud dolore taxidermy. Copper mug cold-pressed gluten-free literally dreamcatcher. Shoreditch live-edge 3 wolf moon prism, laborum synth la croix incididunt gastropub enim pug try-hard yr letterpress offal. Cupidatat et typewriter leggings normcore cillum. Hammock consectetur vinyl, cred lo-fi eiusmod hella messenger bag beard photo booth. Hoodie trust fund cupidatat, vinyl prism crucifix leggings neutra messenger bag la croix copper mug esse whatever.
</p>
</section>
<section id="tab-content-4">
<p>
Distillery cray gochujang commodo art party, fashion axe heirloom seitan subway tile cardigan vaporware hell of try-hard. Helvetica mlkshk in kinfolk ullamco live-edge blue bottle. Man braid wayfarers meditation adipisicing fashion axe pork belly swag ut shaman street art. Aliquip coloring book you probably haven't heard of them craft beer. Laborum slow-carb copper mug taiyaki offal enamel pin tote bag cardigan tbh truffaut mustache man bun direct trade eu snackwave.
</p>
</section>
[for^="tab"]{
/* Unselected tab text */
border-radius:4px 4px 0 0;
padding: 8px 12px;
margin-right:.25em;
}
[id^="tab-content"] {
/* The tab contents, hidden by default */
/* Also: setting to a fixed 12 lines height */
display: none;
height: 12em;
overflow-y: auto;
border-radius:0 0 4px 4px;
padding:12px;
margin-top:3px;
box-shadow: 0 5px 30px rgba(0,0,0,.35);
}
input[id^="tab"]:checked + [for^="tab"]{
/* Selected tab text */
background: #eee;
color: #111;
background-color: #74EBD5;
background-image: linear-gradient(#9FACE6 0%, #74EBD5 100%);
}
#tab1:checked ~ #tab-content-1,
#tab2:checked ~ #tab-content-2,
#tab3:checked ~ #tab-content-3,
#tab4:checked ~ #tab-content-4{
/* The tab contents are displayed when the appropriate tabs are checked.*/
display: block;
background:#74EBD5;
color: #111;
}
What we're doing here is kind of like the accordion example from earlier. We set the name of our radio type to link these "tabs" together, then for every "tab" we follow a ID naming convention.
In our case, we chose to give our tab group name as simply
tgroup
, then we give our tabs IDs that start with
the word "tab".
Our tab buttons are simply inputs followed by their respective labels. We hide the inputs themselves, but style the labels to look like buttons.
We lay out the tab buttons as inputs right next to
each other on HTML, then our tab contents. This is done so
that our tab contents will appear below the tab buttons. This
layout will also be specified in our CSS, with the tilde selectors.
If you want your contents above the buttons, unfortunately you
couldn't reverse the layout and the selectors. One way to do it would
be to use relative positioning. You'd set the contents to be a certain
height, then the buttons can be positioned below it using
position: relative;
and top
.
Our tab contents
will also have ID names that follow our convention, like
"has to start with tab-content
". In our styles,
they will be hidden. We show them according to our checked
tab.
Wanna spoiler something? Use a checkbox there as well~
SNAPE KILLED DUMBLEDORE LOL
<input id="spoiler-1" type="checkbox" hidden>
<label for="spoiler-1">
spoiler
</label>
<div class="spoilered">
<p>
SNAPE KILLED DUMBLEDORE LOL
</p>
[id^="spoiler"] {
display: none;
}
[for^="spoiler"] {
background-color: #FBDA61;
background-image: linear-gradient(45deg, #FBDA61 0%, #FF5ACD 100%);
border-radius: 3px;
padding: 10px;
user-select: none;
font-weight: 700;
}
[for^="spoiler"]:before{
content:"View";
}
.spoilered {
display: block;
transition: max-height .5s cubic-bezier(0,1,0,1), padding .5s;
overflow-y: hidden;
max-height: 0;
margin-top:10px;
background: #334;
color: #fee;
border-radius: 3px;
}
[id^="spoiler"]:checked + [for^="spoiler"] + .spoilered {
transition: max-height 1.5s ease-in-out, padding .5s;
max-height: 999em;
padding:6px 12px;
}
[id^="spoiler"]:checked + [for^="spoiler"]:before{
content:"Hide";
}
We hide our actual input, and style its label. The ID for our
spoiler input must start with spoiler
.
The layout here follows: input, label, spoiler contents. Our CSS selectors depend on those three being one after another, in that order.
We hide the contents, and using the max-height trick described earlier, we animate it showing once we click the button.
Speaking of button, the label we specify in our HTML
is simply "spoiler". The idea is that we can change these
dynamically by using the :before
attribute. "View spoiler"
when it's hidden, "Hide spoiler" when it's shown.
Using the same exact principle, we can use checkbox to make a navbar:
<input class="nav" id="m1" type="checkbox" hidden>
<label class="nav-header" for="m1">
<strong>Header for Menu</strong>
<div class="hamburger-holder">
<span></span>
<span></span>
<span></span>
</div>
</label>
<ul class="nav-menu">
<li>Home</li>
<li>About Us</li>
<li>Contact Us</li>
<li>Products</li>
<li>Services</li>
<li>Inquiry</li>
</ul>
.nav-header, .nav-menu{
/* Set shadow and color */
box-shadow: 0 5px 15px rgba(0,0,0,.4);
color: #110746;
}
/* This is the header container */
.nav-header{
background: #2f8dda;
width:100%;
padding: .3em 1em;
display: flex;
align-items: center;
justify-content: space-between;
}
/* The navbar title. */
.nav-header strong{
display: inline-block;
}
/* The menu itself */
.nav-menu{
overflow-y: hidden;
padding-left: 0;
margin: 0;
max-height: 0;
transition: max-height .5s cubic-bezier(0, 1, 0, 1);
}
/* The menu items */
.nav-menu li{
background: #fff;
list-style: none;
padding: .7em 1em;
transition: background .2s;
}
.nav-menu li:hover{ background: #eee; }
/* This will make the menu expand when the header is clicked */
.nav:checked + .nav-header + .nav-menu{
max-height: 99em;
transition: max-height .8s ease-in-out;
}
/* This bit is for the hamburger icon. */
.hamburger-holder{
display: inline-block;
padding:.5em;
}
.hamburger-holder span{
background: #110746;
width: 1em;
height: .2em;
display: block;
margin: .25em;
border-radius:5px;
transition: transform .5s, opacity .5s, margin .5s;
transform-origin: left;
position: relative;
}
.nav:checked + .nav-header .hamburger-holder span:nth-of-type(1){
transform: rotate(45deg);
left:.125em;
top: .1em;
}
.nav:checked + .nav-header .hamburger-holder span:nth-of-type(2){
opacity: 0;
}
.nav:checked + .nav-header .hamburger-holder span:nth-of-type(3){
transform: rotate(-45deg);
left:.125em;
bottom: .1em;
}
/* These are for the rounded rect effects, get rid of these if y'want */
.nav-header{ border-radius: 4px; }
.nav:checked + label{ border-radius: 4px 4px 0 0; }
.nav-menu{ border-radius: 0 0 4px 4px; }
We have a checkbox which is set as hidden in the HTML, so screenreaders and us can't see it. We don't need to put a name for the checkbox, since we're not using a radio input. We apply a label to it, which will be the menu header along with the little hamburger icon. The hamburger icon is made with invisible spans which won't be read by a screen reader, as well as not shown on a dumb terminal. After that, we have a standard listing as a navigation. The navigation is split from the label to make things a little more logical when the styles fail to load.
Again, you will want to add an ID, since this is what the label element will use to attach to the input. Try to make it as small as possible, it's only for linking. If the class names makes the HTML look redundant, that's because you may want to use this stuff multiple times. Sure, you can omit the class names and go straight to making IDs, but the consequence will be an even larger CSS stylesheet, since you will have to add more ID definitions. It also won't be dynamic enough.
Once again, our CSS relies on the fact that we arranged our HTML in the "input, label, elements" order. If we used the ~ operator, we may have undesired effects. You could try to solve it by wrapping a div around the whole thing, but I'm not going to do that here. Some styles here can stand on their own, but the functionality relies on the elements being set up in that order.
These are tricks I've gathered from a variety of sources, hope it'll help you use less JavaScript for your sites.
Furthermore, this should help dispel the notion that a modern looking website must use scripting for 100% of its features. Give love to the security-minded!
Granted, these tricks will only work if you're using a browser that belongs to this era...