question-mark
Stuck on an issue?

Lightrun Answers was designed to reduce the constant googling that comes with debugging 3rd party libraries. It collects links to all the places you might be looking at while hunting down a tough bug.

And, if you’re still stuck at the end, we’re happy to hop on a call to see how we can help out.

Reversing the relationship in theming

See original GitHub issue

Heyo,

Right now the recommended pattern across styled-components and theming (including the packages that consume it like emotion) is to pass specific styles down from the root of your app:

const Button = styled.button`
  color: ${props => props.theme.main};
`;

const theme = {
  main: 'mediumseagreen',
};

<ThemeProvider theme={theme}>
  <Button>Themed</Button>
</ThemeProvider>

This works for certain types of problems. But when you’re writing a full theme for your app, you’re touching hundreds or even thousands of styles. Structuring it this way becomes painful.

You quickly start having this separate set of styles that lives at the top of your app that is separate from your components:

const themeLight = {
  buttonDefaultColor: "#123456",
  buttonDefaultHoverColor: "#123456",
  buttonDefaultActiveColor: "#123456",
  buttonDefaultFocusColor: "#123456",
  buttonPrimaryColor: "#123456",
  buttonPrimaryHoverColor: "#123456",
  buttonPrimaryActiveColor: "#123456",
  buttonPrimaryFocusColor: "#123456",
  // ...
};

const themeDark = {
  buttonDefaultColor: "#123456",
  buttonDefaultHoverColor: "#123456",
  buttonDefaultActiveColor: "#123456",
  // ...
};

And your components start filling up with lookup functions and lose their scan-ability.

const Button = styled.button`
  color: ${props => props.theme.buttonDefaultColor};
  background-color: ${props => props.theme.buttonDefaultBackgroundColor};
  border-color: ${props => props.theme.buttonDefaultBorderColor};

  &:hover {
    color: ${props => props.theme.buttonDefaultHoverColor};
    background-color: ${props => props.theme.buttonDefaultHoverBackgroundColor};
    border-color: ${props => props.theme.buttonDefaultHoverBorderColor};
  }

  &:active {
    color: ${props => props.theme.buttonDefaultActiveColor};
    background-color: ${props => props.theme.buttonDefaultActiveBackgroundColor};
    border-color: ${props => props.theme.buttonDefaultActiveBorderColor};
  }

  &:focus {
    color: ${props => props.theme.buttonDefaultFocusColor};
    background-color: ${props => props.theme.buttonDefaultFocusBackgroundColor};
    border-color: ${props => props.theme.buttonDefaultFocusBorderColor};
  }
`;

Then you start getting into component variants where you are depending on other properties and every one of these functions gets more complicated:

const Button = styled.button`
  color: ${props => {
    if (props.kind === "default") return props.theme.buttonDefaultColor;
    if (props.kind === "primary") return props.theme.buttonPrimaryColor;
    if (props.kind === "success") return props.theme.buttonSuccessColor;
    if (props.kind === "warning") return props.theme.buttonWarningColor;
    if (props.kind === "danger") return props.theme.buttonDangerColor;
  }};
  // ...
`;

I’ve seen people try to abstract all of this work and accidentally introduce performance problems. (When you’re making calculations for thousands of styles across hundreds of components on every render cycle, it’s hard not to have a performance problem)

It also is tough to try and abstract the giant object of styles at the root of your app. Trying to abstract a design system from the code side is a bit of a recipe for disaster long term.

Reversing the relationship

Colocation is a huge feature of CSS-in-JS, it leads to a whole lot of wins (which anyone using css-in-js already knows so I won’t get into it here). The problems I’ve described with theming are because we’ve given up the colocation of styles and put them in two places.

If we made a couple minor changes to our theming apis, and started documenting/teaching it differently we could set developers up for success.

Let me give an example:

Instead of passing specific styles from the root of the app. We could instead pass down descriptors like:

<ThemeProvider theme={{ mode: "dark" }}>

Then from the component side it initially looks like this:

const buttonColor = {
  default: { light: "#123456", dark: "#123456" },
  primary: { light: "#123456", dark: "#123456" },
  success: { light: "#123456", dark: "#123456" },
  warning: { light: "#123456", dark: "#123456" },
  danger: { light: "#123456", dark: "#123456" },   
};

const Button = styled.button`
  color: ${props => buttonColor[props.kind][props.theme]};
  // ...
`;

Writing it out like this colocates your themes with your styles with your component, and we’re back to the awesome workflow of CSS-in-JS.

API Design

I’ve designed a tiny little API to encourage this pattern called styled-theming. It basically just abstracts this:

const fontSize = {
  small: '1em',
  large: '1.2em',
};

const buttonColor = {
  default: { light: "#123456", dark: "#123456" },
  // ...
};

const Button = styled.button`
  font-size: ${props => fontSize[props.theme.size]};
  color: ${props => buttonColor[props.kind][props.theme.mode]};
  // ...
`;

Into this:

const fontSize = theme('size', {
  small: '1em',
  large: '1.2em',
});

const buttonColor = theme.variants('mode', 'kind', {
  default: { light: "#123456", dark: "#123456" },
  // ...
});

const Button = styled.button`
  font-size: ${fontSize};
  color: ${buttonColor};
  // ...
`;

We’ve started using this API on a project and it has simplified a lot of work being done.


