Change: CSS utils library to Tailwind CSS
Change pattern proposal: CSS utils library to Tailwind CSS
Old Pattern
Over the last few years, we have been primarily relying on our CSS utilities library to apply styles in the projects that adhere to Pajamas' guidelines.
What are CSS utilities?
They are mixins and classes that have a single purpose: applying a specific left margin, a background color, setting the display to flex, etc. At GitLab, CSS utilities are built in accordance with Pajamas' specifications, so they let us apply styles rules in a way that systematically complies with the design system. CSS utils thus play an important role in making our UIs consistent.
How is our library implemented currently?
New utils are to be added manually as SASS mixins to the GitLab UI project. When the @gitlab/ui
NPM package is published, utility classes are generated from the utility mixins. Having this mix of mixins and classes lets us write component styles in GitLab UI that leverage the @include
directive. This is done to support HAML versions of our component that inherit GitLab UI's styles.
More information can be found in GitLab UI's contribution guidelines.
Why have we gone this route?
Simplicity: having a set of ready-to-use CSS utils makes writing new features relatively easy as we do not need to write CSS, or refer to the design system to know what values are allowed.
Performance: previously, we had been writing a most of the styles as SCSS right in the GitLab project, which made the CSS bundles heavier over time. By leveraging CSS utils, we are able to control the bundles' growth as most style rules can be written once, then used as many times as needed by applying the corresponding classes to our markup. More so, given that each util has to be added manually, the library we ship only contains the things we actually need.
What went well?
As was expected, our CSS utils library has decreased the burden of writing additional SCSS as we've reached a point where the utils library is stable enough that it generally exposes everything we need when coding in GitLab or other projects that use the library.
What didn't go so well?
Where to begin...
...development overhead
As mentioned earlier, any missing utility has to be added manually to GitLab UI, resulting in a development flow that looks like this:
- Start working on a project's feature and realize that a utility is missing.
- Create an MR against GitLab UI to add the missing utility, go through the review process and get it merged.
- Wait until a new release of
@gitlab/ui
is published and the package is upgraded in the project you were working on (which is semi-automated thanks to Renovate). - Resume the actual work.
While relatively simple, this process can incur significant delays in milestone work and is arguably annoying.
...inconsistencies
The manual nature of our utils library means that it is vulnerable to human errors, and we have seen inconsistencies make their way in the build as we lack the tooling that could have caught them during reviews:
-
Mis-named utils: we have loosely documented how utils should be named. They are to be prefixed with
gl-
, followed by the breakpoint if applicable, then the style property, and finally the value. But, without automated checks, some utils have been published with different naming conventions, making the library somewhat unpredictable. - Desktop-first approach: most of the responsive utils are written with a mobile-first mindset, but several have been written with the opposite, desktop-first, approach, which can make it particularly hard to implement responsive UIs as we might run into conflicting utils and end up in a dead end.
...deprecations
Having to add utils as they are needed meant that we prevented GitLab UI's CSS bundle from growing too fast. But the flip side is that, when a util stops being used, we often are not aware that it can be removed from GitLab UI, so we potentially pass by opportunities to decrease the bundle's size.
...breaking changes
Related to the above, when we do remove a util from the library, it can be hard to gauge the impact of the change. We generally assume that utils are meant for GitLab, and that if one stop being used there, then it can be removed from GitLab UI. In reality, @gitlab/ui
is used in many internal projects (this more or less relevant search query can help find most of them: https://gitlab.com/search?group_id=9970&scope=blobs&search=filename%3A*package.json+gitlab+%2Bui). So, any utility removal has to be considered a breaking change and would theoretically require that we audit our projects to come up with a migration path.
That said, we're probably better off never removing a util that's been there for more than a few days as it just gets too inconvenient to track its usages.
...lack of distinction between consumers
Relates to the previous point: when any project that leverages GitLab UI's utils needs a new util to be added, the change propagates to all other projects, even ones that might never need that util.
TL;DR
Overall, it seems like our utils library is –to some extent– causing the problems it was meant to solve: it sometimes causes overhead in the development cycle, and we are loosing control over the utils bundle size.
What options do we have?
Possibles alternatives have often been discussed, primarily in GitLab UI CSS utility class library feedback (gitlab-org/gitlab-ui#1090 - closed). Most of the solutions we have considered revolve around improving the tooling in GitLab UI (generating the utils from Js objects, linting, leveraging PurgeCSS in consuming projects, etc.
As part of this RFC, we'd like to consider switching all of our setup to Tailwind CSS.
New Pattern
We would drop our custom-built CSS utils library altogether in favor of Tailwind CSS. Tailwind works by parsing the codebase it's installed in, and generating the necessary utils based on the class names it found. This means that any project that currently relies on CSS utils would have to setup Tailwind.
We'll still need a source of truth for our utils to adhere to Pajamas' specifications. In a first iteration, the source could remain GitLab UI. Tailwind is entirely configurable, so all the default config options would be published as part of the @gitlab/ui
package, and consumers would have to extend them in their own setups.
Advantages of switching patterns
- No more need to contribute to two projects when a util is missing. Once Tailwind is setup with the correct configuration, any utility can be used anytime. Tailwind just generates it on the fly when it detects usage in the codebase.
- Consistent naming: Tailwind enforces its own naming conventions, which it would be impossible to deviate from.
- 100% purged CSS bundle: given that Tailwind only generates the utils for the classes it sees in the codebase, when a util stops being used, it's also removed from the CSS bundle.
- Project-specific: by setting up Tailwind per project, we ensure that the utils generated are for that project only, and that it doesn't inherit utils that are only used in another.
- Fully documented. All of Tailwind's utils are well documented at https://tailwindcss.com/docs.
- IDE integration through Tailwind's plugins.
Disadvantages of switching patterns
- More setup: each project that requires CSS utils would need to have Tailwind set up, which might be more or less tedious depending on the environment.
- One more dependency in each project.
- Inability to use string interpolation to build class names dynamically (Tailwind needs to see the full names to generate the required classes).
- Need for yet another migration: we'll need to align our library's naming conventions with Tailwind's before we can make the switch.
What is the impact on our existing codebase?
To proceed to the switch in the smoothest way possible, we would likely need to ensure that all our existing utils match Tailwind's, this means revisiting our naming conventions and fixing all existing inconsistencies and responsiveness issues prior to the switch.
In GitLab UI, component styles would be written with the @apply
instead of @include
.
We will also need to identify all "interpolated utilities" such as gl-px-${padding}
and migrate them to full utility names. Tailwind needs to be able to find all the utilities used in the codebase, which would not play well with string interpolation. More information.
Do we have to start with the GitLab project?
Not necessarily! We could experiment with the setup in smaller projects where the impact would be much lower. eg:
- design.gitlab.com: this project has been using its own utilities, and there's an ongoing initiative to replace them with GitLab UI utils, so there's an opportunity to set up Tailwind right away and complete the migration with it instead.
- customers-gitlab-com: because this is a Rails app, setting Tailwind up might give some good data on how to proceed in GitLab (however, it's worth noting that both apps don't use the same gems for frontend assets compilation).
Reference implementation
- GitLab UI reference implementation.
- GitLab reference implementation.
- design.gitlab.com reference implementation.
These MRs are rough PoCs of how the Tailwind CSS setup would look like in GitLab UI, and integrated in GitLab.