Helix Block design
There’s a common phrase when one learns how to make a proper espresso.
“Making espresso is an afternoon to learn and a lifetime to master.”
Over the last few years, I have seen this be the case with creating blocks on Helix. Anyone can understand the basics of building a block within about an hour. Stick your JS here, your CSS there, and you’re off to the races. Mastering block design is a completely different world that can be fraught with over-thinking or trying to map blocks to a mental model of past CMSes.
A good block needs to have the following principles:
- It’s intuitive to author in a Word context.
- It leverages section metadata when appropriate.
- The JS & CSS are resilient as possible to different author inputs.
- It can be localized.
- It’s accessible.
- It’s SEO friendly.
- There’s no impact to the performance of the page.
- Do not use 3rd party frameworks or build tools.
Some of these are hard and fast rules. “In can be localized.” Duh. Some are softer and squishier. Let’s unpack them.
It’s intuitive to author in a Word context.
You want to create a basic structure of content where if you squint, you can see the final form of the UI. There are some exceptions to this. For example; beyond three columns and blocks get gross and jammed inside the Word editor. If you find a scenario where you need more than three columns, you likely need to flip the axis, so your columns are now represented as rows. Again, your mileage may vary, and you should work with your authors to determine what makes the most sense. Maybe only four columns get the axis treatment because it’s such an edge case and your 80% use cases don’t need to suffer.
It should also be 100% Word Online compatible. If you dig deep enough, you will find features in Word Desktop that do not exist in Word Online. We should always be working on the lowest common denominator as we build our content structures. The most common example of this is that Word Online does not support having image links while Word Desktop does.
Another sub-principle here is that we do not want to recreate AEM dialogs or headless form fields. I have seen from time-to-time developers having a natural tendency to make what I call key/value pair blocks. These are blocks where an author is presented with a key (Title) and they must fill out the value in the right column. This should be avoided to the largest extent possible.
Leverage Section Metadata
I’m just going to say it: Section Metadata is the secret weapon of fulfilling your block edge cases. Let’s say we have an accordion block. You create three on a page because you want them next to each other (as columns). But you have a problem: blocks cannot go inside other blocks. You cannot put an accordion in a 3-up column block. What do you do? Enter Section Metadata. The power of Section Metadata is that you can add this block, put in a style like three-up, and write some CSS inside your accordion CSS to react to that class being added to the section.
Another use case for section metadata is pulling out content that could be considered block agnostic. Let us say you have a form block and that form block has a title. Does the title really need to be coupled with the block? Or should you have a title block that can live in your section and be paired with your form block? Maybe you don’t even need a title block. This de-coupling provides authors greater flexibility in solving a variety of layout needs.
Make your JS & CSS resilient
It can be tempting to use pseudo selectors in CSS. Selectors like nth-child or last-of-type. This can make your CSS more brittle than necessary. It’s always recommended to decorate your DOM through JS and use human readable selectors in your CSS. This means that even if you use a pseudo selector, that’s done once in JS.
Another trick to successful block making is to divide your content into required and optional parts. If you have a marquee block, what is absolutely required? The title? Great. Did you find an image? Did you find two images? Is the first image in its own row or is it in a cell adjacent to your foreground content? If it’s in its own row, we know this is a background image. Often you can start with your title (heading) and work your way outwards. Did you find content right above your title? That’s called a detail. Did you find a paragraph below? There’s your body. What about a paragraph that only contains links? There’s your action area. As you expand from you title you can start probing your block for optional areas.
This next tip will sound a bit obvious: don’t break the content. You may get into a situation where you need to heavily decorate a block. Maybe this is for accessibility, maybe this is for semantics. You’re now in need of document.createElement(). If you really must go this route, it’s important to bring all the content over for the ride and you need to also avoid using innerHTML. I’ve seen situations where developers assume that only a paragraph will be a child and that is all that is needed to move into the new element. Suddenly, an author tries to use an image in an accordion and it doesn’t work. Worse, using innerHTML to grab the current DOM state is a recipe for disaster. In doing so you are potentially damaging an events attached to the current DOM state. Maybe there’s a Preact (or insert framework of choice) component in the list of children... do you want to crush all those interactions with a healthy dollop of innerHTML? I didn’t think so.
There’s no impact to the performance of the page.
It’s a programming cliché, but it’s time for us to get lazy with how we build blocks. If you’re doing more than five query selectors or finding yourself looking for the same DOM element in different functions, you need to start seeing how you can group your selector queries and start passing elements around. Also remember: CSS selectors work from the child up. If you do a search for parent > ul, CSS (and querySelector) will look for every UL before it traverses up to the rest of the selector. This is where block decoration really helps. You can start to get specific and reduce the work the browser needs to do.
I want to touch back on document.createElement(). If you end up using this function, you will surely need element.append() (and certainly not appendChild!). You want to do all the inner operations of append before you do your most outer append. This will ensure the browser only has to render once rather than re-rendering when you add children. Let’s say we have a child, parent, and body. If you append parent to body, you’ll get a render. If you then you append child to parent, you get another render. If we flip this and append child to parent first, nothing is being rendered because it has not yet been appended to a visible DOM element. Once you attach parent to body, the render happens. Another way to save CPU cycles is to append multiple elements at once. This is why I recommend append, which allows for multiple elements vs appendChild which only accepts one.
There’s one other consideration regarding document.createElement vs innerHTML. It may be alluring to look at the appends you’re making and say, “I can do this in one innerHTML operation.” This may be true, but you need to know the performance implications of doing so. You also need to think whether you want to do a lot of template literals in your JS. It’s also important to know that if you need to do any additional modifications (aria states, events, etc.) after setting innerHTML, you will have to hit the DOM again to get those new elements. Reasonable people can argue over when to use what method, but I still personally find createElement to be a cleaner method with fewer drawbacks. I personally find a mix of innerHTML & createElement to be the worst of both worlds because you’re solving the same problem two different ways in a single solution.
In general, you don’t want your block visible until you’ve done all your decoration. Some of this is handled with the block loader, but you may have internal decorating that can cause cumulative layout shift (CLS). Maybe you have an expensive operation to fetch some JSON. You may not want to show your block until the JSON has been downloaded. There will always be a little bit of experimentation when you have expensive tasks that happen during block decoration.
Last, but not least, you may run into blocks that require 3rd party dependencies. I won’t sugar coat this: these blocks are the worst. These blocks will likely tank your TBT, CLS, and LCP if you let them. Don’t. All blocks that require a third-party dependency (YouTube, form service, etc.) should be lazily loaded with IntersectionObserver.
Do not use 3rd party frameworks and build tools
Do you remember when jquery was cool? Do you remember when
What’s written above is, of course, subject to change. What was once considered best practice yesterday is no longer considered best practice today. What I’ve compiled above is a collection of practices I find to be intuitive, reasonable, performant, and easy to maintain. If building great Helix blocks is like learning espresso, maintaining them should be like a river polishing a rock gently over time. We’ll save that for another post.