Skip to content

Extract and generate custom utilities completely with tailwind

What does this MR do and why?

Create extraction script for utility classes

This is the first step towards generating all utility classes with tailwind. The idea behind this is the following:

Parse all utility classes being provided by @gitlab/ui into a JSON object where each class is represented as something like this:

'.bg-gray-10': {
  'background-color': 'var(--gray-10, #fbfafd)',
},

We then let tailwind generate all the utilities and compare the class definitions above with the class definitions in @gitlab/ui. They can fall into four categories:

  • exact matches: The tailwind generated class matches @gitlab/ui 1:1
  • potential mismatches: The tailwind generated class mismatches significantly. This can be okay or we may need some adjustments to the tailwind config.
  • hardcoded colors: Some of the utility classes have hard coded colors which the conversion script is not able to resolve to a CSS variable.
  • safe to use legacy utils: These utils have no overlap with tailwind at all. The class names completely differ. This means we can feed the definitions above into tailwind, so that tailwind is able to generate the classes

As part of the conversion certain aspects are normalized, for example colors like #fbfafd are normalized to var(--gray-10, #fbfafd), but only if the class name contains gray-10.

Furthermore we currently check-in two files that later will be git-ignored:

  1. config/helpers/tailwind/css_in_js.js – containing the css-in-js definitions for tailwind to generate the legacy utils with tailwind
  2. config/helpers/tailwind/all_utilities.haml – containing a list of all utility classes, so that tailwind is able to generate all utilities and not just the used ones

Align tailwind and legacy utils

Through some manual comparison and config changes, we are able to align a significant part of utilities between tailwind and the legacy utilities.

The following plugins have been aligned by extending the tailwind theme, writing some custom CSS or manually declaring them to be the same value:

  • accessibility (gl-sr-only): There is a mismatch between the two, but the result is the same. (border-width: 0 vs border: 0)
  • borderRadius (gl-rounded-full): Tailwind uses 9999px while we use 50% for full. By extending the theme we can resolve the mismatch between the two.
  • borderWidth (gl-border, etc): This one is a little tricky because our shorthands gl-border, gl-border-[rltb] define not just border-width: 1px but border: solid 1px #dcdcde. This means we need to ensure that they are defined before any other utility classes that have an impact on the border-width, border-style and border-color. Therefore we define those 5 classes manually at the beginning of our tailwind file, so that the other utility classes can overwrite those values.
  • boxShadows (gl-shadow, etc): We update the definitions in the theme. Unfortunately the resulting CSS still doesn't match a 100% because tailwind uses some --tw custom properties. But visually they match.
  • rotate (gl-rotate-90, etc): We directly set transform: rotate(90deg) while tailwind works by setting a custom property --tw-rotate. This means that tailwind's classes are composable (e.g. gl-rotate-90 gl-translate-x-0 while our's would overwrite each other's transform property. We should just prefer tailwind's methodology over ours.
  • translate (gl-translate-x-0, etc): see rotate above

The following plugins have been aligned by disabling the corePlugins and relying on custom cssInJS definitions. We probably can deal with these differences later:

  • backgroundImage (gl-bg-none): Tailwind sets background-image: none while we are setting `background:none.
  • lineClamp (gl-line-clamp-1, etc): Our sets white-space: normal, which tailwind doesn't do.
  • opacity (gl-opacity-1 to gl-opacity-10): Tailwind has a more granular scale from 1 to 100, while we use a scale from 1 to 10.
  • outlineStyle (gl-outline-none): in tailwind this class uses 2px solid transparent, while we are setting outline: none. Tailwind probably has it's reasons and we should dig deeper
  • outlineWidth (gl-outline-0): we are using outline: 0 while tailwind uses outline-witdh: 0px. This probably is an alright difference, but we should double-check

Migrate a few hardcoded color to variable ones

The algorithm only normalizes a color like #fbfafd to var(--gray-10, #fbfafd) if the class name contains gray-10.

Therefore we add a map of all remaining classes which contain a hardcoded color to their corresponding variable color.

Compare dark and light mode utilities

The whole conversion process from GitLab UI utilities to Tailwind utilities is done on the light mode version of our utilities. All the hard-coded colors are replaced with CSS custom properties, now we should verify that:

  1. The classes generated by tailwind match the light mode version
  2. The classes generated by tailwind match the dark mode version

So we created a little script to compare the two.

While doing this, we have found 3 minor inconsistencies:

  1. The .gl-dark .gl-dark-invert-keep-hue util. It probably should be part of our dark mode overrided.
  2. The .t-gray-a-08 color is overwritten in app/assets/stylesheets/themes/_dark.scss
  3. The text-secondary color as well.

Add linting around tailwind utils to CI

This changes the two newly introduced scripts slightly.

scripts/frontend/convert_utils_to_css_in_js.mjs has been renamed to scripts/frontend/tailwind_all_the_way.mjs. It now executes, if called directly:

  1. Compiles application_utilities_to_be_replaced.css files
  2. Does run the extraction like before
  3. Compiles tailwind_all_the_way.css

scripts/frontend/compare_css_util_classes.mjs now:

  1. Calls the the script above, if there are newly hardcoded colors or mismatching utils, the exit code will be 1.
  2. Does the comparison, like before, but exiting with a 1 if there is a mismatch between tailwind and light mode / dark mode utils.

This allows us to call the first script when compiling assets and the second script during static-analysis in CI.

Furthermore it allows us to remove the checked-in all_utilities.haml and css_in_js.js and ignore them instead because the scripts create them as needed.

MR acceptance checklist

Please evaluate this MR against the MR acceptance checklist. It helps you analyze changes to reduce risks in quality, performance, reliability, security, and maintainability.

Screenshots or screen recordings

Before After
Screenshot_2024-04-02_at_20.19.15 Screenshot_2024-04-02_at_20.19.34

How to set up and validate locally

Note: Please revert the changes below after a review, otherwise you might run into CSS issues on other branches

  1. In rails console (or via toogle: http://127.0.0.1:3000/rails/features)
    Feature.enable(:tailwind_all_the_way)
  2. In your GDK, add: export TAILWIND_ALL_THE_WAY=true to env.runit
  3. Restart webpack / vite: gdk restart webpack / gdk restart vite
  4. Visit any page. The page should look visually alright and a CSS file named assets/tailwind_all_the_way-[hash].css should be loaded
Edited by Lukas 'ai-pi' Eipert

Merge request reports