Web Performance Calendar

The speed geek's favorite time of year
2025 Edition
ABOUT THE AUTHOR
Aaron T. Grogg photo

Aaron T. Grogg (in/aarontgrogg, @aarontgrogg.bsky, @aarontgrogg) is a Web Developer / Performance Optimization Specialist

For many years now, JavaScript has been the workhorse of the web. If you wanted to do something that couldn’t be done with just HTML and CSS, you could usually find a way to do it with JS.

And, for the most part, that has served us well. JS has certainly helped push the user experience forward, and has even helped push HTML and CSS forward!

But as new HTML and CSS methods gain traction, we need to consider replacing some “standard” JS patterns with new methods that require no, or lo, JS.

I have nothing against JS, but it has better things to do than deal with your accordions or offscreen navigation menus…

Plus, once JS has been downloaded, it often needs to be decompressed, evaluated, processed, and then usually continues to consume memory in order to monitor page and user activities.

If we can hand-off any JS functionality to native HTML or CSS, then users can download less stuff, and the remaining JS can pay attention to more important tasks that HTML and CSS still can’t handle.

Below are a few examples, and I hope to soon reveal a design library of as many components as I can think of that were once the purview of JS, but can now be handled with no, or very little, JS… Stay tuned!


Table of Contents:


Accordions / Expanding Content Panels

This is a very common, practical pattern: Click a thing, reveal some additional content, typically via a “slide down” action or similar.

Description:

The details and summary HTML elements provide an HTML-only replacement to the typical JS accordion:

CopePen: codepen.io/aarontgrogg/pen/GgoOqVX

Use cases:

  • Hiding/showing content
  • Expanding content sections

Basic implementation:

<details>
  <summary>Initially closed, click to open</summary>
  Content is initially hidden, but can be revealed by clicking the summary.
</details>

Add an open attribute to set the default appearance as “open”:

<details open>
  <summary>Initially open, click to close</summary>
  Content is initially visible, but can be hidden by clicking the summary.
</details>

Use the same name attribute on all related details (like radio buttons) to restrict only one open panel at a time:

<details name="foo" open>
  <summary>Initially open, clicking others will close this</summary>
  Content is initially visible, but can be hidden by clicking the summary; only one panel can be open at a time.
</details>
<details name="foo">
  <summary>Initially closed, clicking will open this, and close others</summary>
  Content is initially hidden, but can be revealed by clicking the summary; only one panel can be open at a time.
</details>
<details name="foo">
  <summary>Initially closed, clicking will open this, and close others</summary>
  Content is initially hidden, but can be revealed by clicking the summary; only one panel can be open at a time.
</details>

TIP: You can also customize the appearance with CSS, including animating the open/close behavior and customizing the marker, and trigger the open/close via JS.

Resources:

Browser compatibility:


Expanding and Contracting Form Field Elements

Form field element size has always been static, initially based on HTML attributes, later made more flexible via CSS, but it was never fluid.

Now one line of CSS can do just that.

Description:

Allow textarea, input or select elements to expand or contract to fit the content within them, without JS listeners monitoring key strokes and updating inline styles:

CopePen: codepen.io/aarontgrogg/pen/myPGBWG

Use cases:

  • Textarea, email address or URL field that expands as a user types
  • Select element that can reduce to only the size needed
  • Especially helpful with “inline” form fields (see Ahmad’s examples in the Resources section below)

Basic implementation:

<textarea>
  As content is added here, this element will expand to display it.
</textarea>
textarea {
  field-sizing: content;
}

This can also be applied to input elements to allow them to expand horizontally as the user types (handy for email addresses or long URLs), or for select elements to allow the to collapse to expand to snugly fit the currently selected option.

Resources:

Browser compatibility:


Input with Autofilter Suggestions Dropdown

This has long been the purview of JS: Type into a text box and have a list appear, like a select, but also be able to free-type an option, and have the dropdown list “filter” as you type.

This can only be done with pure JS magic, right?

Description:

Combining the HTML input and datalist elements can create a dropdown of options that autofilters as you type:

CodePen: codepen.io/aarontgrogg/pen/yyePPor

Use cases:

  • Site search
  • Product search or filter
  • Filter any list of data

Basic implementation:

<label for="browser">Browser</label>
<input type="text"
       list="browsers"
       id="browser" name="browser"
       size="50"
       autocomplete="off" />
<datalist id="browsers">
  <option value="Arc"></option>
  <option value="Brave"></option>
  <option value="Chrome"></option>
  <option value="DuckDuckGo"></option>
  <option value="Firefox"></option>
  <option value="Microsoft Edge"></option>
  <option value="Opera"></option>
  <option value="Safari"></option>
  <option value="Tor"></option>
  <option value="Vivaldi"></option>
</datalist>

You can also use other input types:

<label for="quantity">Quantity</label>
<input type="number" 
       list="quantity-options"
       id="quantity" name="quantity" />
<datalist id="quantity-options">
  <option value="1"></option>
  <option value="2"></option>
  <option value="5"></option>
  <option value="10"></option>
  <option value="20"></option>
  <option value="50"></option>
</datalist>

<label for="appointment">Appointment</label>
<input type="time" 
       list="appointments"
       id="appointment" name="appointment" />
<datalist id="appointments">
  <option value="12:00"></option>
  <option value="13:00"></option>
  <option value="14:00"></option>
</datalist>

NOTE: At the time of this writing, Firefox was limited to only textual-based input types, so no date, time, range or color for now… 🙁

Resources:

Browser compatibility:


Modals / Popovers

A very familiar UI pattern: click a button, display a content panel above all other content, typically preventing interaction with the content behind it.

Description:

The popover and popovertarget attributes can replace the traditional JS-driven modal/popover/overlay:

