Skip to content
Courses CSS Nouveau Vanilla Has Never Tasted So Hot Final Exercise: Building a Business App UI

Final Exercise: Building a Business App UI

Let’s Go Fly a Kite Build a Dashboard! #

Alas, I’ve got nothing for you half as fun as participating in hijinks made possible by the effervescent magic of Mary Poppins. But perhaps we can still have a bit of merriment as we put all our skills to the test in the building of a quintessential business dashboard.

Mary Poppins clapping in bemusement

The Assignment #

What sorts of things do business need to look at every day? Charts. Definitely charts. So let’s add some charts to our dashboard.

Hmm, what else. Messages? Sure, some kind of latest messages or “notifications” or whatever. That’d be great.

Maybe some upcoming group activities? People who run businesses often like to plan activities. Things like trust falls or brainstorming OKRs or very awkward holiday parties. I hear that’s good for “synergy”.

Of course, modern business people are often “on the go”, which means they’re using smartphones. We most certainly need to ensure our business dashboard works on a smartphone!

We’ll want to have a sidebar menu with a list of links to the various areas of interest for this “intranet” solution—key content which the dashboard can surface at-a-glance. (And naturally this sidebar should be responsive.)

And obviously we won’t be using React or Vue or Svelte or Angular or Solid or any other JavaScript framework to author our UI components, nor will we be using Tailwind or CSS-in-JS solutions or CSS Modules or any other build step. Vanilla all the way, baby!

My Solution #

Demo Repo

Holy business dashboard, Batman! It lives!

Now before I get onto all the code-level fiddly bits, let’s see how well I did on fulfilling the key points of the assignment.

Charts? We got charts!

Messages? We got messages!

Upcoming group activities in which everyone is simply thrilled to participate? Absolutely!

Attractive design which scales well on different screen sizes from mobile to large desktop displays? You be the judge!

And most importantly, is everything in the UI using vanilla web tech? Yes! 🎉

Technical Notes #

If you want to kick the tires locally, just clone the repo and run npm install && npm run start and open http://localhost:4000.

For this project, I decided to use Eleventy (plus a smattering of Liquid templates) as a static site generator/dev server…but trust me, I’ve kept the build processing to a strict minimum. This is merely a tiny step above authoring plain files in a folder (everything’s wired together solely with simple file includes), so that you can see just how far you can get with vanilla everything. Heck, I’m not even using something like esbuild to produce CSS & JS bundles! The CSS imports are vanilla imports, so the browser will just download and process each file per spec.

Most of the includes are nothing more than Declarative Shadow DOM (DSD) templates. Because of the minimalist nature of the build system, it was easier to do it this way rather than wrap entire components, as you might do when using a system like WebC. While I would argue this isn’t ideal for production work, I think it’s good for an educational exercise. I really wanted to illustrate just how much the call sites of components are simply HTML as you are used to (with the DSD embedded within each block of markup comprising merely an implementation detail of that component).

Now it’s true that with frequent usage of DSD, HTML payloads will be a little heavier with those embedded style tags compared to other solutions. However, with good Brotli compression you will find that the page size is really still pretty small. I’ve seen other sites sporting inlined CSS and/or JS with way heavier homepages.

Regarding JavaScript, there’s…very little used on the site. For charts, I imported Chart.js and then wrapped it in a fairly simple web component. By far, the trickiest part was getting charts to be responsive and fit well within the dashboard’s grid cells. In terms of JavaScript for a few components, I just wrote inline <script type="module"> blocks in the template files. You should definitely not do this in production, but for the purposes of this demo it’s fine.

Styling Deep Dive #

You will be shocked how little CSS I actually used, outside of individual components. The global stylesheets are tiny. I ended up resetting almost nothing. Open Props is of course used to provide a good set of design tokens to start off with, but otherwise…less is more. I did a bit of work as well to separate out “base tokens” from “semantic tokens” like we’ve discussed on previous episodes.

Certainly if this project were to grow into a larger admin UI, more global CSS would be needed…but how much more? Perhaps not much more!

Pretty much all of the main site layout is handled by a DSD template attached to <body>. Yup, I did it. I added shadow DOM to the whole web page. 🤯 I really do think this technique featuring slots + CSS grid is one of the coolest things in all of modern web design. The only thing which might give you pause is that DSD requires a polyfill (which is included in the project) in older browser versions still, otherwise in disabled-JS environments layouts will look wonky. Could you use a CSS Grid-based layout without DSD? Obviously yes…but is it as elegant? 🤷🏻‍♂️

