How to properly abstract/alias custom theme settings?

The theme I’m working on can be found here.

Inspired by the great work of @Segun on their Chakra UI Frontity Theme, I decided that I wanted to better abstract aspects of my theme to variables set in it’s settings.

So in src/index.js there is the theme JS object:

import Theme from "./components";
import image from "@frontity/html2react/processors/image";

const desertJackalope = {
  name: "desert-jackalope",
  roots: {
    // In Frontity, any package can add React components to the site.
    // We use roots for that, scoped to the "theme" namespace.
    theme: Theme
  },
  state: {
    // State is where the packages store their default settings and other
    // relevant state. It is scoped to the "theme" namespace.
    theme: {
      isBlog: false,
      colors: {
        primary: {
          default: "#2657eb",
          heavy: "#1f38c5"
        }
      }
    }
  },
  // Actions are functions that modify the state or deal with other parts of
  // Frontity like libraries.
  actions: {
    theme: {}
  },
  libraries: {
    html2react: {
      // Add a processor to html2react so it processes the <img> tags
      // inside the content HTML. You can add your own processors too.
      processors: [image]
    }
  }
};

export default desertJackalope;

As you can see I have added a couple of key-value pairs to the theme object. isBlog determines whether or not the website is set to “blog” mode or if it’s set to “case study mode”. I also set some default theme colors.

isBlog works fine, and as intended which you can see an example of it’s use in src/components/archive/archive.js:

import React from "react";
import { connect } from "frontity";
import BlogArchive from "./BlogArchive";
import CaseStudyArchive from "./CaseStudyArchive";

const Archive = ({ state, data }) => {
  // check whether or not blog or case study presentation

  return (
    <>
      {state.theme.isBlog ? (
        <BlogArchive data={data} />
      ) : (
        <CaseStudyArchive data={data} />
      )}
    </>
  );
};

export default connect(Archive);

However when I try to add the colors to my global css in src/components/index.js, I get a result of undefined. This is how the code currently looks. You can see my commented out attempts at directly importing desertJackalope (the name of the JS object in my src/index.js file) and my attempt at setting the value of desertJackalope.theme.colors.primary.default to the const primaryColor.

import React from "react";
import { Global, css, connect, styled } from "frontity";
import Header from "./Header";
import Archive from "./Archive";
import Post from "./Post";
import Page404 from "./Page404";
import Page from "./Page";
import Loading from "./Loading";
import { useTransition, animated } from "react-spring";
import Meta from "./Meta";
import { colors } from "../theme";
import Footer from "./Footer";
//import desertJackalope from "../index";
//const primaryColor = desertJackalope.theme.colors.primary.default;

const Theme = ({ state }) => {
  const transitions = useTransition(state.router.link, link => link, {
    from: { opacity: 0 },
    enter: { opacity: 1 },
    leave: { opacity: 0 }
  });

  return (
    <>
      <Meta />
      <Global styles={globalStyles} />
      <Header />

      {transitions.map(({ item, props, key }) => {
        const data = state.source.get(item);
        return (
          <animated.div key={key} style={props}>
            <Absolute>
              {(data.isFetching && <Loading />) ||
                (data.isArchive && <Archive data={data} />) ||
                (data.isPage && <Page data={data} />) ||
                (data.isPostType && <Post data={data} />) ||
                (data.is404 && <Page404 />)}
            </Absolute>
          </animated.div>
        );
      })}
      {/* <Footer /> */}
    </>
  );
};

export default connect(Theme);

//- GLOBAL STYLES CSS

//- Color vars
const primaryColor = colors.primary.default;
const heavyprimaryColor = colors.primary.heavy;
const accentColor = colors.accent;
const darkColor = colors.dark[100];
const darkColor90 = colors.dark[90];
const darkColor80 = colors.dark[80];
const darkColor30 = colors.dark[30];

