Progressive enhancement
Time to read
, 1975 words, 4th grade
Now that weʼve seen how semantic HTML alone produces a fully-responsive web page, where do we go next? Our plain HTML page is ugly as sin, so weʼre gonna need some CSS to spiff it up. But today letʼs keep the CSS simple. Weʼll return to good CSS practices soon in a follow-up article.
On this page
Start with the HTML
To begin, letʼs start with some essential HTML:
You can see an example page here.
No doubt you have spotted the class
attributes. We prefer to
apply
CSS
by class. We can fall back to class-and-tag-name
if we need more
specificity, as youʼll see below.
So whatʼs up with the “xx-” prefix?
Simple. To avoid name clashes with imported CSS, we namespace our classes. For example, the Craft Code site might use “cc-” as in cc-accordion.
Of course,
if your site is a simple one, that might be overkill.
Add the initial CSS
Letʼs add a bit of CSS to give it that “accordion” look and feel. Here we go:
Again, we provide an example page. We never claimed that it would be pretty.
This is already a workable accordion component. But most accordions have more than one expandable element. So letʼs add a few more elements:
We have also adjusted the CSS a bit to make it work with multiple elements:
-
We added the
.xx-accordion-group
properties. And moved the outer border from the individual elements to the group. We also changed the border color to match the<summary>
background color. -
We removed the top border from the content block. Then we replaced it
with a bottom border on the
<summary>
element. We also changed the color to the background color of the<details>
elements. This makes it appear as a horizontal rule between the accordion elements. -
We donʼt want that bottom border on the last element in the accordion group. So we used
.xx-accordion:last-child summary
to remove that one bottom border.
Here is the full accordion example.
Enhance the accordion
Rats! The accordion elements open and close instantly when you click on the summary. It would be nice if we animated the open and close.
We bet that your first thought will be that we can do this with CSS animations. We thought so, too. But it turns out that it is fiendishly difficult to make that work. By that we mean that we couldnʼt. Sigh.
CSS should always be your first choice for effects, but when CSS fails you, JavaScript steps up. We can do it the way we used to.
CSS transitions for the loss
Naturally, the first thing we tried was a CSS transition on the height of the content block. But this only works if you know the
height of the block in advance. It wonʼt work (for
us, anyway) with height set to auto
or fit-content.
OK, fine! Instead we set the open height to a CSS property with a
fallback to auto:
var(--xx-accordion-height, auto)
. That oughta do it.
Now we need to set that property for each accordion element separately. Enter JavaScript!
JavaScript to the rescue
The first thing we want to do is write a function that grabs any and all accordion elements on the page. Then weʼll add an event listener to each of them. And we want our function to run once as soon as the DOM has finished loading.
Here, then, is our first pass at the JavaScript:
And our modified CSS:
And it does work. Beautifully … but intermittently. Try it. And then it stops working at all. And it never animates the close. Why not?
Because when the <details>
element toggles closed,
it sets its “open” value to
false
. And thatʼs that.
Slam!
OK, fine. Time to show it whoʼs boss.
Kicking butt and taking names
First, letʼs delete the CSS we added. Forget using a transition! Weʼll do this the old-fashioned way.
First issue: when the <details>
element toggles shut,
it closes the accordion abruptly. We need to prevent that. To do so, we will
seize control.
Instead of handling the toggle
event, letʼs capture
the click
on the <summary>
element. Then we will prevent the click from bubbling up.
We still need to know the height of the open accordion, but we donʼt need it in our CSS. So letʼs use a property
instead. Weʼll call it xxOpenHeight
. (Do remember to
replace xx
with your own namespace.)
Check out the enhanced example and open the browser console. Note how clicking on the a summary prints the height of that accordionʼs content block to the console. Weʼre off to a good start.
Now letʼs complete the toggleAccordion
function to
animate the accordion elements. Weʼll explain this
code line by line below.
Letʼs take this one segment at a time. When we break bigger items into smaller ones, comprehension becomes easy.
Breaking it down
We need to prevent the element from toggling until we want
it to. We can do this with event.preventDefault()
on line #2
above. Now
we are responsible for opening and closing the accordion ourselves.
Now we set three constants to control the speed at which the accordion
opens and closes. DELAY
is the number of milliseconds between
calls to our recursive functions (below). The shorter the
delay, the more rapidly we call the function recursively.
OPEN_INCREMENT
and SHUT_INCREMENT
set the number
of pixels that the accordion opens or shuts on each recursive call. The larger
the number, the bigger the jumps and the faster the accordion opens or closes.
The trick to these is to play with the settings until you get a smooth open/close animation at a rate that feels right.
We can get the target
from the event
(line #1).
This is the HTML summary
element on which we clicked. From that,
we get the parent details
element using summary.closest("details")
(line #2).
We can use the querySelector
method (line #3) on the details
element to grab the div
containing the accordion content. Finally, on line #4, we get the xxOpenHeight
value that we set as a property on the
details
element previously.
Now we have the elements we need and the open height of the accordion content.
Next we use a simple conditional to check that we found the details
,
summary
, and content div
elements. If not, we return
immediately from the function as there is nothing we can do. We canʼt manipulate elements that donʼt exist.
We are going to need a shutAccordion
function to animate the
closing of the accordion. We will call this function recursively. Each call
will close the accordion by
SHUT_INCREMENT
pixels after a delay of DELAY
.
On line #2, we get the current height of the content block and parse it into an integer.
We are going to continue the recursion only if our height is greater
than zero. So we wrap the recursive call in a conditional block, lines
#4 to #12. This ensures that the recursion only happens when currentHeight > 0
.
Inside the conditional, on lines #5 to #7, we set the height of the
content block. If the current height is less than the SHUT_INCREMENT
, then we set the height to 0
. If not, then we set the
height to the current height minus the SHUT_INCREMENT
.
We have now shrunk the content block height by SHUT_INCREMENT
pixels. Next, we call the shutAccordion
function recursively
after a delay of DELAY
ms.
Then we return immediately from the current function call, so the code
on line #14 above is not applied. When the currentHeight
equals
0
, we skip the conditional block. Thatʼs when the code
on line #14 sets the open
property to false
.
This permits other event handlers to run.
We need to decide when to call our shutAccordion
function. As this conditional makes clear, we call it when the accordion
is open
.
Inside the conditional we need to make sure that the content block is
fully open. We set the current content block maxHeight
to the
openHeight
we measured earlier. Then we call shutAccordion
and immediately return. Returning prevents the openAccordion
function from running as well.
Speaking of which, we also need a openAccordion
function to
animate opening the accordion.
First, we set accordion.open
to true
. Letʼs not forget that. Then, on line #4, we get the current height of the
content block.
If the current height is less than the openHeight
, then the block is not yet fully open. We increment that height by OPEN_INCREMENT
pixels. Then we recursively call openAccordion
after DELAY
milliseconds.
Note that this code is never executed if the accordion is closed. So we
donʼt need to wrap the following code in another conditional or an else
block. Letʼs finish up.
First, letʼs ensure that the accordion is fully closed by setting
the maxHeight
on the content block to 0px
(line
#1).
Then we call openAccordion()
on line #3 to start the open animation.
Easy peasy when you break it all down!
Putting it all together
Try the final example above. You might notice a problem. The content of the content block becomes visible immediately rather than appearing as the accordion opens. This happens even though the height of the block is zero.
This happens because we havenʼt hidden the overflow. We can do that by adding overflow-y: hidden
to the .xx-accordion-content
CSS.
Once we do that, then our accordion should animate flawlessly. Now disable JavaScript in the DevTools of your browser. Check it out. It still works. It doesnʼt animate. So what? We can live with that, right?
And if you disable both CSS and JavaScript, then it still works fine! It even works on the Lynx text-only browser, which doesnʼt recognize the <details>
element. All that means is that the accordion is always open.
Again, we can live with that.
Thatʼs progressive enhancement for you. Our code is simple and responsive and works for everyone. And if you have CSS and JS enabled, then you get a nicer experience.
Note: we have edited this content to improve it on 7 March 2024.