Case in point…I love the fact that in the site_layout.liquid file, I can define my “Skip to content” link at the bottom of the file, yet it’s slotted in at the top of the shadow DOM template so that it’s first in the browser’s source order. That’s a technique not enough folks cover when talking about the shadow DOM: your authoring-phase source order and the browser’s rendered source order can be different! 🤯

Besides using CSS grid for the main site layout (along with some fairly advanced trickery to handle a position: fixed sticky header and other grid area considerations), I used grid for laying out all the various cells in the main dashboard section. There are two grids: one for the charts, another for the messages & activities. (And the top grid tweaks the default “minimum grid cell size” to adjust auto-responsive characteristics.)

Flexbox is also used, but generally just to lay out items on a single axis (aka a button and a text label, or a toolbar). It’s taken me years to feel comfortable wielding both grid and flexbox depending on the task at hand (ngl, I found grid syntax super confusing at first!), so don’t feel bad if some of this stuff looks like Martian to you! You’ll get the hang of it in time.

Custom Elements #

As you can see, almost everything on the site is operating within custom elements featuring the bd- prefix. Most of them are HTML+CSS only, no JavaScript! A full list:

One interesting thing about the callout and messages components is they utilize several design tokens in the same manner so that they’re visually consistent, even though they serve different purposes.

Regarding the venerable button component: zooming out to the design system level, there are lots of ways to handle buttons & link buttons (visually similar, but one’s an actual <button> and the other is <a href=>). And a cornucopia of opinions abound! Here’s one such treatment by Cory LaViska of Shoelace about many of the issues involved.

Because I have almost no real build system here, I couldn’t add any conditional logic to switch tags depending on incoming component parameters, even if I wanted to! So I tried something which now feels to me rather clever: I kept the link or button tag in light DOM as the sole child of the <bd-button> component, and styled them through the use of ::slotted(*), ::slotted(a), and ::slotted(button) to cover the various use cases.

While this pattern is more complicated in a way than what you might do if you were using Lit or some other web component framework, it’s actually very simple from the buildless perspective. Attaching your event handler to your button is straightforward, and your link can likewise do all the linky stuff it might need to do (even including play well with other libraries you might add like Turbo or Swup to add nice SPA-like page transitions). The <bd-button> wrapper simply lets you add your DSD styling and any other shared behaviors you might need to handle down the road.

“But Jared. All these components with DSD templates aren’t real web components. Most of them I can’t instantiate them through JavaScript. They don’t have client-side templates for rendering on the fly. Mostly everything here is just server-rendered HTML!” …Yeah, that’s the whole point. 😅 I think most components you write on most projects should be HTML-first. You get to decide what’s also bundled into a client setting and usable in pure frontend UI.

For example, perhaps you want <bd-button> to be fully-usable client-side, because you have a particular feature which needs that level of frontend fidelity without waiting for full server roundtrips. OK, there are some things you can do!

The nice thing about authoring everything HTML-first, vanilla-wherever-possible is it’s pretty straightforward to adopt more advanced and opinionated tooling later. Whereas it’s very challenging to go the opposite direction—for example backing out of heavy-duty client-side JavaScript components and switching to simple HTML templates.

Component Wrappers #

A comment on my Chart component: this is relatively bare-bones in terms of supporting all the various chart types and customizations provided by Chart.js, but it’s a start. I hope it helps illustrate the dilemma we sometimes find ourselves in when working alongside the JavaScript ecosystem. Some libraries which sit outside of a specific framework like React, Vue, etc., merely offer imperative APIs. It’s all about calling a function or object instantiation, passing a bunch of config options, and then “mounting” the widget on a <div> or whatever.

In the age of web components, this is no longer acceptable. We need to be able to mount these widgets easily anywhere we need to via custom elements, and use APIs either through HTML attributes, simple JavaScript properties, or—at the most complicated end of the spectrum—event-based callbacks (which is the solution I arrived at here). Whether we’re talking about charts, text editors, form controls like popup calendars or autocomplete combo boxes, image editors, or any other sorts of interactive controls in your UI, I’m team web components all the way. It’d be great if everyone provided them right out of the gate…but whenever that’s not the case, we’ll have to write our own wrappers. Bummer, but doable.

Funky Stuff #

