Skip to content

Components: Start using CSS Modules #103004

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: trunk
Choose a base branch
from
Open

Components: Start using CSS Modules #103004

wants to merge 2 commits into from

Conversation

mirka
Copy link
Member

@mirka mirka commented Apr 30, 2025

Closes ARC-85

Proposed Changes

This lays the foundations so we can start using CSS Modules for new components added to @automattic/components.

Webpack already supports this, so all we need to add is some type declarations for better devex. I converted WordPressLogo as a simple example — we can't actually remove the problematic wordpress-logo class until the existing usages are addressed.

An IDE extension like this is recommend for autocompletion and go to definition. I would've liked to also add typescript-plugin-css-modules, but we're blocked by this limitation.

Why are these changes being made?

Non-obscured CSS classes have been problematic as they encourage consumers to override inner styles, making them fragile to changes. We also see name collision issues due to poor namespacing. CSS Modules also tend to be safer for AI-assisted coding since we don't have to worry about name collisions.

Testing Instructions

  • yarn components:storybook:start and check the WordPressLogo story. The classname containing the styles should be obscured, while the wordpress-logo classname is kept for back compat.
  • Check one of the existing usages for regressions.

Pre-merge Checklist

  • Has the general commit checklist been followed? (PCYsg-hS-p2)
  • Have you written new tests for your changes?
  • Have you tested the feature in Simple (P9HQHe-k8-p2), Atomic (P9HQHe-jW-p2), and self-hosted Jetpack sites (PCYsg-g6b-p2)?
  • Have you checked for TypeScript, React or other console errors?
  • Have you used memoizing on expensive computations? More info in Memoizing with create-selector and Using memoizing selectors and Our Approach to Data
  • Have we added the "[Status] String Freeze" label as soon as any new strings were ready for translation (p4TIVU-5Jq-p2)?
    • For UI changes, have we tested the change in various languages (for example, ES, PT, FR, or DE)? The length of text and words vary significantly between languages.
  • For changes affecting Jetpack: Have we added the "[Status] Needs Privacy Updates" label if this pull request changes what data or activity we track or use (p4TIVU-aUh-p2)?

@mirka mirka added Components [Type] Tooling Related to tools, scripts, automation, DevX, etc. labels Apr 30, 2025
@mirka mirka self-assigned this Apr 30, 2025
@matticbot
Copy link
Contributor

Here is how your PR affects size of JS and CSS bundles shipped to the user's browser:

App Entrypoints (~78 bytes added 📈 [gzipped])

name           parsed_size           gzip_size
entry-stepper       +116 B  (+0.0%)      +35 B  (+0.0%)
entry-login         +116 B  (+0.0%)      +44 B  (+0.0%)

Common code that is always downloaded and parsed every time the app is loaded, no matter which route is used.

Sections (~440 bytes added 📈 [gzipped])

