HTML and CSS can solve many problems, but JavaScript is also very important.

Written byKalanKalan
💡

If you have any questions or feedback, pleasefill out this form

This post is translated by ChatGPT and originally written in Mandarin, so there may be some inaccuracies or mistakes.

In 2017, I read an article – Effective Frontend 1: Use HTML/CSS to Solve Problems Instead of JS. I felt a strong resonance with it during my first reading and learned many techniques I wasn't familiar with at the time. I highly recommend everyone take a look. Although JavaScript can solve most problems, from the perspective of accessibility, performance, and bundle size, it is generally better to use CSS where possible. However, trying to avoid JS doesn't mean we should completely eliminate it; there is still a distinction between the two. This article will revisit the aforementioned piece and highlight some areas I believe could be improved.

Using :hover for Tooltip Styles

Indeed, using :hover to indicate to users that a UI component is interactive is considered basic knowledge for front-end engineers. The article also mentions achieving dropdown menu effects with :hover.

<div data-editor data-tab="css" hidden>
  <div data-style>
.dropdown {
  position: relative;
  display: inline-flex;
  justify-content: space-around;
  gap: 1em;
}
.item { display: none; }
.dropdown-item:hover + .item {
  display: block;
  position: absolute;
  top: 30px;
  left: 0;
  padding: 10px;
  background-color: #efefef;
}
  </div>
<div data-body>
<div class="dropdown">
  <a href="#" class="dropdown-item">Contact Me</a>
  <div class="item">
    <ul>
      <li>Item1</li>
      <li>Item2</li>
    </ul>
  </div>
</div>
</div>
</div>

Changing display: block on hover, while the default is display: none, seems straightforward. But what if the user isn't navigating with a mouse? If users navigate with a keyboard, :hover has no effect. Additionally, it limits the DOM structure; the UI elements that need to be triggered must be adjacent to the dropdown list.

Therefore, my suggestion is that when using :hover to indicate that an element can be interacted with, consider how users who do not navigate with a mouse should interact. This includes:

  • Adding :focus or listening for click events to allow users to trigger the dropdown menu
  • Incorporating aria-expanded to inform screen readers of the current state of the menu, and adding keyboard navigation so users can control options with the up and down keys
<div data-editor hidden data-tab="markup">
<div data-style>
.dropdown {
  position: relative;
  display: inline-flex;
  justify-content: space-around;
  gap: 1em;
}
.item {
  display: none;
}
.dropdown-item:hover + .item,
.dropdown-item:focus + .item {
  display: block;
  position: absolute;
  top: 30px;
  left: 0;
  padding: 10px;
  background-color: #efefef;
}
</div>
<div data-body>
<div class="dropdown">
  <button id="toggle" href="#" class="dropdown-item">Contact Me</button>
  <div class="item" id="dropdown-1" aria-expanded="false">
    <ul>
      <li>Item1</li>
      <li>Item2</li>
    </ul>
  </div>
</div>
</div>
<div data-js>
function toggleExpanded() {
  const node = document.querySelector('#dropdown-1');
  const prev = node.getAttribute('aria-expanded');
  node.setAttribute('aria-expanded', prev === 'true' ? 'false' : 'true');
}
const toggle = document.querySelector('#toggle');
toggle.addEventListener('focus', toggleExpanded);
toggle.addEventListener('blur', toggleExpanded);
toggle.addEventListener('mouseover', toggleExpanded);
toggle.addEventListener('mouseleave', toggleExpanded);
</div>
</div>

In this example, in addition to using hover to trigger the Dropdown, JavaScript is used to listen for focus and mouseover events to adjust aria-expanded. Keyboard navigation is not implemented here as it falls outside the scope of this discussion. Additionally, if using aria-expanded, the CSS can be adjusted to:

.dropdown-item:hover + .item,
.item[aria-expanded="true"] {
  /* style */
}

Custom Styles with :checked and Adjacent Selectors

To implement a custom checkbox or radio button, the techniques mentioned in the article will be necessary, utilizing pseudo-classes along with adjacent selectors for easy customization of checkboxes.

The benefit of using :checked is that we don't need to register additional event listeners to toggle classes. If you want to create custom checkboxes or radios, this approach should be prioritized over building from scratch with <div>, which involves many considerations and may not offer better usability than a slightly unattractive but functional checkbox.

However, when using this method, take note of a few points:

  • Use aria-label to inform screen readers about the purpose of the checkbox or radio (or use aria-labelledby)
  • Use <div role="status"></div> or another method to notify about value changes (if necessary)
  • Implement focus style handling

Screen readers will only announce the label name and whether the checkbox is selected. If the checkbox's purpose is not simply to toggle between selected and unselected (for example, for switching dark mode), adding additional context will help screen readers understand better.