// set global styles
const globalStyles = css`
  @import url("https://fonts.googleapis.com/css?family=Space+Mono:400,400i,700,700i&display=swap");
  :root {
    --primary-heavy: ${heavyprimaryColor};
    --primary: ${primaryColor};
    --snappy: cubic-bezier(0.075, 0.82, 0.165, 1);
    --heavy-snap: cubic-bezier(0.6, -0.28, 0.735, 0.045);
    --accent: ${accentColor};
    --dark: ${darkColor};
    --dark90: ${darkColor90};
    --dark80: ${darkColor80};
    --dark30: ${darkColor30};
    *::selection {
      background: var(--primary);
      color: white;
    }
  }
  body {
    margin: 0;
    font-family: "Space Mono", "Segoe UI", Roboto, "Droid Sans",
      "Helvetica Neue", Helvetica, Arial, sans-serif;
    box-sizing: border-box;
  }
  a,
  a:visited {
    color: inherit;
    text-decoration: none;
  }
`;

//- Page Transition stuff

const Absolute = styled.div`
  position: absolute;
  width: 100%;
  display: flex;
  flex-direction: column;
  align-items: center;
`;

Directly referencing the desertJackalope.state.theme.colors.primary.default value does not work. It does work when inserted into a component, because all components created inherit state, but trying to set desertJackalope.state.theme.colors.primary.default to const primaryColor and then setting the css variable in global state root to ${primaryColor} results in a react error saying that “primaryColor is undefined”.

If instead I create a JS object in src/theme.js called “colors” and I set it such that colors.primary.default is my default color, and then import the colors object to src/index.js and set const primaryColor to colors.primary.default, I can then set the css global root variable to ${primaryColor} then it works no problem… and I’m not clear why.

Setting all my theme color settings in src/theme.js isn’t too bad but it does mean that I can’t have the option of setting default color settings in my src/index.js file with the option of being overridden by the frontity.settings.js file (as I can do with the isBlog value very easily).

Segun sets his colors in his src/index.js file but he passes all colors to his components via prop drilling. So it works because those components inherit state. But for some reason when I try passing the contents of desertJackalope.state.theme etc to a const and pass that into CSS variables it does not work.

I find prop drilling messy, and I would prefer to abstract/alias my theme colors via my custom src/theme.js file than have to muck up my component markup with a bunch of color variables.

I assume this problem is caused by how frontity is handling the src/index.js object. Clearly the object gets some kind of interaction as the default state set in that file can be overridden by frontity.settings.js but I’m not clear how, and if however it is doing it is preventing me from aliasing and including my theme colors the way that I want to.

Does this make sense?

Hey @thedonquixotic :wave:

Okay, I think you touched upon a couple of problems and I ll try to unpack them all.

1. importing the theme

I notice that in your example, you had a commented out line:

//const primaryColor = desertJackalope.theme.colors.primary.default;

as you have correctly indicated later in your post, you are missing the state, so the whole line should be:

const primaryColor = desertJackalope.state.theme.colors.primary.default;

With that fixed, you should be able to use import the index.js and use the values defined there.

CSS custom properties.

All that being said, what is your use case for CSS custom properties?

The variables that you define in state.theme in the src/index.js file can be accessed in any component as long as it’s wrapped with connect(). So, this essentially serves the same function as CSS Custom Properties.

In fact, you have already used that functionality by using the isBlog property in your src/components/Archive/archive.js

As far as I can tell, you don’t have to do any prop drilling because as long as you wrap your component with connect(), your theme variables should be available in that component in state.theme. :slight_smile:

Addiionally, not that there are some caveats with using <Global/>, most notably that Frontity is not able to optimise the CSS that is provided in it. More info here

Hope this helps!

1 Like

Thank you for the excellent response!

Correcint it to desertJackalope.state.theme.colors.primary.default; doesn’t fix the issue unfortunately.

Here’s what I added:

import desertJackalope from "../index";
const primeColor = desertJackalope.state.theme.colors.primary.default;

(it says primeColor so that I could just swap out variable name without having to unimport the other variables)

here’s the output I get:

from console:

SERVER STARTED -- Listening @ http://localhost:3000
  - mode: development
  - target: module


webpack built client 4ab813001e2039c4a137 in 8346ms
ℹ 「wdm」: Child client:
                     Asset      Size        Chunks             Chunk Names
         archive.module.js  64.9 KiB       archive  [emitted]  archive    
    server-front.module.js  5.75 MiB  server-front  [emitted]  server-front
     + 1 hidden asset
Child server:
        Asset      Size  Chunks             Chunk Names
    server.js  8.43 MiB    main  [emitted]  main
ℹ 「wdm」: Compiled successfully.
TypeError: Cannot read property 'state' of undefined
    at eval (webpack-internal:///./packages/desert-jackalope/src/components/index.js:19:343)
    at Module../packages/desert-jackalope/src/components/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5396:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./packages/desert-jackalope/src/index.js:2:69)
    at Module../packages/desert-jackalope/src/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5540:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./build/bundling/entry-points/server.ts:3:84)
    at Module../build/bundling/entry-points/server.ts (/mnt/c/Users/aslan/home/work/server-front/build/server.js:139:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at /mnt/c/Users/aslan/home/work/server-front/build/server.js:104:18
TypeError: Cannot read property 'state' of undefined
    at eval (webpack-internal:///./packages/desert-jackalope/src/components/index.js:19:343)
    at Module../packages/desert-jackalope/src/components/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5396:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./packages/desert-jackalope/src/index.js:2:69)
    at Module../packages/desert-jackalope/src/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5540:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./build/bundling/entry-points/server.ts:3:84)
    at Module../build/bundling/entry-points/server.ts (/mnt/c/Users/aslan/home/work/server-front/build/server.js:139:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at /mnt/c/Users/aslan/home/work/server-front/build/server.js:104:18

from browser response

TypeError: Cannot read property 'state' of undefined
    at eval (webpack-internal:///./packages/desert-jackalope/src/components/index.js:19:343)
    at Module../packages/desert-jackalope/src/components/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5396:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./packages/desert-jackalope/src/index.js:2:69)
    at Module../packages/desert-jackalope/src/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:5540:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./build/bundling/entry-points/server.ts:3:84)
    at Module../build/bundling/entry-points/server.ts (/mnt/c/Users/aslan/home/work/server-front/build/server.js:139:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at /mnt/c/Users/aslan/home/work/server-front/build/server.js:104:18

CSS Custom Properties

My reason for using CSS custom properties is informed by a philosophy of relying on the native browser implementations as much as possible.

Global

Yeah I’m aware of those issues. I’ve tried to keep the global CSS as small as possible. It basically holds just the root variables.

I see. It seems like the issue you are having has to do with an incorrect import path.

However, I would strongly recommend not to import this package directly. The main reason is that the data will not be reactive:

Normally, if you update the value of colors.primary in some component, frontity will re-render all the components that use that variable if you pass it down from state.theme. If you just import it directly, frontity has no knowledge of that and won’t rerender.

I see. I would not worry about the particular features being “native” or not. Using the variables defined in JS gives you the same benefits. Passing them again into CSS Variables is just extra work. There is no significant difference in performance and CSS variables won’t work in IE11 and below!

So, in your case, you should just wrap your components with connect() and if you want to pass state to your CSS you can do one of:

1. Create a function that gets state and returns the CSS:

const MyComp = ({ state }) => (
  <>
    <Global styles={globalStyles(state)} />
    <div css={someStyle(state)}>...</div>
  </>
);

const globalStyles = state => css`
  body {
    color: ${state.theme.colors.primary};
  }
`;

const someStyle = state => css`
  color: ${state.theme.colors.primary};
`;

2. Define the CSS inside the component:

const MyComp = ({ state }) => {
  const globalStyles = css`
    body {
      color: ${state.theme.colors.primary};
    }
  `;
  
  const someStyle = css`
    color: ${state.theme.colors.primary};
  `;

  return (
    <>
      <Global styles={globalStyles} />
      <div css={someStyle}>...</div>
    </>
  );
};

3. Pass down props to styled components (not possible with Global):

const MyComp = ({ state }) => (
  <>
    <Global styles={globalStyles(state)} />
    <Div primary={state.theme.colors.primary}>...</Div>
  </>
);

const globalStyles = state => css`
  body {
    color: ${state.theme.colors.primary};
  }
`;

const Div = styled.div`
  color: ${props => props.primary};
`;

Sorry, I don’t think so. I’ve double checked my import path and it is correct.

Also I don’t need the data to be reactive. It’s intended to be static variables used to define the visual theming.

Are you saying though that if the server settings in frontity.settings.js file changes those variables that they won’t change?

I’m not sure what you are referring to exactly, but I think that this is not what I meant. I was only referring to the re-rendering of components on the client.

The solution that I suggested is the preferred way to accomplish what you are trying to do even if you “don’t need it to be reactive” right now, because:

  1. you might want some part of to be reactive in the future.
  2. this way you don’t have to import your variables in every file that uses them, just use the state prop
  3. you are already using the same mechanism for non-style related settings and actions, so why use a different one just for styles? :upside_down_face:

Hope this helps!

What I was referring to was this:
Settings which are established or changed in src/index.js mirror settings that can be written in frontity.settings.js. src/index.js establishes default values that then can be overwritten on a per server basis via frontity.settings.js.

So for instance, by default my theme has set desertJackalope.state.theme.isBlog to “false”, but if I install my theme on a server and in frontity.settings.js set state.theme.isBlog to true then those settings will override it.

This isn’t the point though. Maybe I can do things one way or another but whether or not I’m using custom CSS properties isn’t the issue. The issue is that I’ am not being able to access the state from outside of a component. I can access isBlog blog from inside the component but if I want to access a key-value held in state as a variable that I use in my CSS styling I can’t. The way that @Segun accesses Chakra UI theme’s key-values for colors is through the use of prop drilling which I’d like to avoid.

So what I’m trying to figure out is how can I set color values in my src/index.js file and access those key-values from outside of components that are directly inheriting state?

Let me give you some simplified examples I’ve tried and their results:

Example 1 (export variable from src/index.js file)

src/index.js

import Theme from "./components";
import image from "@frontity/html2react/processors/image";

const desertJackalope = {
  name: "desert-jackalope",
  roots: {
    // In Frontity, any package can add React components to the site.
    // We use roots for that, scoped to the "theme" namespace.
    theme: Theme
  },
  state: {
    // State is where the packages store their default settings and other
    // relevant state. It is scoped to the "theme" namespace.
    theme: {
      isBlog: false,
      colors: {
        primary: {
          default: "#2657eb",
          heavy: "#1f38c5"
        }
      },
      newsletterURL:
        "https://the-jackalope.us8.list-manage.com/subscribe/post?u=7fc8ae244460f6dd1c74dd7bf&amp;id=74ff6c880b",
      footerlinks: {
        github: "https://github.com/jcklpe",
        blog: "https://www.jackalope.tech"
      }
    }
  },
  // Actions are functions that modify the state or deal with other parts of
  // Frontity like libraries.
  actions: {
    theme: {}
  },
  libraries: {
    html2react: {
      // Add a processor to html2react so it processes the <img> tags
      // inside the content HTML. You can add your own processors too.
      processors: [image]
    }
  }
};

const primeColor = desertJackalope.state.theme.colors.primary.default;

export { primeColor };

export default desertJackalope;


src/component/index.js

import React from "react";
import { css, connect, styled } from "frontity";

const Theme = ({ state }) => {
  return (
    <section>
      <TestComponent> testing</TestComponent>
      <p>testing</p>
    </section>
  );
};

export default connect(Theme);

//- CSS

const TestComponent = styled.p`
 background: ${primeColor};
`;


resulting error:

ReferenceError: primeColor is not defined
    at eval (webpack-internal:///./packages/desert-jackalope/src/components/index.js:9:147)
    at Module../packages/desert-jackalope/src/components/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:4912:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./packages/desert-jackalope/src/index.js:3:69)
    at Module../packages/desert-jackalope/src/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:4924:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./build/bundling/entry-points/server.ts:3:84)
    at Module../build/bundling/entry-points/server.ts (/mnt/c/Users/aslan/home/work/server-front/build/server.js:139:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at /mnt/c/Users/aslan/home/work/server-front/build/server.js:104:18

Example 2 (export primeColor using connect() )

Okay this is me trying something new based on what you’ve said. But this also does not work. It is able to compile but it does not result in the intended styling:

src/index.js

import Theme from "./components";
import image from "@frontity/html2react/processors/image";
import { css, connect, styled } from "frontity";

const desertJackalope = {
  name: "desert-jackalope",
  roots: {
    // In Frontity, any package can add React components to the site.
    // We use roots for that, scoped to the "theme" namespace.
    theme: Theme
  },
  state: {
    // State is where the packages store their default settings and other
    // relevant state. It is scoped to the "theme" namespace.
    theme: {
      isBlog: false,
      colors: {
        primary: {
          default: "#2657eb",
          heavy: "#1f38c5"
        }
      },
      newsletterURL:
        "https://the-jackalope.us8.list-manage.com/subscribe/post?u=7fc8ae244460f6dd1c74dd7bf&amp;id=74ff6c880b",
      footerlinks: {
        github: "https://github.com/jcklpe",
        blog: "https://www.jackalope.tech"
      }
    }
  },
  // Actions are functions that modify the state or deal with other parts of
  // Frontity like libraries.
  actions: {
    theme: {}
  },
  libraries: {
    html2react: {
      // Add a processor to html2react so it processes the <img> tags
      // inside the content HTML. You can add your own processors too.
      processors: [image]
    }
  }
};

const primeColor = desertJackalope.state.theme.colors.primary.default;

export default connect(desertJackalope, primeColor);


the src/components/index file:

import React from "react";
import { css, connect, styled } from "frontity";

import { primeColor } from "../index";

const Theme = ({ state }) => {
  return (
    <section>
      <TestComponent> testing ing</TestComponent>
      <p>testing</p>
    </section>
  );
};

export default connect(Theme);

//- CSS

const TestComponent = styled.p`
  background: ${primeColor};
`;


Result
This doesn’t throw an error but it also doesn’t apply the styling at all.


Example 3 (export desertJackalope theme via connect)

The src/index file:

import Theme from "./components";
import image from "@frontity/html2react/processors/image";
import { css, connect, styled } from "frontity";

const desertJackalope = {
  name: "desert-jackalope",
  roots: {
    // In Frontity, any package can add React components to the site.
    // We use roots for that, scoped to the "theme" namespace.
    theme: Theme
  },
  state: {
    // State is where the packages store their default settings and other
    // relevant state. It is scoped to the "theme" namespace.
    theme: {
      isBlog: false,
      colors: {
        primary: {
          default: "#2657eb",
          heavy: "#1f38c5"
        }
      },
      newsletterURL:
        "https://the-jackalope.us8.list-manage.com/subscribe/post?u=7fc8ae244460f6dd1c74dd7bf&amp;id=74ff6c880b",
      footerlinks: {
        github: "https://github.com/jcklpe",
        blog: "https://www.jackalope.tech"
      }
    }
  },
  // Actions are functions that modify the state or deal with other parts of
  // Frontity like libraries.
  actions: {
    theme: {}
  },
  libraries: {
    html2react: {
      // Add a processor to html2react so it processes the <img> tags
      // inside the content HTML. You can add your own processors too.
      processors: [image]
    }
  }
};

export default connect(desertJackalope);

the src/component/index file:

import React from "react";
import { css, connect, styled } from "frontity";

import { desertJackalope } from "../index";

const primeColor = desertJackalope.state.theme.colors.primary.default;

const Theme = ({ state }) => {
  return (
    <section>
      <TestComponent> testing ing</TestComponent>
      <p>testing</p>
      <p>{primeColor}</p>
    </section>
  );
};

export default connect(Theme);

//- CSS

const TestComponent = styled.p`
  background: ${primeColor};
`;


Results in:

TypeError: Cannot read property 'state' of undefined
    at eval (webpack-internal:///./packages/desert-jackalope/src/components/index.js:9:73)
    at Module../packages/desert-jackalope/src/components/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:4912:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./packages/desert-jackalope/src/index.js:2:69)
    at Module../packages/desert-jackalope/src/index.js (/mnt/c/Users/aslan/home/work/server-front/build/server.js:4924:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at eval (webpack-internal:///./build/bundling/entry-points/server.ts:3:84)
    at Module../build/bundling/entry-points/server.ts (/mnt/c/Users/aslan/home/work/server-front/build/server.js:139:1)
    at __webpack_require__ (/mnt/c/Users/aslan/home/work/server-front/build/server.js:27:30)
    at /mnt/c/Users/aslan/home/work/server-front/build/server.js:104:18

Does this make sense? Do you see what I’m asking how to do?

Possibly related issue here: Double rendering of items held in state array?

Am I misunderstanding how themes/settings work with state for Frontity?