Web widgets are something everyone needs for their site. They’ve been done a hundred ways over the years, but essentially it’s some bundle of HTML, CSS, and JS. The problem is that there are so many ways of doing this badly. The worst is “semantic” classes and JS hooks. Semantic-ish markup is used in quick-start frameworks like Bootstrap and Materialize, and encouraged by some frontend devs as “best practices.”
Simaantec Makrpu
Semantic markup: semantic to who? It’s not like your end-users are gonna read this stuff. Google doesn’t care, outside of the element types. And it’s certainly not “semantic” for your fellow devs. Have a look at the example CodePen linked from this post.
This CodePen represents the html, css, and js for two sections of our Instagram-clone web site. We have a posts-index
page and a post-page
; two separate pages on our site that both display a set of posts with an image and some controls using semantic patterns. Some notes on how it works:
- Does the
post
class name do anything? It’s semantic, and tells us this section is a post. But that was probably obvious because the html is in_post.html.erb
orpost.jsx
or something, so it’s not saying anything we didn’t already know. - What does a post look like? What styles might it have applied? Can’t tell from reading the html. Is it laid out using
float: left
ortext-align
orinline-block
orflexbox
? I’d have to find the css and figure it out. - Once I do figure it out, I need to look even further to see what
post featured
changes. Isfeatured
something that only applies topost
, or canfeatured
be applied to anything? If I makeuser featured
will that have conflicts? - Why are
post
andfeatured
at the same level of CSS specificity? Their rules will override each other based on an arcane system no one but a dedicated CSS engineer will understand. - The
.post{img{}}
pattern is a land mine for anyone else. What if I want to add an icon indicating file type for that post? It’s going to get all the styles of the post image on my icon, and I won’t know about these style conflicts until it looks weird in my browser. I’ll have to “inspect element” in the browser or grep for it in the CSS, and figure out how to extract or override them. What if I want to add a “fav” button to each post? I have to fix / override.post{button{}}
. Who left this giant tangled mess I have to clean up? - Does
.post
have any javascript attached to it? From reading the html, I have no idea. I have to go hunting for that class name in the JS. Ah, it does - the “hide” behavior on any<button>
inside thepost
markup. Again, the new “fav” button has to work around this.- What happens on the
featured
post? For the big post on your individual post page, a “hide” button doesn’t even make sense, so it’s not there. Why is the JS listening for it? - If I add my “fav” button, where do I put that JS? We’ll want that on both the
featured
and the regularpost
elements. - What if we want “hide” to do different things in each place? For example, it should get more images from the main feed on the
posts-index
page, but more images from the “related images” feed in therelated-images
section. Do we use different selector scoping? Data attributes? Copy + paste the JS with minor alterations? The more places we use this component, the more convoluted the logic here will get.
- What happens on the
Two steps forward, three back
Okay, we can apply semantic class names to everything: hide-button
, post-image
, featured-post-image
, etc. Bootstrap does it, so this must be a good idea, right? Well, no. We haven’t really solved anything, just kicked all these questions down another level. We still have no idea where CSS rules and JS behaviors are attached, and how they’re scoped is going to be even more of a tangled maze.
What we have here is spaghetti code. You have extremely tight coupling between what’s in your template, css, and js, so reusing any one of those is impossible without the others. If you make a new page, you have to take all of that baggage with you.
Solutions
In the rest of our code, we try to avoid tight coupling. We try to make modules in our systems small, with a single responsibility, and reusable. In Ruby we tend to favor composable systems. Why do we treat CSS and JS differently? CSS by its nature isn’t very conducive to this, and JS object systems are currently all over the place (function objects? ES6 Class
? Factories?). Still, if we’re trying to write moar gooderer code, we’ll have to do something different.
I’m not the first one to get annoyed by all this. Here’s Nicholas Gallagher from Twitter on how to handle these problems. Here’s Ethan Muller from Sparkbox on patterns he’s seen to get around them.
I’ve found a setup that I’m pretty happy with.
- Use visual class names as described by Ethan above.
- Decouples CSS rules from markup content: an
image-card
is animage-card
is animage-card
- Makes grouping and extracting CSS rules easy - any element using the same CSS rules can use the same class names
- Decouples CSS rules from markup content: an
- Name classes with BEM notation, and don’t use inheritence.
- No squabbles over selector specificity
- Relationships between your classes are clear:
image-card__caption
is clearly a child ofimage-card
just from reading the markup - Prevents class name clobbering:
image-card highlighted
could be clobbered or messed up when someone else wants ahighlighted
class, butimage-card image-card--highlighted
won’t be
- Use
.js-*
classes as hooks for javascript, not semantic-ish class names.- Decouples JS from html structure - your drop-down keyboard navigator widget can work whether it’s on a
<ul>
, an<ol>
, or any arbitrary set of elements, as long as they have.js-dropdown-keyboarderer
and.js-dropdown-keyboarderer-item
- Decouples JS from styles. Your
.js-fav-button
can be tiny on one screen and huge on another without CSS conflicts or overrides - Clearly indicates that this element has behavior specified in your JS - as soon as you read the markup, you know you have to consider the behaviors as well
data-*
attributes have all the same advantages, but they are longer to type and about 85% slower to find (at least, on my desktop Chrome)
- Decouples JS from html structure - your drop-down keyboard navigator widget can work whether it’s on a
This was brought on by using Fortitude on the job, which has most of these solutions baked-in. It had a bit of a learning curve, but within a month or two I noticed how many of the problems and questions listed above simply didn’t come up. After using Bootstrap 3 for the previous year and running into every. single. one. multiple. times. I was ready for something new. I quickly fell in love.
The minute anyone decided to go against the conventions, developing on that part of the site got 10x harder. Reusing the partials and components with “semantic” markup was impossible - I had to split things up myself to move forward. Some components were even tied to specific pages! Clear as day: “do not re-use this, just copy+paste everything to your new page.”
I’d much rather be shipping cool stuff than decoupling systems that should never have been tightly bound in the first place.