Adopt CSS in JS for styling
APPROVED BY PWG FOR REVIEW WITH MGMT IMPLEMENTATION PENDING
- 1 Background
- 2 Proposal: Adopting CSS-in-JS
- 2.1.1 Benefits
- 2.1.2 Challenges
- 2.1.3 Level of effort
- 2.1.3.1 Initial prototype – 1 - 2 weeks
- 2.2 Proposed decisions
- 2.3 Updates after prototype
Background
Today’s Paragon implementation offers both a React component library and SASS framework in a complete foundation from which to build frontend React applications. Some key refreshers:
Paragon as a SASS framework extends Bootstrap 4 (“Bootstrap 4 + Extras”)
Paragon React components depend upon SASS styles to function.
Paragon offers utility classes and mixins (
.mb-2
,.bg-primary
,@include media-breakpoint-up(md)
) for applications to modify or create new styles in an application.Theming of Paragon is done through overriding SASS variable defaults.
Paragon has been built as an extension of the Bootstrap SCSS framework since its inception.
This approach has served us well for many years, with many benefits and some challenges.
Benefits
The foundation of Paragon didn’t start from zero; We built on the shoulders of giants.
Styles are mobile responsive, cross-browser, and well tested.
Bootstrap documentation is a useful resource to lean on when Paragon documentation is incomplete or missing.
Challenges
React components are coupled with a SCSS framework.
This makes upgrading Paragon risky since outputted CSS, by its nature, has global styling implicationsFor example: If we upgrade edx-platform from Bootstrap 4.0 (with its own small modifications) to Paragon SCSS framework, there is a high risk that we introduce visual bugs throughout the system. As a result we haven’t upgraded Paragon in the platform in years.
Paragon as a library is less approachable than it could be.
As we expand the components offered in Paragon it becomes increasingly difficult to know what SCSS will apply to a given class name. In many cases digging into the Bootstrap 4 source code is necessary to understand what’s happening.Sometimes Bootstrap gets in the way of styles we’d like to implement.
TheButton
component styles for example are complicated due to extra functionality that Bootstrap offers, regarding gradients or auto choosing of text colors.Bootstrap 5 likely contains breaking changes (including dropping IE11 support).
If we upgrade to Bootstrap 5 it's likely to be considerable effort with little upside.Theming is build-time only via SCSS
It doesn’t scale well with more complex cases such as palette variations in a theme.
Key urgent problem: Upgrading Paragon in edx platform is extremely likely to introduce unexpected CSS interactions with existing styles. It will likely cause visual bugs that we miss and end up delivering to customers without heavy efforts to identify them beforehand.
Core issues with a SCSS approach to styling
SCSS and CSS is global (until we get our shadow DOM game on).
Today we mitigate this through the use of prefixes like.pgn__
or scoping#my-mfe .btn {...}
. This has been the way, but over time this means:Upgrading foundational styles in large projects is high risk
We don’t know what side-effects our new styles will have, visual breaking changes are silent.It becomes difficult to determine what styles apply to a given html element, making it time consuming to change existing styling
If there are interactions between different styles its often very hard to wade through styles to make even simple updates (example: moving icons of the course card to a new line in a mobile view in edx-platform was very difficult work JJ tackled). In MFEs we mitigate this problem by keeping our applications small, but someday it will become a problem there too.SCSS is rarely deleted since it’s hard to know what impact deleting it will have.
This exacerbates the above problems.
Styles and markup are not colocated.
This makes styling our applications harder than it needs to be. By adding matching class names in Javascript components and in SCSS definitions we:Invent names for things that already have a name or don’t need one:
<MyHeader className=”my-header” />
We create a ‘dual reusability’ problem (if someone knows a real term please teach me)
Both our react component is reusable and our css class name is reusable. We can reuse the class name in many components or in raw html with no way to verify these relationships.
We need a solution that…
Allows use to define styles for components without side effects.
Our component style definitions should guaranteeKeeps styling definitions close or connected to the components they apply to.
We’ve improved this slightly in Paragon by moving SCSS into component directories, but it’s not a perfect solution.Let’s us grow out of Bootstrap.
Out needs are growing beyond what Bootstrap offers. We need a solution that helps us scale our design system in a consistent way.
Proposal: Adopting CSS-in-JS
We have observed the power of mixing our javascript with our html – it’s time for CSS. By adopting CSS-in-JS we will address the above three needed solutions.
Example syntax of a Button.jsx
react component:
const Button = styled.button`
background: transparent;
border-radius: 3px;
border: 2px solid palevioletred;
color: palevioletred;
margin: 0 1em;
padding: 0.25em 1em;
`;
const App = () => (
<Button />
);
This technology will dynamically generate a class name scoped only to this component. This technology is Gatsby compatible.
Libraries I propose we adopt:
Styled-system (for a proven, consistent theming interface)
These libraries are well established and trusted: https://styled-components.com/showcase. See them in use on http://zillow.com or https://www.spotify.com/us/ . On Github there are 33.3k stars for styled-components, and 6.7k stars for styled-system.
Benefits
Theming can happen at run time.
MFEs could swap themes without being rebuilt (valuable for Open edX operators).
Dark mode or user preferred tweaks become achievable for edx.org
Component styles are easily scoped to individual components without naming things unnecessarily.
We could upgrade Paragon in existing code bases that use Bootstrap with less effort and risk.
Because styles are scoped to individual components and don’t rely on the global style sheet they become portable and effectively have no css side-effects.
We will significantly reduce the effort and risk to upgrade Paragon in edx-platform.
Contributing to Paragon or MFEs will be easier to do (as far as styling goes):
Javascript, HTML, and styling all live together eliminating the need to understand a broader SCSS context to contribute well.
Contributors to MFEs will no longer need to determine an appropriate place to put SCSS.
If MFE owners adopt this technology for custom components over SCSS (not required) they will see similar benefits: reducing the risk of multiple teams introducing conflicting styling changes.
It becomes easier to remove styling code over time.
Since components would couple HTML, CSS, and Javascript, when a component is removed there would be no need to track down any related SASS.
Challenges
We will have two apis for making minor spacing or color customizations: Bootstrap CSS utilities (today), and Styled System prop apis.
Bootstrap CSS utility class names are used heavily throughout our MFEs, removing it entirely in the near or medium term is unlikely.
During conversion of Paragon components over to CSS in JS some will have the styled system prop apis available while others will not
This could cause confusion about when the new technology is available vs not
Contributors to Paragon will need to learn a new method of styling their components
We would make SCSS no longer be acceptable to add with new components.
There will be at least one breaking change in Paragon
This shouldn’t be too big of a problem, but all MFEs will need to add a
<ParagonProvider theme={theme} />
components at the root of their React app and install astyled-components
peer dependency. Component level changes are likely non-breaking.
Level of effort
Initial prototype – 1 - 2 weeks
Add the two CSS-in-JS libraries to Paragon
Create a
ParagonProvider
component for injecting a themeCreate
theme.js
and an edx.org theme that match our bootstrap theme variablesAttempt the conversion of 3 - 6 components to CSS-in-JS and remove their SCSS dependencies.
Proposed decisions
- We will adopt a CSS in JS technology and incrementally replace our dependency on Bootstrap 4 in Paragon components (leaving css utils available)
- We will develop plan to leverage this technology incrementally (and with only one breaking change) and in a reversible way. We will also outline the expected end-state of the adoption.
- We will adopt styled-components and styled-system as our CSS-in-JS and theming implementations.
Updates after prototype
After prototyping out several different solutions this PR is the desired path forward [PAR-449] feat: add emotion by abutterworth · Pull Request #702 · openedx/paragon and brand updates feat: add theme js by abutterworth · Pull Request #5 · openedx/brand-openedx feat: add theme js by abutterworth · Pull Request #34 · edx/brand-edx.org .
Between the leading css-in-js implementations:
- Use @emotion/react instead of styled-components
styled-components offers only the “styled api”
const Button = styled.button`color: red;`
Whereas the emotion family of packages offers both that api and several others. The API offered by @emotion/react uses a css prop:
<div css={css`color: red;`} />
The benefit of this api is that in the future any theming or style utilities that replace Bootstrap css utility classes can apply uniformly to any react element, Paragon or not.
Regarding theming packages
Both styled-components and emotion support theming, but are essentially unopinionated in how themes are constructed or consumed.
The theming/utility packages that build on styled-components or emotion respectively: Styled System and Theme UI, reflect the apis of the core packages they rely upon. I believe that using styled-system or any package that uses only the “styled api” would result in an inconsistent API for replacing Bootstrap css utilities and would cause significant developer pain. A themable component using Styled System may look like this where utilities are component props:
Theme UI, which relies on emotion seems like a better alternative, it’s api can apply to any component, whether Paragon or not:
This is encourage, but it requires cautious inspection. After working with it a little bit I’ve determined that we should use some of the underlying helper functions that theme-ui packages discretely and avoid wholesale adoption. Why?
styled-system and theme-ui are created by the same people, they abandoned styled-system abruptly to work on theme-ui. I fear the same could happen with theme-ui.
Theme ui supplies its own themable components and appears to be a batteries included system.
There are lingering open issues in github regarding how components with multiple axes of variance should be handled (e.g. size, colorScheme, variant of a button). It looks like something may land and be merged in the next six months, but in the meantime we would need to build our own workarounds anyway.
Theme ui creates yet another interface for developers to learn. I think adopting and getting used to working with emotion first will help the team understand the tooling rather than mixing things immediately.
- Defer building the CSS-in-JS version of CSS utilities later and iterate slowly on theming helpers
Emotion supports theming out of the box, but it’s somewhat verbose. Helper functions can significantly reduce the verbosity, but risk being too opinionated. We should move slowly and cautiously on this front. We can take our first steps without making this theming system decision right now.
- Adopt the theme-specification shape for theme.js files
Many theme-able libraries that utilize CSS-in-JS use this theme specification: https://styled-system.com/theme-specification/ . It outlines a well formed shape for themes. Even if we don’t use theme-ui or styled-system, this is a proven structure that we can safely adopt.
[PAR-449] feat: add emotion by abutterworth · Pull Request #702 · openedx/paragon
feat: add theme js by abutterworth · Pull Request #34 · edx/brand-edx.org
feat: add theme js by abutterworth · Pull Request #5 · openedx/brand-openedx
feat: CSS-in-JS pt 2 by muselesscreator · Pull Request #712 · openedx/paragon
feat: add paragon theme support by muselesscreator · Pull Request #35 · edx/brand-edx.org
feat: add paragon theme support by muselesscreator · Pull Request #6 · openedx/brand-openedx
Go, no go decision:
Community Engineering team has veto power
Release a major version of Paragon (v.15.0.0) and minor version of brand-edx.org and brand-openedx
Initial adoption by MFEs – 2 - 4 weeks
@David Joy (Deactivated) What do you think?
Upgrade Paragon
Upgrade brand package
brand-edx.org
Add
ParagonProvider
to React app
Incremental conversion of components to CSS-in-JS – Estimate TBD after prototype.
SWAG right now 26 engineer weeks (10 complete per week x 2). Paragon currently exports a total of 129 components. Also see component usages on the doc site.
For each component.
Remove SCSS import in
index.scss
Add CSS-in-JS styles to component: Involves changes to the elements that are rendered. Example:
<div className=”card” />
becomesconst Card = styled.div
Leave the SCSS in the project with a comment that it is deprecated (maintain easy reversibility of the change)
Add any needed keys to the theme.js file.
Expected medium - long term outcomes
Bootstrap CSS utilities stay in our codebase forever.
Paragon continues to offer them as an export. New MFEs do not include the Paragon SASS.
Some engineers continue to use Bootstrap CSS utilities because they are used to them or they support a particular need.
Some deprecated components are never converted to CSS-in-JS.
They remain in the code base for over a year
We have avoided pushing teams off of deprecated components, in the past, we could continue this approach.
Adam B’s Belief – I believe adopting CSS in JS will significantly improve the frontend developer experience both in MFEs and in edx-platform. Components in our React applications will be more modular, and engineers more free to create unique visual exceptions to code without worrying how they will impact the system as a whole.