Experiences with Astro + Lit + Tailwind
Maarten Verbaarschot - Feb 6, 2024
About the project
For one of our clients, we’ve recently built a new website. This website is mostly for marketing purposes and to offer contact options to (potential) customers. For this site, we decided that the following items would be our main priorities as developers:
Easy content management for copywriters, with or without CMS. The content is expected to be mostly static, updated maximally a couple of times per year.
Responsive layout, especially with phones in mind. Their old site had not been maintained for quite a long time, and had no responsiveness at all yet. Making the new site look representative again meant living up to expectations of today’s users, so it needs to look good on all devices and screen sizes.
Good accessibility, performance, and indexing in search engines. Considering that the site is mostly static text and pictures, we did not expect any major challenges here. Still, we made sure to explicitly list them as priorities and continuously measure these as much as possible.
A simple codebase that is easy to oversee and work with, and basically also the smallest possible investment to get the first version of the new site live. The site will not get updated very often, but when it needs an update a few times a year, it should take no hassle at all for a developer to achieve what is needed.
The foundation
Considering the priorities listed above, we figured the following foundation fits well:
Markdown, as alternative for a CMS. Our client has a bunch of projects that already use a certain CMS, but apparently it’s on the expensive side, and it’s a lot of work to set up for a new project. Considering we want to put the first version of this site live with the smallest possible investment, our expectation that the site will get updated only a few times a year, and that Markdown is pretty easy to learn, we felt that at least for our first year this could be an acceptable solution. The CMS that our client uses, has a certain way of choosing UI components per block of content, which we could mimic with MDX (Markdown + JSX). By wrapping bits of Markdown in components using JSX, we separate content from components yet give a lot of control to the copywriter over which content is rendered with which component. If after the first year, we feel the need to upgrade to the CMS, the structure we created in our MDX files will be relatively easy to understand and translate to the CMS structure.
Server-side rendering. For accessibility and SEO, client-side rendering is relatively well-supported by a lot of tools nowadays, so in that sense you could say that server-side rendering isn’t as much of a big deal anymore as it used to be. But for the fastest page rendering on all types of devices, without blank loading screens or layout shifts, server-side rendering is still an effective solution. Considering the static nature of our site’s content, we can even pre-render our content during our deployment pipeline as is often done nowadays, which will speed up the site even further as it will have to execute a lot less logic on-demand when a person visits the site.
Minimal client-side JavaScript. Many web-based projects get built using JavaScript frameworks nowadays, which often results in a nice architecture for frontend developers to work with, but also results in a lot of JavaScript getting shipped to the client which can hurt performance. Even if the site does not have any interactive elements, it will ship a ton of JavaScript to the client when using these frameworks. Before the rise of these JavaScript frameworks, many sites would on the client side consist of mostly HTML and CSS, and only tiny bits of JavaScript where really needed. In recent years, we see the rise of a new generation of JavaScript frameworks which allow you to have the architecture-related benefits of a JavaScript framework, but without shipping any JavaScript to the client by default. The site of our client is expected to have small interactive elements here and there which will need JavaScript, but the majority of the site is expected to be text and pictures which won't need JavaScript. So, we wanted to try one of these ‘new generation’ frameworks that allow us to fully control what JavaScript gets shipped to the client and what JavaScript doesn’t.
Multi-Page App architecture. Most web-based projects get built as Single-Page Apps nowadays, to make the navigation feel more like it does in for example native iOS- and Android apps, and to be able to easily share application state across pages. This does require a client-side JavaScript foundation, though, which will immediately add extra ‘weight’ to the site. Although that weight doesn’t necessarily have to be extreme, we felt that if we could do without, it would be best to keep the site lighter. With our server-side rendering during deployment which would make pages load and render fast, and the fact that we didn’t expect to need a lot of application state across pages, we felt that we could do without the Single-Page App solution and just navigate between HTML pages the ‘old-fashioned’ way using plain HTML links and no JavaScript.
A CSS architecture that will allow us to easily develop layouts with thorough responsiveness, going further than ‘aligning everything vertically on smaller screens’: responsive font sizes, responsive spacing, layouts that are a grid on one breakpoint but a flexbox on another breakpoint, etc. Using ‘raw’ CSS for a project like this will on one hand have the benefit of not requiring extra framework knowledge to do the work, but the code can get verbose. We’ve seen a rise of a certain type of CSS frameworks in recent years that apply a utility class-based approach. This approach on its own is not exactly new, for example Nicole Sullivan’s OOCSS approach was sometimes interpreted this way, and the famous Bootstrap framework also has a collection of utility classes. The ‘new’ part is mostly about making every utility class a responsive one. Some of these frameworks also allow you to configure them thoroughly, for example the breakpoints, spacing units, font size units, color palettes, etc., of which the framework will then generate all needed utilities. This will potentially make it easier to write responsive code that by default matches everything in your style guide. Some of these frameworks also optimize the CSS output, making sure that only the CSS that your site actually needs is sent to the client, rather than sending all CSS utilities even if they aren’t needed. These frameworks do seem to currently trigger either a love or hate reaction in developers. Also, the approach won’t lend itself for every project. For this project, the approach felt like a good fit, so we went for it.
The frameworks
Astro
Astro is a relatively young JavaScript framework (first released in September 2022). It’s part of a recent generation of JavaScript frameworks that ship no JavaScript to the client by default. Another example of a framework like this, is Qwik.
The main point of these frameworks is to have an architecture with 1 codebase for backend and frontend, in JavaScript, yet give the developer full control over which bits of JavaScript get sent to the client. Another thing they do is provide integrations for UI libraries such as React, Vue, Svelte, Lit, etc. Angular seems to be an exception so far, perhaps because it’s a bigger framework which contains way more than just UI components, making it harder to translate to this kind of architecture without changing Angular itself.
Astro provides most of the foundation we need for our site: Markdown, server-side rendering, and by default a multi-page app architecture. It does this with a zero-config setup, allowing us to get started with this part of our foundation with no investment at all. Creating page routes is a matter of creating MDX files such as /pages/example-page.mdx
, automatically resulting in routes such as example.com/example-page
. Then adding the content in Markdown syntax and wrapping certain content blocks in components using JSX syntax.
The rest of our desired foundation is covered via Astro integrations of the other 2 frameworks: Lit and Tailwind.
In case some of you are wondering why we went for Astro and not Qwik: the easiest excuse I can give is simply that I only discovered Qwik a couple of months after we built the site. Looking back now, both look good. At the time of writing, Astro does have the advantage of having the most UI library integrations. Qwik’s current advantage is that it gives even more control over which JavaScript you send to the client, even reducing the parts of the UI library that get shipped when they are not needed. With Astro currently, once you use a UI library, the library gets shipped in its entirety. I suppose both frameworks can inspire each other to keep exploring these kinds of improvements. Healthy competition.
Lit
To be completely honest, if it would’ve been up to me when I started on this project, I’m not so sure Lit would’ve been my first choice. This is because I’ve seen companies migrating from Angular to Polymer in the past, thinking that Polymer’s web component-based approach meant that applications would become ‘framework-agnostic’. Thus, no more framework migrations in the future. They were sensitive to this after having experienced the migration from Angular 1 to 2. Unfortunately, a bunch of companies then also fell in this web component pitfall at the time. Polymer got abandoned at some point by its authors and anyone using it was forced to migrate to something else. Lit is the successor of Polymer, so of course the authors hoped that people would migrate from Polymer to Lit.
There were a bunch of libraries at the time who had similar marketing: leveraging web components and web standards, instead of profiling themselves as a framework. I get that it can sound like you will be framework-agnostic. But in the end they are all frameworks. They all provide additional implementations and syntax to make things work in a practical way. Web components in their pure form have been practically unusable in software projects so far, because the solutions are so low-level. It’s not necessarily a bad thing, but you’re going to need something on top of it to make it easier to just build your product without drowning in low-level APIs.
Regardless of what I personally would’ve chosen, in this case it was pretty simple: it was company policy to use Lit for all frontends. Period. So I gave it an honest chance. Although my thoughts about the whole Polymer situation made me sceptical, when forgetting about history for a moment and just looking at what Lit is today, it looks pretty good.
Lit provides a web component-based architecture, enhanced with solutions to declaratively add reactive data, styling and more. It's lightweight, fast, easy to learn, and as rare as it is nowadays: it can even be used without relying on compilers, which will sound nice to companies who have experienced the struggle of upgrading or migrating between JavaScript build systems such as Webpack. Astro provides an official integration for Lit.
Tailwind
If you’ve ever used Bootstrap, you might be familiar with their utility classes. I’ve always appreciated these, especially on projects that have a bit more of a marketing purpose, which will often cause the layouts to vary much more than in projects that are composed mostly out of consistent looking data grids and forms.
When I saw Tailwind for the first time, I felt like: this is like Bootstrap with just the utilities, and then maximizing what you can achieve with utilities. In marketing projects such as this website, the designers often want a lot of control on the page layouts and UI components. Using a framework like Bootstrap in these kinds of projects, can cause a lot of customizations done over time to the framework to make it look like the designer wanted, in the form of custom CSS which overrides the framework CSS. Bootstrap is configurable, but you can run into limitations. I’ve also seen companies eventually switch from Bootstrap to a fully custom CSS foundation based on their own design system.
Basically, Tailwind feels like a sweet spot to me for these kinds of projects. You get a certain structure that will help you focus on your project, without getting potentially distracted by CSS foundation related challenges or discussions. You get a highly configurable framework with good documentation, yet full control on the page layouts and UI components which you will build yourself using the Tailwind utilities.
Our designer liked the idea as well, there was already a Figma environment with a thorough style guide which we ended up mapping to our Tailwind configuration.
Astro provides an official integration for Tailwind.
Highlighted successes and challenges
Overall, I have quite a positive feeling about how this project went. We were able to build the site in a rapid pace yet still have a nicely structured codebase that was easy to read and predictable to work with.
Below I have highlighted a few things that I think were the most important successes and challenges.
Writing content in MDX files
The MDX approach as alternative to a CMS worked really well for the initial version of the site. Because Astro’s page routes don’t require configuration, it’s easy for even a copywriter to just add an *.mdx
file in the pages
folder, using the folder structure and file names to determine what the URL of the page will become on the site.
The Markdown syntax is easy to read, and using component properties and slots we pass pieces of Markdown to components that will render it in a certain way. We kept the properties and slots of these components as simple as possible to not overwhelm the content creators with too many technical details.
Optimizing the production build
The default Astro setup already optimized our production build quite a lot. We used Lighthouse to check regularly how our site scored, as we were developing it.
Astro’s image component does well at compressing images based on the way you are using them. Thanks to this we could just export any image we needed from Figma, add it to our codebase without any manual adjustments to the image, pass it to Astro’s image component with a few properties (e.g. to set the size), and let Astro do all the optimization for us.
We ended up adding the Astro Icon integration as well, which made it easy for us to work with SVG icons. Part of our client’s design system uses Material Design icons, which ship with Astro Icon as one of the default icon sets. We also had a bunch of custom, branding-related icons which Astro Icon was also able to work with. Astro Icon also provides icon sprite generation, which we made use of to reduce the amount of inlined SVG code on pages that would use the same icon multiple times.
Astro’s Tailwind integration also optimizes the CSS output well, partially with a stylesheet shared across pages and partially with page specific stylesheets.
All in all, with very little effort, we were able to achieve perfect Lighthouse scores.
A very easy-to-learn tech stack
This was my first time working with Astro and Lit, I did build a few projects with Tailwind before. For my teammate, all three were new plus they were catching up about CSS and JavaScript along the way. We both felt that this stack was easy to get used to, and to get things done fast while keeping the code well-structured.
One thing that was easy to confuse a bit in the beginning, is that both Astro and Lit provide ‘components’. (Astro component documentation, Lit component documentation) The ones in Astro work based on JSX syntax, while Lit has their own syntax based on JavaScript template literals. And then when you embed a Lit component in an Astro page, it will be JSX syntax again. Having 2 documentation sites open and trying to learn both in parallel can be a bit confusing, but in the end it wasn’t too extreme.
Rapid yet structured responsive designing
Tailwind’s documentation with its search function is excellent, and so is the autocompletion of the IDE we used: IntelliJ. The Tailwind site of course doesn’t know how we configured Tailwind in our project, so that’s the only thing to keep in mind while looking at their documentation (e.g. our spacing units were different from the ones listed on Tailwind’s site). That’s where the IntelliJ plugin comes in, because that one does understand how we configured Tailwind. The autocompletion reduces the need to peek into the style guide to a minimum, while building components.
Working with Tailwind utilities really accelerated us. Once you style the smallest breakpoint and then enhance things where needed, using the breakpoint-specific version of each utility class, you have an optimized layout for every screen size in no time. Sometimes the amount of breakpoint-specific styles can become a bit much to inline in an element’s class attribute, in these cases we used Tailwind’s @apply
directive to prevent the HTML from getting too overwhelming. The same also works well for elements that are repeated a lot in the HTML, for example when you want to style each <li>
in an <ul>
.
Server-side rendering the Shadow DOM
This turned out to be a bit more of a challenge. Lit supports server-side rendering, but the implementation that does so is still listed as ‘experimental’. We tried it out with Astro’s Lit integration, and it did correctly server-side render our pages without errors. However, the fact that every Lit element uses Shadow DOM by default makes the HTML output far from ideal.
For example, if you have a Lit component called ‘Button’ which contains a template and stylesheet, and then use that button in a lot of places on the site. With any component library that doesn’t use Shadow DOM, the template and stylesheet would be loaded somewhere once, and re-used for all Button instances. But with Lit’s declarative Shadow DOM, in combination with server-side rendering, this Button template and stylesheet both get inlined for every single Button instance. This causes our HTML output to grow quite a lot.
Lit does provide an option to disable Shadow DOM for a component, but the consequence of this is that you won’t be able to use slots anymore, which is often the thing you actually want to use for these kinds of components. Also, disabling the Shadow DOM for a component seemed to be ignored at the time by Lit’s server-side rendering solution, a result of the fact that this solution is still experimental and in development I suppose.
We ended up working around this to keep our HTML output optimal. We used Astro’s own component system for the majority of the site, which didn’t need JavaScript. Think of buttons, typography, card components, anything that would be considered a reusable part of a design system. Then only for interactive elements that would need JavaScript, we would create a Lit component to implement the interactive behavior, passing pre-styled Astro content via slots to the Lit components.
At some point while we were almost finished with the site, I noticed that Reddit did a migration to Lit and Tailwind also. I was peeking a bit into their code using the browser’s inspection tools, and it seemed that they were applying a similar technique: passing pre-rendered and pre-styled content to Lit components via slots.
While this trick does the job, it does feel a bit clunky, and you wouldn’t need to do this if you didn’t use Shadow DOM to begin with.
We do use Lit for a bunch of other projects of this client, where no server-side rendering is applied. These are dashboard-like applications for logged in users, for example. I see Lit working in those projects really well, and I do experience Lit as quite a pleasant library to work with this way. Just when a project needs to apply +server-side rendering, I hope a better way will come along to achieve this in the future.
Using TypeScript for Astro components
Whenever working with a component-based architecture, I think one of the best things to do is to always clearly document which input and output those components are designed to work with. Input usually means passing data (e.g. the label of a button), output usually means reacting to something the user is doing with this component (e.g. clicking on a button).
This input and output is named somewhat differently per component framework: in Lit the input is referred to as ‘properties’ and the output as ‘events’. Astro refers to input as ‘props’, and its components never emit any output since they are only used to render the initial state of a page during the server-side rendering process.
In both cases, the frameworks support using TypeScript to document your component’s properties. We noticed however that in Astro, the type checking is a bit limited. There is a CLI command that you can run, astro check
, which can be used to for example do a type check in a CI/CD pipeline. In IntelliJ however, although in most TypeScript projects you will get a clear red underline immediately if you type something that fails the type check, it doesn’t do this in an Astro TypeScript project (yet).
For our relatively small website project, we accepted this limitation for now as it wasn’t bothering too much. For bigger projects with more developers working on it, this will get unacceptable at some point, probably. I’m not sure if this is a limitation of Astro itself or of its IntelliJ plugin, but either way it will hopefully keep improving over time.
Conclusion
If Astro stays as reliable and efficient as it is today, I see myself using it more often in the future, for various projects. Its island architecture is very powerful, especially for bigger, long-term frontend projects that might want to implement some form of microfrontend architecture. I like that it has a bunch of smart lazy-loading options, for example to lazy-load components until that area of the page is scrolled into the viewport (using the client:visible
directive). The fact that it can work with many different UI libraries also makes it potentially migration-friendly. Well, as long as the foundation stays in Astro of course.
Projects like Astro, Lit and Tailwind are very promising. Although for all three, we will have to see how they will stand the test of time. At the moment I appreciate working with them, and I hope they will become long-lasting foundations of various software projects, similar to how Angular over the years has been an amazing influence on the frontend landscape.