<div data-editor data-tab="markup" hidden>
<div data-style>
.label:has(input:focus-visible) {
  outline: 2px solid blue;
}
.track {
	position: relative;
	display: inline-block;
	width: 50px;
	height: 1.2rem;
	border-radius: 9999px;
	background-color: #778da9;
	cursor: pointer;
}
.track .cursor {
  position: absolute;
  left: 0;
  top: 1px;
  display: inline-block;
  width: 1.1rem;
  height: 1.1rem;
  border-radius: 50%;
  border: 1px solid #efefef;
  transition: transform 0.3s ease-in;
  background-color: #fff;
}
.checkbox .cursor {
  transform: translateX(0);
}
.checkbox:checked~.track .cursor {
  transform: translateX(1.3rem);
}
.checkbox:checked~.track {
  background-color: #778da9;
}
/* keep hidden but focusable */
.hidden {
  position: absolute !important;
  width: 1px !important;
  height: 1px !important;
  padding: 0 !important;
  margin: -1px !important;
  overflow: hidden !important;
  clip: rect(0, 0, 0, 0) !important;
  white-space: nowrap !important;
  border: 0 !important;
}
</div>
<div data-body>
<label for="toggle" class="label">
  <input type="checkbox" class="checkbox hidden" id="toggle" aria-label="toggle"/>
  <span class="track"><span class="cursor"></span></span>
</label>
<div role="status" id="status"></div>
</div>
<div data-js>
toggle.addEventListener('change', function(e) {
  const node = document.getElementById('status');
  node.innerHTML = 'Theme has been toggled';
});
</div>
</div>

To keep the input hidden while allowing it to maintain focus, we don't use display: none directly but instead use other CSS properties to hide it. Additionally, to style the focus during keyboard navigation, we’ve included :focus-visible, which means the focus styles will only apply when navigating with the keyboard (e.g., using the Tab key), so clicking won’t trigger the focus outline.

Equal Height Columns

This article was written in 2016. While the methods discussed are valid, they feel somewhat old-fashioned. In 2022, with flex support being robust, we can directly use flexbox to solve this issue. If you need finer control, you could opt for grid.

The principle is that due to the nature of flexbox layout, align-items defaults to stretch, causing the container's height to be determined by the tallest item in the same row. It's important to remember that if multiple rows are needed, you should add flex-wrap: wrap; otherwise, flexbox will attempt to fit everything into a single row by default.

<div data-editor data-tab="css" hidden>
<div data-style>
.container {
  max-width: 1200px;
  width: 95%;
  margin: 0 auto;
  display: flex; /* comment me */
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 1em;
}
.card {
  width: 30%;
  // display: inline-block; /* comment me */ 
  padding: 8px;
  background-color: #efefef;
}
.card img {max-width: 100%;}
</div>
<div data-body>
<div class="container">
  <div class="card">
      <img src="https://unsplash.com/photos/hwLAI5lRhdM/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8Mnx8amFwYW58amF8MHx8fHwxNjY5NTE4NjA1&force=true&w=640" />
    <h3>Information</h3>
    <p>This photo is taken by Unsplash Clay Banks</p>
  </div>
  <div class="card">
    <img src="https://unsplash.com/photos/alY6_OpdwRQ/download?ixid=MnwxMjA3fDB8MXxzZWFyY2h8OHx8amFwYW58amF8MHx8fHwxNjY5NTE4NjA1&force=true&w=640"/>
    <h3>Tokyo</h3>
    <p>This photo is taken by Unsplash Jezael Melgoza.</p>
    <p>I like Tokyo because it's such a great place where I can see people melt down.</p>
  </div>
</div>
</div>
</div>

Form Submission

I completely agree with the author that many people overlook the fact that the native <form> element has been around for decades and that defining form content using <form> can save a significant amount of JavaScript code.

It mentions utilizing the browser's native form validation mechanism in combination with the :invalid pseudo-class for styling purposes. In the example, the submit button is set to opacity: 0.5 when in an invalid state. However, probably due to the example context, the author uses <span> instead of <button>. In practice, <button> should be used, and JavaScript should be employed to add disabled when the input values are invalid.

If you’re not yet familiar with forms, you can refer to the following articles:

Making Good Use of Pseudo-Classes

The author mentions pseudo-classes like :checked, :focus, :invalid, etc. Effectively using these pseudo-classes can save unnecessary JavaScript and make the code easier to read.

Regarding pseudo-classes, I also wrote an article introducing some relatively new pseudo-classes. If you're interested, you can check it out: Useful Pseudo Classes for Layout.

Conclusion

2017 was right when I was just starting out in front-end development, and my grasp of detail handling was not strong. Looking back now, I realize that creating a good user experience requires attention to many details; it’s not simply a matter of applying CSS. Often, for the sake of accessibility, JavaScript is an indispensable component.

If you found this article helpful, please consider buying me a coffee ☕ It'll make my ordinary day shine ✨

Buy me a coffee