Joystream / atlas

Whitelabel consumer and publisher experience for Joystream
https://www.joystream.org
GNU General Public License v3.0
100 stars 44 forks source link

Switch to a CSS Variables based theme #34

Closed fulminmaxi closed 3 years ago

fulminmaxi commented 3 years ago

Currently our styling process is based on theme values set in some static files. This has been working fine for now, but I would like to discuss what might be an improved and more maintainable system. I propose we move our theme to use CSS variables, I hope that discussing and evaluating this proposal can help us improve our current theme regardless of if we end up adopting it.

What are CSS Variables? And how can they be useful to us?

From MDN:

Custom properties (sometimes referred to as CSS variables or cascading variables) are entities defined by CSS authors that contain specific values to be reused throughout a document.

In practical terms, if now we style a Button like this:

// colors.ts
{
// ... other theme values
blue: {
    500:  "#4038FF"
// Button.tsx
import {colors} from "path/to/theme"

const Button = styled.button`
    background-color: ${colors.blue[500]};
`

With CSS Variables we would instead do this:

/*  inside theme.css */
html {
  --color-blue-500: #4038FF;
/* other theme values */
}
// Button.tsx
const Button = styled.button`
  background-color: var(--color-blue-500);
`

CSS Variables have several small benefits, which I'm not going to go into since I don't think are relevant for us:

The main benefit I see for us, which makes the idea worth exploring is the possibility of defining how things should change relative to screen size in one single point of the codebase. Let me give an example:

Suppose we want the text of whole app to shrink or grow based on screen size, and we want every component to have this behaviour. How would we do that now? As far as I can see, with our current system we would have 2 options for achieving this: a. Look up each component that defines a font-size, add media queries or a fluidRange to each component to scale it accordingly. b. Add media queries to the top level component, change theme values based on the results and switch up the theme in the theme provider for that.

Option a is a nightmare to implement in terms of development experience, one might argue that if we wrap all the app text into a Text component than we have just one component to change, while true, I believe this is hard to enforce and a bad DX as well. For example you would have to do:

<Button><Text>Click</Text></Button>

instead of

<Button>Click</Button>

Option b is more easy to implement but requires swapping out the theme, which would make every styled component re-render.

With CSS Variables, it would look like this:

/* theme.css */
html {
/* just one example of fluid typography */
  --fontSize-normal: clamp(1rem, 2.5vw, 2rem);
}
// Button.tsx
const Button = styled.button`
font-size: var(--fontSize-normal);
`

I find the CSS var option to be superior by far to both options in terms on maintainability. One aspect that would be fun to explore is if this also helps keep animations in sync, since CSS variables are animatable, it might be possible for example to add a fade animation to all text at once, without having to change components.

Downsides and possible workarounds

For now, I only came up with two possible downsides to this approach.

  1. We lose type safety on theme values.
  2. CSS Variables are global state, so they have all the downsides of global state. For example if two different components were to change the same CSS variable, weird bugs could take place.

Regarding 1, a quick workaround would be to define the theme values in a static file and use that to initialise CSS Variables, then take use this file to define a typed getter function for CSS variables, going a bit more into details:

// GlobalStyles.tsx
import theme from "path/to/theme"

const globalStyles = css`
   html {
    --colors-blue-500: ${theme.colors.blue[500]};
   // ... other theme properties
}
`
// cvar.ts
import theme from "path/to/theme"
// could be an object too instead of a function
function cvar(key: themeKey){
  // have a map that associates each theme key to the corresponding CSS Variable name, for example colors.blue[500] maps to var(--colors-blue-500)
}

Then to use it in a component

import cvar from "path/to/cvar"
// Button.tsx
const button = styled.button`
// you get autocomplete here
  color: ${cvar('colorsBlue500')}

And type safety is restored.

For point 2. I don't think there is a workaround, but I can't think of a single use case where we would change a global CSS variable at runtime in multiple components. And if we needed to do so, we could redefine the variable in the components that need it to prevent global conflicts.

Feedback

I'm sure there are some downsides I might have overlooked or ways to improve this proposal, so any feedback is welcome :)

kdembler commented 3 years ago

Some info: https://www.joshwcomeau.com/css/css-variables-for-react-devs/

mikkio-j commented 3 years ago

CSS variables really offers some helpful features like handling sizes, but after some fiddling with it here are the things I discovered:

mikkio-j commented 3 years ago

Some more info in this topic (it's vanilla js examples though): https://christianheilmann.com/2021/02/08/sharing-data-between-css-and-javascript-using-custom-properties/ I'm beginning to lean towards that idea

kdembler commented 3 years ago

One idea about the issue with passing values via props: instead of doing that we could pass them directly vis CSS vars. The child would consume variables defined by the parent instead of using the prop

kdembler commented 3 years ago

Done! 🎉