Page MenuHomePhabricator

Evaluate pros & cons of CSS utility classes
Closed, DeclinedPublic

Description

Language team has been pointing out the interest in using CSS utility classes.

We need to collect pros & cons about such framework around topics like

  • developer experience
    • An API on top of an API
    • learning curve
  • functionality
    • RTL support
  • performance
  • theme-ability
  • pitfalls in integration into MediaWiki environment
Problem statement

Beneficial touchpoints from previous experience
Component and layout classes

  • grid classes
  • breakpoints

Component internal classes, something alongside design tokens

  • typography derived, like text-xs, text-s
  • spacing classes like spacing-s
  • color derived, like color-progressive, color-error

Known frameworks

Such could possibly be provided by a CSS framework like Tailwind CSS.

Further reading

Acceptance Criteria

Update this task to include pro and con arguments for CSS utility classes.

This task can be referenced during the relevant discussion at the Developer Summit as an aid to reaching a decision.

This task must be completed prior to August 14.

Event Timeline

We should make sure we choose a library that properly supports RTL. It seems Tailwind has support, but potentially a bit over complicated, see: https://github.com/tailwindlabs/tailwindcss/discussions/1492#discussioncomment-6061

From that comment, it seems you'd have to take something like this:

<div class="text-green border pl-4 mr-1 right-0 rounded-l-md">

And write it to support RTL like this:

<div class="text-green border ltr:pl-4 ltr:mr-1 ltr:right-0 ltr:rounded-l-md rtl:pr-4 ltr:ml-1 rtl:left-0 rtl:rounded-r-md">

That said, that comment also points to a tailwind-rtl plugin that seems to use "start" and "end" (instead of 'left' and 'right') so that might be an interesting one to check, depending on how well supported it is.

Using Tailwind and similar solutions makes it basically a given that any local CSS customisation by interface administrators gets a lot more complicated, unless you also require writing understandable, human-readable classes with every Tailwind declaration. I think such customisability is still important, especially when sometimes tools like ContentTranslation can do non-discussed modifications to the wikis (such as the advertising banner it was/is showing on every article creation) that turn out to be controversial in communities and need to be removed/modified locally. If these things would get coded entirely in Tailwind, this will become much harder.

I think we need to focus on a css utility library and not on tailwind because disucssion will focus on good and bad part of tailwind. That is not the intention here. Declarative rendering using Vue is better done if its styling also done in declarative way rather than depending on repeated low level css styles in each SFCs. This is also a way to softly enforce the design system and WMF theming consistantly.

Language team does not use tailwind, we started from basic grid helpers and then expanded to spacing utilities with script directionality support. Personally I have not evaluated or used tailwind so far. I mentioned it as an example. Storybook

@Volker_E Can I request editing the ticket so that we dont discuss tailwind library but discuss the generic issue?

Volker_E renamed this task from Evaluate pros & cons of CSS utility classes (Tailwind CSS) to Evaluate pros & cons of CSS utility classes.Jul 1 2021, 9:33 AM
Volker_E updated the task description. (Show Details)

The reason I've mentioned Tailwind, was that it was mentioned in the conversations before, for discoverability and because is probably the most well-known CSS utility-class framework.

@santhosh From your past implementation experience has there be any other strong use case apart from grid and spacing classes?

Apart from grid and spacing, responsive layout related classes are helpful. For example, if we define, xs, sm, md, lg, xl as breakpoints, then a class xs says display only in xs, md-and-up says, display this element in md and above sized screens etc. We have used this in section translation. We defined the following classes: xs, 'sm`, 'md`, 'lg`, xl, sm-and-down, sm-and-up, md-and-down, md-and-up, lg-and-down, lg-and-up.

Another usecase is typography related - We have not used this in section translation, but utility classes based on design tokens would be nice. For example. text-small, text-large, text-base, text-xs. This get translated to corresponding design tokens. By manual review or linting, we can enforce that font sizes other than this is not used in an application.

In general, I expect a utility class corresponding to a top level design tokens. So that would include colors(progressive, error, warnign, ascend, secondayr, placeholder etc), text alignments(text-start, text-end, text-center), borders etc. In other words, when we design and develop design tokens, if we see that certain style aspects should never overridden by end-developers, name them as utility classes, document, enforce by linting.

In practice, these utility classes are discoverd in development and abstract away to the library.

Example design system with design tokens and corresponding utility classes: https://designsystem.digital.gov/design-tokens/ https://designsystem.digital.gov/utilities/

When I was playing with bootstrap https://www.mediawiki.org/wiki/User:TheDJ/bootstrap. I actually had to drop padding and margin classes from my template styles, because it was so much css that wasn’t directly needed for the demo.

The full bootstrap grid code, with templatestyling prefixing is 63000 characters after minification. By dropping all the margin and padding options, this can be reduced to 28000

No judgement on any framework just wanted to mention that.

One data point from WMDE:
As far as I can tell we're almost never using utility classes, with the one exception of sr-only and even that usage would probably be more semantic if replaced by aria-label where we used it.

