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
oraria-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.