name                                parsed_size           gzip_size
woocommerce-installation                 +116 B  (+0.0%)      +49 B  (+0.0%)
woocommerce                              +116 B  (+0.0%)      +49 B  (+0.1%)
themes                                   +116 B  (+0.0%)      +49 B  (+0.0%)
theme                                    +116 B  (+0.0%)      +49 B  (+0.0%)
subscribers                              +116 B  (+0.0%)      +49 B  (+0.0%)
stats                                    +116 B  (+0.0%)      +49 B  (+0.0%)
staging-site                             +116 B  (+0.0%)      +49 B  (+0.0%)
sites-dashboard                          +116 B  (+0.0%)      +49 B  (+0.0%)
site-settings                            +116 B  (+0.0%)      +49 B  (+0.0%)
site-purchases                           +116 B  (+0.0%)      +49 B  (+0.0%)
site-performance                         +116 B  (+0.0%)      +49 B  (+0.0%)
site-monitoring                          +116 B  (+0.0%)      +49 B  (+0.0%)
site-logs                                +116 B  (+0.0%)      +49 B  (+0.0%)
settings-writing                         +116 B  (+0.0%)      +49 B  (+0.0%)
settings-security                        +116 B  (+0.0%)      +49 B  (+0.0%)
settings-reading                         +116 B  (+0.0%)      +49 B  (+0.0%)
settings-podcast                         +116 B  (+0.0%)      +49 B  (+0.0%)
settings-performance                     +116 B  (+0.0%)      +49 B  (+0.0%)
settings-newsletter                      +116 B  (+0.0%)      +49 B  (+0.0%)
settings-jetpack                         +116 B  (+0.0%)      +49 B  (+0.0%)
settings-discussion                      +116 B  (+0.0%)      +49 B  (+0.0%)
settings                                 +116 B  (+0.0%)      +49 B  (+0.0%)
scan                                     +116 B  (+0.0%)      +49 B  (+0.0%)
reader                                   +116 B  (+0.0%)      +52 B  (+0.0%)
purchases                                +116 B  (+0.0%)      +49 B  (+0.0%)
promote-post-i2                          +116 B  (+0.0%)      +49 B  (+0.0%)
preview                                  +116 B  (+0.0%)      +49 B  (+0.1%)
posts-custom                             +116 B  (+0.0%)      +49 B  (+0.0%)
posts                                    +116 B  (+0.0%)      +49 B  (+0.0%)
plugins                                  +116 B  (+0.0%)      +49 B  (+0.0%)
plans                                    +116 B  (+0.0%)      +49 B  (+0.0%)
people                                   +116 B  (+0.0%)      +49 B  (+0.0%)
pages                                    +116 B  (+0.0%)      +49 B  (+0.0%)
overview                                 +116 B  (+0.0%)      +49 B  (+0.0%)
migrate                                  +116 B  (+0.0%)      +49 B  (+0.0%)
media                                    +116 B  (+0.0%)      +49 B  (+0.0%)
marketplace                              +116 B  (+0.0%)      +49 B  (+0.0%)
marketing                                +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-social                           +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-search                           +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-connect                          +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-settings                   +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-pricing                    +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-plugin-management          +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-partner-portal             +116 B  (+0.0%)      +43 B  (+0.0%)
jetpack-cloud-overview                   +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-features-comparison        +116 B  (+0.0%)      +49 B  (+0.0%)
jetpack-cloud-agency-dashboard           +116 B  (+0.0%)      +43 B  (+0.0%)
jetpack-cloud                            +116 B  (+0.0%)      +49 B  (+0.0%)
import                                   +116 B  (+0.0%)      +49 B  (+0.0%)
hosting                                  +116 B  (+0.0%)      +49 B  (+0.0%)
home                                     +116 B  (+0.0%)      +49 B  (+0.0%)
gutenberg-editor                         +116 B  (+0.0%)      +49 B  (+0.0%)
google-my-business                       +116 B  (+0.0%)      +49 B  (+0.0%)
github-deployments                       +116 B  (+0.0%)      +49 B  (+0.0%)
export                                   +116 B  (+0.0%)      +49 B  (+0.0%)
email                                    +116 B  (+0.0%)      +49 B  (+0.0%)
earn                                     +116 B  (+0.0%)      +49 B  (+0.0%)
domains                                  +116 B  (+0.0%)      +49 B  (+0.0%)
customize                                +116 B  (+0.0%)      +49 B  (+0.1%)
concierge                                +116 B  (+0.0%)      +49 B  (+0.0%)
comments                                 +116 B  (+0.0%)      +49 B  (+0.0%)
checkout                                 +116 B  (+0.0%)      +49 B  (+0.0%)
backup                                   +116 B  (+0.0%)      +49 B  (+0.0%)
add-ons                                  +116 B  (+0.0%)      +49 B  (+0.0%)
activity                                 +116 B  (+0.0%)      +49 B  (+0.0%)
account                                  +116 B  (+0.0%)      +48 B  (+0.0%)
accept-invite                            +116 B  (+0.0%)      +43 B  (+0.0%)
a8c-for-agencies-sites                   +116 B  (+0.0%)      +42 B  (+0.0%)
a8c-for-agencies-referrals               +116 B  (+0.0%)      +43 B  (+0.0%)
a8c-for-agencies-plugins                 +116 B  (+0.0%)      +49 B  (+0.0%)
a8c-for-agencies-partner-directory       +116 B  (+0.0%)      +39 B  (+0.0%)
a8c-for-agencies-overview                +116 B  (+0.0%)      +42 B  (+0.0%)
a8c-for-agencies-migrations              +116 B  (+0.0%)      +34 B  (+0.0%)
a8c-for-agencies-client                  +116 B  (+0.0%)      +47 B  (+0.0%)