In Query Builder, our first project to use the Design System, we're also using the Design System Tokens for layout spacing. However, we're using them inside the BEM-classes in the <style lang="scss">-section of our Single File Components. See for example https://gerrit.wikimedia.org/r/plugins/gitiles/wikidata/query-builder/+/79e0d5b20336e6bf2d481019a128efa189e0cd69/src/components/QueryBuilder.vue#210

Thanks for the feedback @santhosh, @Michael and the added point by @TheDJ.
I think we need to separate concerns here. The grid & possibly breakpoint utility classes are not only concerning components, but also the surrounding they are embedded in. That's different usage from an SPA, a higher complex component nesting other components like the planned language switcher perspective vs a component usage like TypeaheadSearch in modern Vector.
For overarching grid T90687, I could imagine utility classes as one of the possible solutions.

For the lower level, component inherent properties like typography or spacing I still don't see the convincing benefit in such path over using a tokens and mixins or extend approach.

Let's go into an example component style definition for a moment:

.wvui-input {
	
	&__input {
		background-color: @background-color-base;
		color: @color-base--emphasized;
		display: block;
		box-sizing: border-box;
		width: @size-full;
		height: @size-base;
		margin: 0;
		border: @border-width-base @border-style-base @border-color-base;
		border-radius: @border-radius-base;
		padding: @padding-input-text;
		box-shadow: @box-shadow-base;
		font-family: inherit;
		font-size: inherit;
		// Example comment per component CSS property.
		line-height: @line-height-component;
	

Note, that there's a shorthand for @border-base as well, which hasn't yet been used.
This is a close to ideal outcome from my point of view. The centralized tokens are

  • small in number,
  • with little developer abstraction need,
  • simply to change from a theming perspective.

Notice the ability to comment on the styles per component. Not only would utility classes themselves clutter up the template (thanks @AnneT for this input!) and the rendered markup, bringing the same styling comments to the template would multiply this problem. We've received negative feedback about such cluttering from several devs on OOUI using multiple (in a way utility similar) classes, for example:
<span class="oo-ui-widget oo-ui-widget-enabled oo-ui-buttonElement oo-ui-buttonElement-framed oo-ui-labelElement oo-ui-flaggedElement-progressive oo-ui-buttonWidget" aria-disabled="false" aria-labelledby="ooui-10">

Also note, that the reason font & text tokens are currently missing is our relative font size complexity across skins. While DSG has defined 16px as base font size and MinervaNeue is following this, Vector is currently stuck at 14px base (see T254055 for a change attempt) and Monobook at 12.7px. Tokens and also utility classes would have to cater to this complexity and this hasn't been prioritized in the past. See T288110 for an idea how to clear the path for this.

The grid & possibly breakpoint utility classes are not only concerning components, but also the surrounding they are embedded in.

yes, Note that the utility classes are not only for the components in the UI library, but for building UI for an application that use the component library

For the lower level, component inherent properties like typography or spacing I still don't see the convincing benefit in such path over using a tokens and mixins or extend approach.

Similary typography, spacing units are also should be governed by design system and they need to be used in higher level components just like how they are used in buttons. Here is an example from Mediawiki XYZExtension:

<template>
   <div class="row container">
       <div class="xyz-name col-6 ps-2 text-ascend">Some label</div>
       <div class="xyz-value col-6 ps-2 text-small">Some Value</div>
    </div>
</template>
<script>
</script>
<style>
// import the shared componenent library css 
</style/>

Now, this renders as a row with equal sized columns with padding-left: 2*spacing unit. In RTL context, it becomes padding-right with 2 spacing unit. All without writing any css style, except the import(which can be done at applicaiton level). This is because the shared component library provided utilty classes for layout and spacing by abstracting design tokens, grid metrics, RTL/LTR details.

One could argue that the developer of XYZExtension can write this without the utility classes from shared component library as follows:

<template>
   <div class="container">
       <div class="xyz-name">Some label</div>
       <div class="xyz-value">Some Value</div>
    </div>
</template>
<script>
</script>
<style>
.container{
  display: flex; 
  flex-direction: column
}
.xyz-name {
   padding-left: 8px;
  font-size: 1.2em;
  font-color: @color-base--primary;
}
.xyz-value {
   padding-left: 8px;
  font-size:  12px;
}

</style/>

Do you see that pixel values, low level design tokens get leaked into higher layer application like this extension? A designer for this extension will use the vocabulary of pixels, and design token to communcate the design to developers. Non standard spacing units, typography units will leak into UI.

Everything that goes under style tag in higher level UI components are custom styling. A descipline to limit that to minimum is desirable. Composing UI using utility classes make the resulting styles we ship very compact and minimum.

This is where utility classes becomes helpful.

Not only would utility classes themselves clutter up the template

Vue templating is declarative style UI authoring system. So templates usually have declrations about UI rendering. I request you to take a look at some applications using the utility systems provided by Vuetify, tailwind etc

By not having utility classes we are not avoid cluttering, We just moves that to stylesheets and duplicate the code. Slowly defeating the single source of truth envisioned by design tokens by not embrasing it to heigher levels.

Utility classes are there to reduce the amount of CSS statements you would otherwise write, correct ? Allowing you to author quicker, reduce size of shipped CSS and making more readable html where from the classes you can read how the html would render.
But I think within wikipedia, we have a lot of custom stuff due to the sheer size of components / extensions / skins insane levels of backwards compatibility etc so that no matter what you will still always end up with a lot of additional CSS statements that you require. So then you have multiple stacked utility classes (as Volker also mentioned) and additional css. Neither of which will be super readable...

Also a lot of the utility class frameworks depend on compile reduction to reduce the size of the asset delivered. But MediaWiki is so big that first of all you are guaranteed to use almost everything somewhere and second of all, we have no atomic build step that builds the css for the entire site. So effectively you are shipping everything all the time, at worst u have partial duplication. And we don't even have css rule reduction to begin with so...

Basically as far as I have experienced, css utility classes work very well for application building, but less well for lots of components that are used randomly all over the place. I believe Bootstrap recognises this too. If I remember correctly, their components directly use their CSS variables, and their utility classes are for layouting/placing the components. (Their utility classes also use their CSS variables of course)

But... this is not the proposed alternative to utility classes:

<template>
   <div class="container">
       <div class="xyz-name">Some label</div>
       <div class="xyz-value">Some Value</div>
    </div>
</template>
<script>
</script>
<style>
.container{
  display: flex; 
  flex-direction: column
}
.xyz-name {
   padding-left: 8px;
  font-size: 1.2em;
  font-color: @color-base--primary;
}
.xyz-value {
   padding-left: 8px;
  font-size:  12px;
}

</style/>

This is:

<template>
   <div class="container">
       <div class="xyz-name">Some label</div>
       <div class="xyz-value">Some Value</div>
    </div>
</template>

<script>
</script>

<style>
// import tokens from design system here

.container{
  display: flex; 
  flex-direction: column;
}
.xyz-name {
  padding-inline-start: @dimension-layout-xsmall;

  // probably even using a Design System typography mixin for the following:
  font-family: @font-family-style-description;
  font-weight: @font-weight-style-description;
  font-size: @font-size-style-description;
  line-height: $font-line-height-style-description;
  color: $font-color-base;
}
.xyz-value {
   padding-inline-start: @dimension-layout-xsmall;
  // using the mixin here again
}

</style/>

Or am I misunderstanding you in some way?

Or am I misunderstanding you in some way?

What you wrote is the alternative to utility class written in a better way by using all design tokens. But we lose design vocabulary and we are composing it using raw css and repeating very often.

Utility CSS classes are reusable abstractions. In my example the following are utility classes: row, col-2, text-small, ps-2. They are supposed to be defined in WVUI or upcoming library.

The following are not utility classes xyz-value, xyz-name. They are application specific non resuable classes. The mixins and css styles under .xyz-name in your version is redundant and a developer using a design system is not supposed to write it. Developers using design system use higher level vocabulary and does not directly deal with design tokens unless such customization and overriding is justified.

Utility classes are there to reduce the amount of CSS statements you would otherwise write, correct ? Allowing you to author quicker, reduce size of shipped CSS and making more readable html where from the classes you can read how the html would render.

Yes

Basically as far as I have experienced, css utility classes work very well for application building, but less well for lots of components that are used randomly all over the place.

You are right. To get best experience of design system and utility classes, we need build system, but as far as I understand, we are moving towards that idea. Ofcourse not immediate, but frontend build system is an active discussions

I believe Bootstrap recognises this too. If I remember correctly, their components directly use their CSS variables, and their utility classes are for layouting/placing the components. (Their utility classes also use their CSS variables of course)

As per https://getbootstrap.com/docs/5.0/utilities/api/ they have a rich set of utlity classess beyond layouting and spacing. And they are based on a design token system(SASS maps)

Utility CSS classes are reusable abstractions. In my example the following are utility classes: row, col-2, text-small, ps-2. They are supposed to be defined in WVUI or upcoming library.

Ah, gotcha. I think we're getting somewhere here. I agree that things like row, col-2 or text-small can be useful abstractions. But they should be primarily available as (Less) mixins, so that they can be reused in the <style> section of a component. That way, we can retain the horizontal separation of concerns of a Vue.js SFC app into semantic structure/content, behavior, and looks. Whereas using utility classes in the <template> section would introduce styling concerns in a place where it doesn't belong, like using inline styles.

However, I still see value in also mapping those proposed mixins/abstractions to utility classes. That way they can be reused by Gadget authors and others that do not have access to a build step and have to rely on CSS directly.

I think it is safe to say that we're not going to pursue this approach in Codex. However, we are going to move forward with a related CSS-components initiative (see T321351: [EPIC] Add CSS-only components and related tasks). So less "Tailwind" and more "Bootstrap" basically.

Plus 1 to @egardner. Tailwind is architecturally very different from Wikimedia's other CSS approach(es); and it's distinct features don't outweigh possible pitfalls to popularize it.