As with all projects, there’s a fiddly bit here or there that doesn’t feel “quite right” but it’s the best you could do with the time and techniques on hand. Here are a couple of the hacks I know about which could probably be improved:

For the bd-menu-item component, the <a> link tag gets composed into the default slot. This poses a bit of an issue, because it means if you click on another slot like an icon, nothing will happen because it’s not included within the link tag.

So my hack was to intercept clicks on the menu item which aren’t part of the link, and basically simulate link click behavior. Does it work? Well enough. But it’s still a little hacky, because you won’t get the URL showing up in your status bar if hover over the icon instead of the text.

How might we do this differently? We could wrap the entire menu item in <a>. But then the direct child elements of <bd-menu> would be a bunch of <a> elements, not menu items. I don’t really like the semantics of that. But YMMV! Maybe the tradeoff is worth it! (This kind of stuff often comes up when dealing with links in cards. You can read all about these issues in this article by Heydon Pickering.)

The other fiddly bit is needing to add role="list" and role="listitem" in a bunch of places to improve semantics & accessibility. It’s not a huge deal, but the way this minimalist build system works, it’s on the application template author to remember to include those for menu items and messages and things, rather than the component author. If I could use a system like WebC or Lit, I could ensure these components would render out with the proper ARIA roles without needing component consumers to deal with all that.

Conclusion & Next Steps #

There’s probably a whole ton of points and thoughts and things to follow up on I’m forgetting, but that’s what the Discord is for! I welcome your questions, ideas, concerns, and challenges.

Now it’s your turn. How would you build a business dashboard with some of the basic requirements mentioned above? What tech stack or framework might you reach for? How well would it support many of these HTML-first, vanilla-first techniques?

For example, given that this would require a login and legit backend data in a real-world application, you might reach for a fullstack framework like Ruby on Rails. And for building components, you might pull in a Ruby gem like ViewComponent. This would support most of everything you might need on the HTML side—even Declarative Shadow DOM templates—but things get murky beyond there. For example:

Hmm, for a framework which people typically think lets you write a lean-and-mean frontend without all the baggage of JS frameworks…kind feels hard and weird to get started with a robust vanilla-oriented frontend architecture. 😕

Or, let’s say you want to use Astro, a shiny new framework which ships zero JS by default, plays well with HTML-first techniques, and offers SSR features in addition to a static site. Lots of groovy features to be found here! It’s just that…working with Astro’s component model and slots and so forth right alongside the semantics of native web component slots and Declarative Shadow DOM, as well as global light DOM styling vs. component shadow DOM styling—well, I had a frustrating time hacking together some solutions which sort of work. That was a while back though—maybe Astro’s improved things since! But it just goes to show that frameworks which try to help make your job easier sometimes will simply get in your way.

I don’t say all this to discourage you! I’m just saying no matter which tried-and-true or new-hotness framework you might reach for to build a new web application with, it may take a bit of upfront effort to get past the framework-ness and build your as-vanilla-as-possible frontend architecture. I wish things were different in this regard. I wish, in the year 2023 going into 2024, frameworks would expose as much of the specs and semantics of the native web as they possibly can, and provide explicit opt-in to their own framework-y bits.

Let’s hope public pressure and increased awareness of all the marvels of modern frontend engineering take things in a promising direction. In the meantime, you now have some fundamentals under your belt to help you build tokens, resets, classless themes, and components with or without shadow DOM using nothing but vanilla HTML and CSS. Pat yourself on the back! It’s been quite the journey, and these are techniques you can utilize and build upon for years to come.

As a homework assignment, I’d love to see how you might build something like this business dashboard. Which tradeoffs would you choose? How would you build up the start of a design system? Would you reach for Open Props as well? Start from scratch? Use one of the “classless frameworks” we discussed previously? Would you use a well-known library like Shoelace for some of your components? Or build those from scratch too? Would you author vanilla web components? Or use a library like Lit?

Hop in the Discord and show off what you build, or ask questions to help steer you in the right direction!

I sincerely wish this course proved helpful to you and gave you lots of food for thought for further explorations and progress in your career as a web developer. Let’s raise a toast 🥂 to all of the amazing and powerful technology we now have at our fingertips, right there in the modern web browsers we all use today. There’s never been a better time to be a developer working with the medium of the web.

Baby screaming Yeah!


That’s all folks! 👋

Return to
CSS Nouveau Series