Sections contain code specific for a given set of routes. Is downloaded and parsed only when a particular route is navigated to.

Async-loaded Components (~374 bytes added 📈 [gzipped])

name                                             parsed_size           gzip_size
async-load-signup-steps-user                          +116 B  (+0.0%)      +50 B  (+0.0%)
async-load-signup-steps-site-picker                   +116 B  (+0.0%)      +45 B  (+0.1%)
async-load-signup-steps-plans-theme-preselected       +116 B  (+0.0%)      +47 B  (+0.0%)
async-load-signup-steps-plans                         +116 B  (+0.0%)      +49 B  (+0.0%)
async-load-signup-steps-domains                       +116 B  (+0.0%)      +47 B  (+0.0%)
async-load-signup-steps-difm-site-picker              +116 B  (+0.0%)      +45 B  (+0.1%)
async-load-design-blocks                              +116 B  (+0.0%)      +42 B  (+0.0%)
async-load-design                                     +116 B  (+0.0%)      +49 B  (+0.0%)

React components that are loaded lazily, when a certain part of UI is displayed for the first time.

Legend

What is parsed and gzip size?

Parsed Size: Uncompressed size of the JS and CSS files. This much code needs to be parsed and stored in memory.
Gzip Size: Compressed size of the JS and CSS files. This much data needs to be downloaded over network.

Generated by performance advisor bot at iscalypsofastyet.com.