Encouraging this pattern

We should, as a community, encourage developers to work in a way that sets them up for success. We can do this via api design, documentation, teaching, etc.

I would like to see tools include a very lightweight api for encouraging this pattern, it could be the one I created with styled-theming, but also does not have to be.

Further, I would like to see tools like styled-components to start documenting theming this way. I’m happy to write up new docs for the styled-components site and the theming library. But wanted to put this issue out there first.

Thanks for reading

cc @iamstarkov

Issue Analytics

  • State:closed
  • Created 6 years ago
  • Reactions:15
  • Comments:27 (20 by maintainers)

github_iconTop GitHub Comments

2reactions
FabioAntunescommented, Aug 24, 2017

Allow me to disagree, a theme should supply variables/units to be used across the components, not styles. I think the other way around it’s way more unmanageable, you will always need to have some reference on your styles to the base unit you are using, either be it the colour or the font-size. You will need to reference that value across multiple components either importing a variable or using a theme

import { ThemeProvider } from 'styled-components';

const theme = {
  fontSize: '1rem',
  blue: 'blue',
  red: 'red',
  get primary() {
    return this.blue;
  },
  get danger() {
    return this.red;
  }
}

const App = () => (
  <ThemeProvider theme={theme}>
  <div>
   ...
  </div>
  </ThemeProvider>
);

export default App;

I don’t see how this is unmantainable comparing to having this, where you have the colour hardcoded:

const fontSize = theme('size', {
  small: '1em',
  large: '1.2em',
});

const buttonColor = theme.variants('mode', 'kind', {
  default: { light: "#123456", dark: "#123456" },
  // ...
});

const Button = styled.button`
  font-size: ${fontSize};
  color: ${buttonColor};
  // ...
`;

eventually you will end up doing this:

+ import { dark, light } from '../../../myGlobalCssVars';
const fontSize = theme('size', {
  small: '1em',
  large: '1.2em',
});

const buttonColor = theme.variants('mode', 'kind', {
-  default: { light: "#123456", dark: "#123456" },
+  default: { light: "${light}", dark: "${dark}" },
  // ...
});

const Button = styled.button`
  font-size: ${fontSize};
  color: ${buttonColor};
  // ...
`;

if I want to use a different value for the light color I will need to be passing those values over and over again for every single component.

Although the idea is to build independent components you always need to have some global vars to be shared across your components, like font-size, line-height. You need those things to be consistent across your entire application.

2reactions
geelencommented, Aug 11, 2017

This is excellent work. I’ve always considered both the props and theming APIs as kind of a minimum viable approach that anyone can build their own interfaces on top of, once we had a bit more real-world usage.

@mxstbr do you also want to migrate the API of styled-theming into styled-components or move the styled-theming repo into the styled-components org?

I would prefer to keep it as a separate package, but under the organisation, and fully integrated into the documentation. That way, SC itself keeps the low level API (if your interpolation returns a function, we’ll call it) and, for anyone who looks closely, it’s clear how you can build up your own designs from it.

My only question is similar to @nelix’s—it’s not clear to me how I might provide a custom theme. In fact, it feels like more of a way to generate variations of a component rather than theme them. (I guess that’s kinda the issue title now I think about it). I think, if the API could be extended somehow to allow outside-in theming it might be enough to cover all use-cases.

Perhaps that can be done by treating 'mode' as special (or some other word). Then, you could set a top-level theme variable that mode="custom" or something, which then caused each component to look outside of its immediate definitions to something somewhere else that’s also provided by the inherited theme. In the case of a complete theming override, I think it’s fair to assume that the consumer will have gone into the source of the component and looked at its internal structure, so you could pass an object that has the exact same structure as fontSize and buttonColor, there just needs to be a way to wire it up.

If nothing else, though, I really like the way this approach makes it easier for components to not hard-code so much of their CSS. I think the easier that is, the more people will use it, and the more shareable and reusable everyone’s components will be by default.

Read more comments on GitHub >

github_iconTop Results From Across the Web

Reversing a relationship spiral: From vicious to virtuous cycles ...
However, before reaching this conclusion, there may exist possible mechanisms to reverse such a vicious relationship spiral. In fact, ...
Read more >
5 signs of reverse polarity - Living Love
5 signs of reverse polarity in a relationship or dating are: 1. Constantly feeling frustrated and resentful towards your partner or dating ......
Read more >
Reversing relationships direction - No Magic Documentation
The direction of the particular relationship can be reversed. You can reverse the direction of the transition, control flow, object flow, ...
Read more >
Reverse relationships broken after update. [#3219461] - Drupal
After the upgrade i have all the Views using a reverse relationship to a paragraph field are broken. The relationship states this when ......
Read more >
Why Your Savior Complex Is Toxic to Your Relationship
This is a theme that is reinforced by media time and time again. ... Having a reverse savior complex can also lead to...
Read more >

github_iconTop Related Medium Post

No results found

github_iconTop Related StackOverflow Question

No results found

github_iconTroubleshoot Live Code

Lightrun enables developers to add logs, metrics and snapshots to live code - no restarts or redeploys required.
Start Free

github_iconTop Related Reddit Thread

No results found

github_iconTop Related Hackernoon Post

No results found

github_iconTop Related Tweet

No results found

github_iconTop Related Dev.to Post

No results found

github_iconTop Related Hashnode Post

No results found