Theming

Frontity Roadmap :biking_man:

Feature Card

Description

When themes get complex, users don’t want to copy the same color, font, margin… over and over again. Those settings should live in a theme settings configuration and must be easily accessible on the styled components.

  • These theme settings should be in the Frontity state.
  • Styled components should have access to all the state, not only these theme settings.
  • These styled components should subscribe to changes in the state, just like any other connected component.

User Stories

As a theme developer
I want to use some sort of theme settings
so that I can reuse them in my styled components

Examples

export default {
  state: {
    theme: {
      color: "red"
    }
  }
}

const Container = styled.div`
  color: ${({ state }) => state.theme.color};
`;

Or any other state property:

const Container = styled.div`
  color: ${({ state }) => state.router.link === "/" ? "red" : "green"};
`;

Possible solution

Integrate emotion-theming.

The problem with this approach is that the prop is hardcoded to theme. If we want to give easy access to all the state, the syntax is weird:

const Container = styled.div`
  color: ${({ theme }) => theme.state.router.link === "/" ? "red" : "green"};
`;

Also, it is not reactive to changes in state, as far as I know.

Allow the use of connect

We could use the same approach than with any other component: connect:

let Container = styled.div`
  color: ${({ state }) => state.router.link === "/" ? "red" : "green"};
`;
Container = connect(Container);

Right now it only works in the server. I’m not sure why. I guess it has something to do with styled components being forwarding refs, which I guess maybe work as normal components in the server.

Automatically connect styled

The same than the previous one, but done automatically when we re-export styled in the frontity package.

Workarounds

There are two ways to connect a styled component that work fine today.

Using the css prop

const Container = ({ state, children }) => (
  <div css={css`
    color: ${state.router.link === "/" ? "red" : "green"};
  `}>
    {children}
  </div>
);

export default connect(Container);

Using a connected parent

const Container = ({ state, children }) => (
  <StyledContainer link={state.router.link}>{children}</StyledContainer>
);

const StyledContainer = styled.div`
  color: ${({ link }) => link === "/" ? "red" : "green"};
`;

export default connect(Container);

So another possibility is to leave everything as it is today and simply promote one of these two solutions.

I’m currently styling components with the css function of emotion and I think it might be easier to use than styled and it would be a good idea to promote it’s use instead of styled like we are doing now.

1. We can avoid implementing a theming solution as the css will be defined within the React component and therefore all the state is available withing the styles:

const Component = ({ state }) => {
  const divStyles = css`
    background-color: ${state.colors.dark};
    visibility: ${state.menu.isVisible ? "visible" : "hidden"};
  `;

  return <div css={css} />
}

2. We can avoid forcing developers to struggle with naming if they choose to do so, as you can define the css prop directly in the jsx.

const Component = ({ state }) => (
  <div
    css={css`
      background-color: ${state.colors.dark};
      visibility: ${state.div.isVisible ? "visible" : "hidden"};
    `}
  />
);

3. You can style a React component the same way you can style any other element.

const InnerComponent = ({ className }) => (
  <div
    css={css`
      color: red;
    `}
    className={className}
  />
);

const Component = ({ state }) => (
  <div
    css={css`
      background-color: ${state.colors.dark};
      visibility: ${state.div.isVisible ? "visible" : "hidden"};
    `}
  >
    <InnerComponent
      css={css`
        color: blue;
      `}
    />
  </div>
);

4. As pointed by Luis, is the way to style things when using processors, as you can add styles to an element even if the element has been styled before.

const blueAnchors = {
  test: ({ component }) => component === "a",
  process: node => {
    node.props.css = css`
      ${node.props.css}
      color: blue;
    `;
  }
}

My first concern with that approach is that this css component

const Component = ({ children }) => {
  const divStyles = css`
    background-color: ${state.colors.dark};
    visibility: ${state.menu.isVisible ? "visible" : "hidden"};
  `;

  return (
     <div css={divStyles}>
       {children}
     </div>
   );
}

looks unnecessarily more complex than this styled component equivalent:

const Component = styled.div`
  background-color: ${state.colors.dark};
  visibility: ${state.menu.isVisible ? "visible" : "hidden"};
`

for the exact same behavior.

My second concern is that styled components are a big trend in React. Recommending not to use them it’s going to be difficult.

But I agree with you that the css prop could also help to solve this problem. I’m going to update the OP with these two possibilities which work fine today:

const Container = ({ state, children }) => (
  <div css={css`
    color: ${state.router.link === "/" ? "red" : "green"};
  `}>
    {children}
  </div>
);

export default connect(Container);
const Container = ({ state, children }) => (
  <StyledContainer link={state.router.link}>{children}</StyledContainer>
);

const StyledContainer = styled.div`
  color: ${({ link }) => link === "/" ? "red" : "green"};
`;

export default connect(Container);

I understand your first concern, but in my experience that’s not a case you find often, you will usually have a bunch of styled components listed under the React component just to be called from it, and the only case that currently comes to my mind that you might want to just render a single styled component would be to change styles inside a processor, exactly where it can’t be used.

