Theming for PWA Studio #3241
Replies: 3 comments 1 reply
-
Thanks much for the documentation so far! One thing though - the regexp to catch the classnames from
|
Beta Was this translation helpful? Give feedback.
-
It looks like this approach generates a good number of empty classes at the end. .miniCart-editCartButton-A3X {
}
.miniCart-emptyCart-5-O {
}
.miniCart-emptyMessage-1Pd {
} |
Beta Was this translation helpful? Give feedback.
-
I am also experiencing a lot of disconnects with the Tailwind CSS IntelliSense VSCode extension. If I am not missing anything, not being able to use IntelliSense and auto-complete is a big impact on the developer experience. As a result, I can see a handful of developers in our agency refrain from Developer experience is a measure that should be added to the comparison table I think. It looks like mixing CSS modules with TailwindCSS has taken away the simplicity of TailwindCSS for developers to the extent that writing regular CSS is ways easier now. |
Beta Was this translation helpful? Give feedback.
-
What problem are we trying to solve?
Right now, it's difficult for agencies to customize the look and feel of the stores they're building with PWA Studio. Regardless of whether developers are using components from
venia-ui
or authoring their own, our current architecture expects each component to define its own presentation—entirely. While this arrangement gives developers maximum flexibility, it also gives them too little assistance; to fully implement a design, they may need to override every component in the app.We can do better for our developers. What they expect, and what we should deliver, are themes.
What is a theme?
A theme is an independent package that serves as the basis of an app's presentation. A theme should have the following qualities:
Why doesn't PWA Studio already support themes?
Generally, themes are either too bloated to meet the nonfunctional requirements of a PWA or too limited to support the diversity of design we see in Magento stores.
Traditionally, themes have been used to generate and serve all of the rulesets that an app could potentially need. This happens because theming systems typically aren't sophisticated enough to statically analyze an app's content—let alone scripts—to determine which rulesets are needed and which ones aren't. As a result, all rulesets must be served to the browser.
While serving the whole theme ensures that a shopper sees the correct presentation, it's also the exact kind of overserving that a PWA aims to avoid. Styles are render-blocking (must be served before the page renders), so unused rulesets end up delaying the initial render, which frustrates the shopper and leads to penalties in all the most important performance metrics for PWAs.
Some theming systems attempt to shrink their footprint by minimizing complexity (reducing the number of concepts and variants), but such an approach constrains design, so it's only viable if design authority rests with the theme. In our case, agencies give each Magento store a unique design and retain authority over its complexity, so we can't afford to constrain them arbitrarily.
We need a solution that remains optimized even as an agency's design language grows.
What would the ideal solution look like?
In addition to supporting packaged themes featuring the authoring qualities listed earlier, a theming solution should also generate output optimized for fast rendering and low bandwidth. Ideally, output would have the following characteristics:
Essentially, we need a theming system that allows us to bundle styles the same way we bundle scripts. If we could design an ideal solution, it would optimize our CSS transparently, replacing all of our duplicate declarations with shared ones in roughly three steps:
Unfortunately, automatic collection and deduplication are difficult. Components often apply classnames dynamically based on state, and generated rulesets don't always work with advanced selectors, so potential solutions become complex fairly quickly. Linkedin's CSS Blocks seems to be the most sophisticated attempt, but it uses new syntax, a runtime, and several framework integrations, and there are still a number of edges it doesn't cover; it also doesn't solve for central configuration or theme distribution.
In any case, CSS Blocks doesn't have enough activity or traction to justify refactoring PWA Studio. For this scope of change, we need something more familiar, more idiomatic, and more widely accepted—even if that means deduplication ends up being a bit more manual.
Introducing Tailwind
Tailwind is a popular CSS framework and theming system centered around a utility-first philosophy. At a high level, workflows with Tailwind involve three steps:
Tailwind config
React component
On the one hand, this workflow allows Tailwind to reach something close to the ideal solution described earlier. When authors reuse generated classnames, in effect, they're manually deduplicating whole rulesets. For highly consistent designs that benefit from such reuse, the resulting CSS bundles can be very small.
On the other hand, Tailwind's philosophy rejects the core principle behind CSS itself: separation of content and presentation. Content is inherently meaningful and presentation is inherently arbitrary, so CSS exists to separate the two into static and dynamic layers, respectively. But Tailwind asserts that, for developers trying to maintain a consistent presentation across a variety of content created by a variety of authors, content should be the dynamic layer instead—and in a world of PWAs, perhaps it already is.
Tailwind's position is compelling, but there's an important caveat: it only works if the teams applying presentation to components are also maintaining those components. Such an assumption is true for many applications, but it's not true for PWA Studio; in our case, Adobe maintains the library of Venia components, but agencies maintain only a minimal set of changes and additions. So we still prefer an arrangement where agencies can overhaul presentation from central configuration alone, without touching components.
But what if we could restrict Tailwind to the presentation layer?
Revisiting CSS Modules
Today, each
venia-ui
component has its own default presentation defined in its own.css
file. These files use acss-loader
feature called CSS Modules that hashes and namespaces selectors; when a component imports a CSS file, the imported object contains the raw classnames as keys and the translated classnames as values, and Webpack discards any unused rulesets.React component
CSS module
Thanks to this classname translation, agencies using PWA Studio don't need to worry about classname collisions when writing components. In fact, a developer can even install third-party extensions without checking for classname conflicts, since extensions may also allow
css-loader
to translate their classnames. These benefits simply aren't available with global classnames.Fortunately, CSS Modules provides another relevant feature: composition. Each ruleset may contain one or more instance of a special
composes
property, which accepts a selector as a value; whencss-loader
parses this property, it imports the rulesets for that selector and merges them into the parent, just like a preprocessor mixin. Venia components already take advantage of this feature to avoid concatenating classnames.React component
CSS module
This arrangement preserves the separation of concerns that we cherish, in that components know nothing about presentational abstractions. But there's actually another hidden benefit here: by assigning a single, semantic, locally unique classname to each significant element (and since presentational changes don't require these classnames to change) we're establishing a stable API for each component. Local classnames act as keys or identifiers, helping developers write selectors that remain accurate over time—and in the future they may even help us expose elements as Targetables for extension. It's a pattern worth keeping, and it imposes no burden on our developers.
Rather, the pattern that imposes a maintenance burden is how we use raw values. In most cases, when we write a declaration, we set a raw or arbitrary value rather than referencing a token, so we end up with lots of duplicate or inconsistent values. These values are hard for us to maintain, hard for agencies to change, and superfluous for shoppers. Colors are an exception here (we use global tokens), but as discussed earlier, our current token system would scale poorly, so expanding it to all types of values isn't an option.
What we'd like is a way for Tailwind to provide abstractions that refer to centralized values, and for our existing files (with CSS Modules) to use those abstractions. Composition would work, but Tailwind only outputs global rulesets. Perhaps, if
composes
were able to concatenate global classnames, we could get these two systems to work together.Combining Tailwind and CSS Modules
Fortunately,
composes
can target global classnames, not just local ones. This means Venia's rulesets can compose from Tailwind-generated classnames without any changes on the component side.Tailwind config
React component
CSS module
This is a good start: Tailwind generates rulesets, and components compose their presentation from those rulesets. But Tailwind generates lots of rulesets—thousands of them, totaling several megabytes—so we need to help the bundler identify which ones are in use so that it can exclude the rest during the build. To accomplish that, we just need to tell Tailwind where to look for its classnames.
Tailwind config
Perfect. Now the build will contain only the generated rulesets that our components actually use.
Comparing solutions
Does an organization actively maintain and distribute the library?
The Bootstrap team maintains the
bootstrap
package on NPM, and has published in the last 30 days.Adobe maintains packages under the
@spectrum-css
scope on NPM, and has published in the last 30 days.Tailwind Labs maintains the
tailwindcss
package on NPM, and has published in the last 30 days.Can Venia's existing rulesets consume theme variables and classnames?
Venia would need to adopt Sass (convert source, add loader) in order to use variables.
Venia would be able to use custom properties and compose classnames with no change.
Venia would be able to use custom properties and compose classnames with no change.
Can developers add and modify theme values used to generate rulesets?
User-defined Sass variables replace the defaults before the bundler generates rulesets.
The bundler only generates rulesets from default variables, so users need to serve additional rulesets.
User-defined config values replace the defaults before the bundler generates rulesets.
Do specific theme values derive from more general ones?
Components depend on several core Sass files.
Components depend on several core CSS files.
Config sections can depend on other config sections, and styles exist in discrete layers.
Does the bundler chunk and serve stylesheets over time, rather than all at once?
Users can import rulesets dynamically, but it's safer to serve them up front.
Users can import rulesets dynamically, but it's safer to serve them up front.
The bundler generates only one stylesheet, but we could eventually split it up.
Can a theme depend on another theme and reconfigure it?
A theme can depend on another, but will likely duplicate some imports.
A theme can depend on another, but will likely duplicate some imports.
A theme can designate other themes as presets and reconfigure all of them.
Can developers compose a theme from a variety of packages and plugins?
Bootstrap defines most rulesets in optional Sass files, and developers can follow the same pattern.
Spectrum distributes each component individually, and developers can follow the same pattern.
Themes can include plugins, which can use theme values to generate rulesets.
Does the bundler automatically prune unused rulesets?
The bundler includes every ruleset that the content imports.
The bundler includes every ruleset that the content imports.
The bundler excludes any rulesets that the content doesn't reference.
Implementation details
Initial setup
We need to create a theme package and configure scaffolded apps to use it by default. Scaffolded apps should only need a
tailwind.config.js
file and a dependency on the theme package.postcss-loader
andtailwindcss
to our Webpack configurationtailwind.config.js
to@magento/venia-concept
tailwind.config.js
@magento/venia-theme
package ("theme") inside the monorepotailwind.config.js
Plugin architecture
The Tailwind preset for Venia should include styles for all of our components. In order to let developers replace or exclude styles for individual components, though, we should create a Tailwind plugin for each component. Furthermore, for simplicity, we should allow developers to enable or disable plugins via configuration rather than requiring them to import plugins and apply them explicitly.
venia
property on the Tailwind configuration'stheme
entryvenia.plugins
entryvenia.plugins
before running each pluginVariable hierarchy
Specifying all of Venia's core theme values in Tailwind configuration is a good start, but we should offer developers more than a binary choice between changing one instance and changing every instance of a value. Rather, we should take this opportunity to deconstruct Venia's design by identifying common ways that core values are used and create semantically named, higher-order values that tie these use cases together. Developers always have the option to change components individually, but they should also be able to change all components that share an abstraction.
Beta Was this translation helpful? Give feedback.
All reactions