Kalan's Blog

Software Engineer / Taiwanese / Life in Fukuoka

Current Theme light

In 2017, I read an article titled Effective Front-end 1: Use HTML/CSS to Solve Problems Instead of JavaScript, which resonated with me and taught me many useful techniques. I highly recommend everyone to give it a read. Although JavaScript can solve most problems, from the perspective of accessibility, performance, and bundle size, it is better to use CSS whenever possible. However, "avoid using JS as much as possible" does not mean completely eliminating its usage. There are still some differences between the two approaches. In this article, I will revisit the mentioned article and point out some areas that I believe can be improved.

Using :hover for Tooltip Styles

Indeed, using :hover to indicate interactive UI elements is common knowledge for frontend developers. The article also mentioned using :hover to achieve a dropdown menu effect.

.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;
}

By changing display: block on hover and display: none in the default state, we can achieve the desired effect. However, what if the user is navigating without a mouse? The :hover pseudo-class will not have any effect in that case. Moreover, it is limited to the DOM structure, requiring the triggering UI element to be adjacent to the dropdown list.

Therefore, my suggestion is to consider alternative interactions when using :hover to indicate interactive elements. This may include:

  • Adding :focus or listening for click events to allow users to trigger the dropdown menu.
  • Adding aria-expanded to inform screen readers about the current state of the menu and adding keyboard navigation to allow users to navigate options using the arrow keys.
.dropdown-item:hover + .item,
.dropdown-item:focus + .item {
  display: block;
  position: absolute;
  top: 30px;
  left: 0;
  padding: 10px;
  background-color: #efefef;
}

In this example, in addition to using :hover, JavaScript is used to listen for focus and mouseover events to adjust the aria-expanded attribute. Keyboard navigation is not implemented as it is beyond the scope of this article. Additionally, if using aria-expanded, the CSS can be adjusted as follows:

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

Customizing Styles with :checked and Adjacent Selectors

If you want to implement custom checkboxes or radio buttons, the technique mentioned in the article using pseudo-classes and adjacent selectors can be handy.

The advantage of using :checked is that we don't need to add additional event listeners to toggle classes. When creating custom checkboxes or radio buttons, it is recommended to prioritize this approach over creating them from scratch using <div>. Not only does it reduce complexity, but it also ensures usability compared to visually unappealing checkboxes that still function properly.

However, when using this approach, there are a few things to consider:

  • Use aria-label or aria-labelledby to provide a description of the purpose of the checkbox or radio button for screen readers.
  • Use <div role="status"></div> or other methods to notify value changes if necessary.
  • Implement styles for focus.

When screen readers read checkboxes, they only announce the label name and whether it is checked or not. If the purpose of the checkbox is not simply selecting or deselecting (e.g., toggling a dark theme), providing additional hints can make it easier for screen readers to understand.

.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;
}

To hide the input element while preserving the focus effect, display: none is not used. Instead, other CSS properties are used to hide it. Additionally, :focus-visible is added to apply focus styles only when using keyboard navigation (e.g., tab) and not when clicking.

Equal Height Columns

Although the methods mentioned in the article are feasible, they are somewhat old-school. In 2022, with the widespread support for flexbox, we can directly use flexbox or, for more granular control, grid to achieve equal height columns.

The principle is to utilize the align-items property, which defaults to stretch, in flex layout. The height of the container will be based on the tallest element in the same row. It is important to note that if multiple rows are needed, flex-wrap: wrap should be added; otherwise, flexbox will attempt to fit all elements into a single row by default.

.container {
  max-width: 1200px;
  width: 95%;
  margin: 0 auto;
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-start;
  gap: 1em;
}
.card {
  width: 30%;
  padding: 8px;
  background-color: #efefef;
}
.card img {
  max-width: 100%;
}

Form Submission

I agree with the author's point that many people overlook the fact that native HTML forms (<form>) have been around for decades. Defining form content using <form> can save a significant amount of JavaScript code.

The article mentioned using the browser's native form validation mechanism combined with the :invalid pseudo-class for styling. In the example, the submit button's opacity is set to 0.5 when in an invalid state. However, in practice, it is recommended to use a <button> element and add the disabled attribute through JavaScript when the input values are invalid.

If you are unfamiliar with forms, you can refer to the following two articles:

Leveraging Pseudo-classes

The author mentioned the use of pseudo-classes such as :checked, :focus, and :invalid. Utilizing these pseudo-classes can reduce the need for unnecessary JavaScript and make the code easier to read.

I have also written an article introducing some relatively new pseudo-classes related to layout. If you are interested, you can check it out: Useful Pseudo-classes for Layout

Conclusion

In 2017, when I was just starting out in frontend development, I had a limited understanding of the details. Looking back now, I realize that creating a well-designed UI requires considering many details. It's not just about applying CSS; often, JavaScript plays an essential role for accessibility considerations.

Prev

Pseudo-classes used for styling

Next

2022 Advent Of Code: Cathode-Ray Tube

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

Buy me a coffee

作者

Kalan 頭像照片,在淡水拍攝,淺藍背景

愷開 | Kalan

Hi, I'm Kai. I'm Taiwanese and moved to Japan in 2019 for work. Currently settled in Fukuoka. In addition to being familiar with frontend development, I also have experience in IoT, app development, backend, and electronics. Recently, I started playing electric guitar! Feel free to contact me via email for consultations or collaborations or music! I hope to connect with more people through this blog.