@@ -1,4 +1,4 @@
.wordpress-logo {
.wordpressLogo {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kebab casing also works, but camel case is the official recommendation.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Kebab casing also works, but camel case is the official recommendation.

It's an interesting topic. It definitely feels more natural in JavaScript to reference with a camelCase property, and ultimately the class names are clobbered before they get to the client so it doesn't matter there.

That said, with all the inherited experience of BEM or general kebab-case styling in CSS, I wonder if this will be an issue for people, especially in transitioning between files with different conventions. Are there ways we can make this easier, or enforce some consistency within respective file types? Thinking documentation or tooling like Stylelint rules.

I also personally don't have a huge issue with referencing the kebab-case properties to keep consistency, even if it's a little less ergonomic.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No strong opinion on my end. The ergonomics/safety characteristics aren't that different once you're using an IDE extension or the ts plugin.

One argument in favor of camel case is that it'll be more obvious that the file is a module, not a horrible BEM file 😂

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have a pretty strong opinion to keep using kebab case, at least until it's the official recommendation of the WordPress CSS Coding Standards

@@ -4,3 +4,13 @@ declare module '*.svg' {
}

declare const __i18n_text_domain__: string;

declare module '*.module.css' {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Keeping this to only @automattic/components for now, but I think we'll also want it in at least client/ as well.

@mirka mirka requested a review from a team April 30, 2025 12:44
@matticbot matticbot added the [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. label Apr 30, 2025
@@ -1,4 +1,4 @@
.wordpress-logo {
Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You may have noticed that there's a redundant copy of this component in calypso/components 😇 #103012

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Let's kill it with fire! A migration is a matter of a single Cursor prompt these days. The v2 hosting dashboard eslint rules might be an exception that will need a manual hand.

Copy link
Member

@aduth aduth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd be personally curious for feedback from others on kebab-case vs. camelCase, but otherwise LGTM.

I'm not entirely familiar how we expect this to be integrated into build pipelines, so a few related questions for my own understanding:

  • Does this impose additional requirements on downstream consumers of @automattic/components to be able to support CSS modules? Do we need to document that?
  • Maybe dependent on project & build tool, but where do these styles end up? I see in the Storybook preview they're injected through JavaScript, but would we want/expect them to be loaded as stylesheets in production environments?

Comment on lines +22 to +23
// and should be normalized to be merged instead. Ideally, we also remove the `wordpress-logo`
// class because it clashes very easily.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ideally, we also remove the wordpress-logo class because it clashes very easily.

With the eventual removal of wordpress-logo, what's the desired end-state where a component is currently overriding styles of .wordpress-logo ? Would that component be expected to pass its own className to reference it in its styles?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Removing the hardcoded .wordpress-logo would be a breaking change, although I'm ok with it if communicated well.

Would that component be expected to pass its own className to reference it in its styles?

Yes, that's my expectation

@mirka
Copy link
Member Author

mirka commented Apr 30, 2025

  • Does this impose additional requirements on downstream consumers of @automattic/components to be able to support CSS modules? Do we need to document that?
  • Maybe dependent on project & build tool, but where do these styles end up? I see in the Storybook preview they're injected through JavaScript, but would we want/expect them to be loaded as stylesheets in production environments?

Thank you for asking this, I hadn't fully considered that part. I took some time to investigate a bit more:

The @automattic/components package currently ships with uncompiled, unbundled SCSS files. Nearly all the "packages" in this repo just do a tsc as their build step — no CSS/SCSS processing. It seems like the setup is more for in-repo use and less for external portability.

So as of today, a consuming project already needs to handle CSS/SCSS concerns themselves. We can assume this will be sass-loader + css-loader + style-loader in Webpack, or out of the box handling in Vite. Either will process CSS Modules by default unless modules are specifically disabled. I think it would be reasonable to expect A8C consumers to do some minor troubleshooting in the rare cases where there are config tweaks they need to make to get modules working.

Though, I definitely think we should consider shipping the public npm package with SCSS and CSS Modules preprocessed, at least. I understand we may want to leave flexibility in how consuming projects want to handle the compiled CSS though (injection, extraction, etc).

(Needless to say, if we were to start using CSS Modules in @wordpress/components, we'd have to preprocess everything, JS and final CSS file and all.)

@Automattic/team-calypso Does this sound reasonable? Requiring that consuming projects handle CSS Modules on their own for now? And that we should consider Webpack bundling the published @automattic/components package, sooner or later?

Whatever we decide, I can add it to the package readme.

Copy link
Member

@tyxla tyxla left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks for working on it, Lena!

@Automattic/team-calypso Does this sound reasonable? Requiring that consuming projects handle CSS Modules on their own for now? And that we should consider Webpack bundling the published @automattic/components package, sooner or later?

I'd be curious to see what that looks like before I can tell. The existing use in Jetpack might be a good case to consider:

https://github.com/Automattic/jetpack/blob/3d51e972fb6023a81b6a9feb1fb34eec9c7a6836/projects/packages/jetpack-mu-wpcom/src/features/wpcom-dashboard-widgets/celebrate-launch/celebrate-launch-modal.js#L1

Ideally, it should be quite straightforward to use the package as part of the DS, with as few steps as possible.

@youknowriad
Copy link
Contributor

Should we update the CSS guidelines of Calypso to clarify the new approach?

@mirka
Copy link
Member Author

mirka commented May 6, 2025

Should we update the CSS guidelines of Calypso to clarify the new approach?

Eventually, yes, but I'm still a bit hesitant to encourage a widespread roll out just yet. I'm hoping we can get a feel of the best practices first in a more contained/controlled scope.

@ciampo
Copy link
Contributor

ciampo commented May 6, 2025

I'd personally feel safer if the package just shipped its built CSS styles, thus making its internal stack irrelevant to consumers. Is that something completely out of the picture?

@mirka
Copy link
Member Author

mirka commented May 9, 2025

I'd personally feel safer if the package just shipped its built CSS styles, thus making its internal stack irrelevant to consumers. Is that something completely out of the picture?

There are levels to "building CSS styles":

  1. Does nothing.
  2. The CSS Module files are built to normal stylesheets, and the JS is built to plaintext class names.
  3. The SCSS stylesheets are built to CSS.
  4. The CSS stylesheets are bundled into a single stylesheet.

The current @automattic/components package is Level 0, and @wordpress/components is Level 3.

I don't think it's beneficial for @automattic/components to go up to Level 3, as the CSS won't be treeshakeable. The maximum we would go to is Level 2.

And my argument here is that most modern build tooling that is set up to consume a Level 1–2 components package can basically consume a Level 0 package, often with no configuration changes.

I am totally fine with bumping @automattic/components up to a Level 2, but it probably won't make a big difference in most of our consumer projects.

Copy link
Contributor

ciampo commented May 9, 2025

Got it. I think I'd be ok with taking the laziest approach (ie. lowest ranking in Lena's scale) that won't break the bulid process of the projects consuming this package — ie. if we can get away with 0 and projects like calypso / jetpack / woo analytics / etc will continue to build correctly, then we can start with 0.

@youknowriad
Copy link
Contributor

From my perspective, any package shipped to npm needs to be pre-built using standard technologies. An npm consumer shouldn't have to know how a package is developed internally to be able to use it. Now, how the package is pre-built is a decision the package builder does.

So I think we should:

  • build SASS
  • build CSS modules

The ideal output is that a consumer doing import Something from '@automattic/components' doesn't need to anything else for this to work out of the box.

For '@wordpress/components', we accepted the fact that CSS is built separately from JS and asked people to load the CSS manually. The reason is that WordPress has its own style management flow, so we need the CSS file to be separate. But this is not ideal as well.

I guess what I'm saying is that yes, we should do Level 2 like suggested by @mirka

I am totally fine with bumping @automattic/components up to a Level 2, but it probably won't make a big difference in most of our consumer projects.

It would make a difference, just today I saw a message on Slack that Jetpack is not building properly because SASS versions conflicts between what is expected from @automattic/components and what Jetpack is using.

@mirka
Copy link
Member Author

mirka commented May 12, 2025

It would make a difference, just today I saw a message on Slack that Jetpack is not building properly because SASS versions conflicts between what is expected from @automattic/components and what Jetpack is using.

Good point, this will likely happen with deprecations like #101983 too.

I'll put this on hold until we figure out the build (ARC-170).

@retrofox
Copy link
Contributor

I think it's related to this issue of this PRT that tries to import the Badge component

@jsnajdr
Copy link
Member

jsnajdr commented May 12, 2025

Good questions to ask are:

  1. By compiling SCSS down to CSS, are we losing any information or flexibility? For example, the SCSS stylesheets could in theory use some SCSS variables ($something) that different consumers could define differently. Theme variables or brand fonts, for example. Then the compiled CSS files will have hardcoded values and the flexibility is lost.
  2. Are we placing some unreasonable requirements on the consumers' build pipeline? If the SCSS files use some common variables, functions or mixins, the consumer's SCSS build enviroment needs to provide them. If it doesn't, things fail. Maybe that's the source of the Jetpack issue that Riad mentions.

Both questions are two sides of the same coin: extra flexibility can place extra demands on the SCSS consumer. And if there is no flexibility provided, or if the flexibility is not used, we only have extra demands/costs and nothing in return.

@ciampo ciampo mentioned this pull request May 19, 2025
8 tasks
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Components [Status] Blocked / Hold [Status] Needs Review The PR is ready for review. This also triggers e2e canary tests and wp-desktop tests automatically. [Type] Tooling Related to tools, scripts, automation, DevX, etc.
Projects
None yet
Development

Successfully merging this pull request may close these issues.

8 participants