CodePen: codepen.io/aarontgrogg/pen/QwyOKNW

Use cases:

  • Hiding/showing side panels / additional information

Basic implementation

An auto popover (default) can be “light dismissed” (clicking outside of it or hitting the esc key). Opening an auto automatically closes any other auto popovers that were open.

<button popovertarget="pop-auto">
  Toggle Popover
</button>
<dialog popover id="pop-auto">
  I'm an "auto" Popover!
</dialog>

A manual popover can not be “light dismissed”. It does not close other manual popovers when opened. Clicking the button a second time will close the one it opened.

<button popovertarget="pop-manual">
  Toggle Popover
</button>
<dialog popover="manual" id="pop-manual">
  I'm a "manual" Popover!
</dialog>

You can also add a button to close the popover by giving it the same popovertarget as the button that opened it:

<button popovertarget="pop-manual">
  Toggle Popover
</button>
<dialog popover="manual" id="pop-manual">
  I'm a "manual" Popover!
  <button popovertarget="pop-manual">×</button>
</dialog>

NOTE: There is also a popover="hint", but it is currently inconsistently implemented so I recommend against using it.

Resources:

Browser compatibility:


Offscreen Nav / Content

Building on the pattern above, content slides in from one side of the screen or another.

Description:

The Modal / Popover functionality can also be used to create an offscreen navigation that requires no JS:

CodePen: codepen.io/aarontgrogg/pen/wBMPMVG

Use cases:

  • Hiding/showing navigation menus

Basic implementation:

<button popovertarget="menu">
  Toggle Menu
</button>
<nav popover id="menu" role="menu">
  Nav Content
</nav>
#menu {
  margin: 0;
  height: 100vh;
  translate: -100vw;
}
#menu:popover-open {
  translate: 0;
}

I use a nav element to give it semantic value, but you can use any HTML element (div, section, aside, etc.).

A popover defaults to position: fixed per the User Agent Stylesheet, and is simply pushed off screen when closed, and pulled back onscreen when it is open. Note that margin: 0 is required if you want to override the User Agent center-alignment.

Clicking outside of the above menu closes it. You can force the panel to stay open, requiring a manual/explicit close, by using popover="manual".

You can also add a backdrop pseudo element and style it as you wish:

#menu::backdrop {
  background: rgb(190 190 190 / 75%);
}

If you choose to make your offscreen nav a manual (requiring an explicit close), you can also add a close button that has the same popovertarget as the button that opened it:

<button popovertarget="menu">
  Toggle Menu
</button>
<nav popover id="menu" role="menu">
  <button popovertarget="menu">×</button>
  Nav Content
</nav>

Resources:

Browser compatibility:


Smooth Scrolling

Rather than a sudden, jarring “jump” from one place in a page to another, perform a smooth scroll. This also helps the user realize the context of what is happening.

Description:

Add one line of CSS to the html element to add smooth scrolling without JS click listeners and scrollTo animations:

CopePen: codepen.io/aarontgrogg/pen/OPNovZR

Use cases:

  • Hyperlink jumps within the same page
  • Deep-links that land in a new page

Basic implementation:

@media (prefers-reduced-motion: no-preference) {
  html {
    scroll-behavior: smooth;
  }
}

IMPORTANT: Notice that this declaration is placed within a media query that checks whether or not the user prefers-reduced-motion. Any animation in a page should be placed within a similar check, as motion can cause some people to have a very bad experience. And other people just may not enjoy motion like that, so, we might as well just be nice. 🙂

TIP: After arriving at the “scrolled-to” location, you might find that the scrolled-to content is too close to the top of the browser window. And if you have a fixed top menu, it might even appear behind that menu. Add a scroll-margin-top or scroll-padding-top to give the :target a little “room to breathe”:

:target {
  scroll-margin-top: 100px; /* Or whatever space you need */
}

Resources:

Browser compatibility:


Sticky Content

Whether it is a header, side navigation menu, or table of contents, having a bit of content that sometimes flows with the document, and sometimes sticks in place, always required a JS IntersectionObserver to monitor your scroll position, measure the distance between elements, and update inline styles; no more.

Description:

Add one line of CSS to the element that you want to “stick”, and it will flow with the document until it hits its container’s edge, then it will just “stick” there, until its container also scrolls, all without any JS:

CopePen: codepen.io/aarontgrogg/pen/OPNovZR

Use cases:

  • Keep the global header stuck to the top of the browser window
  • Keep section headers visible while scrolling
  • Keep table column headers visible while scrolling
  • keep side nav or related content visible while scrolling

Basic implementation:

<section>
  <h2>Section Header 1</h2>
  <p>As you scroll, the header above will "stick" to the top of the browser window until the bottom of this section scrolls past the top of the browser window.</p>
</section>
<section>
  <h2>Section Header 2</h2>
  <p>And once the first section scrolls past the top of the browser window, the header from this section will "stick" to the top of the browser window, until the bottom of this section scrolls past the top of the browser window.</p>
</section>
h2 {
  position: sticky;
  top: 0;
}

This can also be used to make global headers stick to the top of the browser window, or to make things like side panels sort of “float” while content around them scrolls.

TIP: You can use any value for top, if you wanted something to float a little lower in the page.

WARNING: There are a few “gotchas” to keep an eye on.

Resources:

Browser compatibility:


Summation

While we all love the power and flexibility JS provides, we should also respect it, and our users, by limiting its use to only what it needs to do.

These are all super simple examples, there is sooo much more that has changed in recent years, especially in the world of CSS!

If you are hungry for more, I am currently working on a design library to demonstrate as many NoLoJS patterns as I can find; stay tuned!

Happy reducing!
Atg