On the other side, it’s more intuitive to use css for those that are not experienced with React or styled-components in my opinion.

Hmm… you’re right, my first example wasn’t fair because you can use css directly in your non-styled components.

This is a more fair example:

const div = state => css`
  background-color: ${state.colors.dark};
  visibility: ${state.menu.isVisible ? "visible" : "hidden"};
`;

const blue = css`
  color: "blue";
`;

const bold = css`
  font-weight: "bold";
`;

const MainComponent = ({ state }) => (
  <div css={div(state)}>
    <a css={blue} href="...">
      Some link here
   </a>
   <span css={bold}>
      More stuff
   </span>
 </div>
);

versus

const StyledDiv = styled.div`
  background-color: ${({ dark }) => dark};
  visibility: ${({ isVisible }) => isVisible ? "visible" : "hidden"};
`;

const BlueAnchor = styled.a`
  color: "blue";
`;

const BoldSpan = styled.span`
  font-weight: "bold";
`;

const MainComponent = ({ state }) => (
  <StyledDiv dark={state.colors.dark} isVisible={state.menu.isVisible}>
    <BlueAnchor href="...">
      Some link here
   </BlueAnchor>
   <BoldSpan>
      More stuff
   </BoldSpan>
 </StyledDiv>
);

Which is basically the same… Actually, state is easier to pass down and composition seems much easier because it can be done with arrays, and you get to choose the order:

const danger = css`
  color: red;
`

const base = css`
  background-color: darkgreen;
  color: turquoise;
`

render(
  <div>
    <div css={base}>This will be turquoise</div>
    <div css={[danger, base]}>
      This will be also be turquoise since the base styles
      overwrite the danger styles.
    </div>
    <div css={[base, danger]}>This will be red</div>
  </div>
)
1 Like

Looking at my last examples… it certainly does.

I don’t like to define the styles with css outside of the component to avoid mapping the state, and in that case, compared with styled I don’t appreciate much difference.

The composition with arrays is very nice, didn’t know about it.

Also, I think it helps readability when you look at the jsx code and see what elements you are rendering without scrolling down and checking what are you using as a styled component.

Avoid mapping the state is fine, but otherwise I think it’s cleaner and probably slightly more performant to have them defined outside the component.

What I really like is the different approach on composability. It lets you think in terms of style concepts instead of whole component/tags:

const Comp = () => (
  <>
    <span css={blue}>This is a blue span</span>
    <span css={bold}>This is a bold span</span>
    <span css={[blue, bold]}>This is a blue bold span</span>
  </>
);

const blue = css`
  color: "blue";
`;

const bold = css`
  font-weitght: "bold";
`;

I also plays nice with extensions like styled systems:

import { space, layout, color } from "styled-system";

const Comp = () => (
  <>
    <div css={[space, layout} width={1/2} fontSize={4} />
    <div css={[layout, color} m={2} color='gray.0' />
  </>
);

versus

import { space, layout, color } from "styled-system";

const Box = styled.div`
  ${space}
  ${layout}
  ${color}
`;


const Comp = () => (
  <>
    <Box width={1/2} fontSize={4} />
    <Box m={2} color='gray.0' />
  </>
);

But we still need to figure out how to include add a theme that all packages can modify.

Even though I really like @orballo’s approach with the css prop, I still think we should add a way to connect a styled component.

I was taking a look at the observer implementation of mobx-react and I saw that they support forwardRef on their observer (our connect) so that’s something we can explore if we ever want to add something like:

let Container = styled.div`
  color: ${({ state }) => state.router.link === "/" ? "red" : "green"};
`;
Container = connect(Container);

@orballo the css prop is great, it makes everything easier :laughing: : Styling the HTML content with the `css` prop

1 Like

Now that we are collaborating with RisingStack and Miklos Bertalan with react-easy-state, I’ve brought this issue under their attention:

To be taken into account
It seems the mere installation of emotion-theming in a package provokes an error

  1. Clone the branch before-emotion-theminghttps://github.com/frontity-community/texts-app/tree/before-emotion-theming
  2. cd packages/cristina-theme
  3. npm i -S emotion-theming
  4. cd ../.. && npx frontity dev

I get this…

 TypeError: Cannot read property 'add' of undefined
      at e.init (webpack-internal:///./packages/cristina-theme/node_modules/react-helmet-async/lib/index.module.js:14:9234)
      at e.render (webpack-internal:///./packages/cristina-theme/node_modules/react-helmet-async/lib/index.module.js:14:9307)
      at processChild (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:402:631)
      at resolve (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:396:122)
      at ReactDOMServerRenderer.render (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:433:1199)
      at ReactDOMServerRenderer.read (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:433:55)
      at renderToString (webpack-internal:///./node_modules/react-dom/cjs/react-dom-server.node.development.js:470:116)
      at app.use (webpack-internal:///./node_modules/@frontity/core/src/server/index.tsx:53:243)
      at process._tickCallback (internal/process/next_tick.js:68:7)