Skip to content

Instantly share code, notes, and snippets.

@xcoderzach
Last active March 21, 2018 20:53
Show Gist options
  • Star 2 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save xcoderzach/0df5724d416be78a824c16e6ae9b42cc to your computer and use it in GitHub Desktop.
Save xcoderzach/0df5724d416be78a824c16e6ae9b42cc to your computer and use it in GitHub Desktop.

Alternative Method for Component Library Theming

I really like the idea of styled components as the lowest level visual primitive, so theme-ing via passing around color strings and pixel values (or worse, 😨 css snippets 😨) makes me sad 😢.

Here's an alternative. Instead of passing in theme variables, which requires the library author to explicitly allow certain css properties to be overridden, we pass styled components as the theme😃. Now, styled components are the lowest level visual primitive that a user works with. Plus, it allows for much much more powerful extension. A user can decorate, wrap, replace, or extend a library's styled components to their ❤️'s content.

Not only that, but because they're just components, the library user isn't forced to use styled-components (even if they should 😉️). They could just use their own components with scoped styles, using radium, aprodite, css modules, or anything else.

Another potential benefit would be the ability to maintain a "semantic component" that describes the what the component does and how it does it, and either inject react-native, or html/css styled-components.

In the Library

In the library itself, instead of importing the styled components, we pull them in from the theme.

const Axis ({
  orient,
  scale,
  tickValues,
  tickFormat,
  theme: {
    myChartLib: {
      AxisContainer, DomainLine, TickContainer, TickLine, TickLabel
    }
  }
}) => {
  // ... Pretend that there's a bunch of chart axis related math here!
  // Notice how clean and semantic this component is 👌👇
  <AxisContainer>
    <DomainLine d={domainPath} />
    {tickValues.map((tick, i) =>
      <TickContainer style={{ transform: transform(position, tick) }}>
        <TickLine
          x2={tickSizeInner}
          y1={0.5}
          y2={0.5}
        />
        <TickLabel
          x={spacing}
          y={0.5}
        >{format(tick)}</TickLabel>
      </TickContainer>
    )}
  </AxisContainer>
}

export default withTheme(Axis)

The Library's Theme Provider

The library should export a theme provider that will pass the theme's default styled components so they can be decorated. This gives the library author the ability to control which components they allow the user to override. But it also gives the user the power to wrap, extend, or replace the component however they see fit.

const defaultTheme = {
  AxisContainer,
  DomainLine,
  TickContainer,
  TickLine,
  TickLabel
}

// ⚠️💀 This probably requires more thought if you want to handle multiple levels of theme
// providers.
const ComponentLibThemeProvider = (props) => {
  const userTheme = (typeof theme === 'function') ? props.theme(defaultTheme) : props.theme
  // 👆 If a library consumer passes a function as their theme,
  // give them the default theme as an argument so they can 🎨decorate🎨.
  const theme = {
    myChartLib: Object.assign({}, defaultTheme, userTheme)
  }
  return (
    <ThemeProvider theme={theme}>
      {props.children}
    </ThemeProvider>
  )
}

Userland Theming

Now, the library user can replace or extend these components, using the usual styled components methods. It could also potentially be used internally by the library author, to do things like modifying a Button that appears inside of a ButtonGroup, etc. (alongside, or as an alternative to #227)

const myTheme = ({ TickLabel }) => ({
  // I want the axis labels on my component to be cream colored and slanted
  TickLabel: TickLabel`
    color: cream;
    transform: rotate(45deg) translate(-10px, 10px);
  `,

  // I'm going to completely override the implementation of the TickLine, with my own
  // line
  TickLine: styled.line`
    fill: aquamarine;
  `
})

// Now, any axis inside FancyLineChart axis will use these components 👆.
const MyFancyLineChart = (props) => (
  <LineChart>
    <ChartThemeProvider theme={myTheme}>
      {props.children}
    </ChartThemeProvider>
  </LineChart>
)

###Downsides

  • Library authors have to be sure not to expose too many components, as changes to the structure of the component will very often cause breakages. This is very similar to the problem of making "public" class names for users to override, changes to the markup will most likely cause user styles to break.
  • Props passed to injected theme components become public apis that must be documented and maintained.
  • If used as the sole method of themeing, every small changes to borders, colors, etc, would require a user to override all library components. I think a hybrid approach of theme variables for basic things, with the option to override/extend components would probably be best here.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment