Creating custom themes

You can start using Reshaped with the provided theme, but at some point, you might want to apply custom values to the design tokens to align them with your brand. To address this, Reshaped has a command-line interface for creating new themes.

To add new themes, create a reshaped.config.js file with the theme definitions next to your project's package.json file.

const config = {
  themes: {
    productTheme: {
      color: {
        foregroundNeutral: { hex: "#1a1a1a", hexDark: "#fff" },
      },
    },
  },
};

module.exports = config;

In this example, we have defined a theme that changes only the foregroundNeutral token value. All other values are inherited from the default Reshaped theme. You can check all supported tokens and their format in the last section of this page.

If you're changing the default viewport values in your theme, make sure to use getConfig function in your postcss.config.js file instead of the default config and pass the location of your theme CSS file.

const path = require("path");
const { getConfig } = require("reshaped/config/postcss");

module.exports = getConfig({
  themeMediaCSSPath: path.resolve(__dirname, "src/themes/my-theme/media.css"),
});

When creating new themes, you have to override each color token manually. If you don't need to use specific color values, check out our documentation on generating the color palette automatically based on a single color value.

Now that you have added a config file with theme definitions, you can use the Reshaped CLI to generate these themes. Add an NPM script to call the CLI in your package.json:

{
  "scripts": {
    "build:themes": "reshaped theming --output src/themes"
  }
}

Running yarn build:themes or npm run build:themes will take the theme definitions from the reshaped.config.js file and compile them into the src/themes folder. The script will create a folder for each theme and theme fragment with variable files inside.

src
└── themes
    ├── productTheme
    │   └── theme.module.css
    └── fragments
        └── twitter
            └── theme.module.css

With the themes built, you can now import them into your code. We can start by picking the productTheme theme we just built and passing it to the Reshaped provider:

import { Reshaped } from "reshaped";
import "themes/productTheme/theme.css";

const Application = ({ children }) => (
  <Reshaped theme="productTheme">{children}</Reshaped>
);

Our product now uses a custom theme and has a new foregroundNeutral token value available. It still uses other tokens from the default Reshaped theme, which means the Button component uses a violet color for its background.

Let's create a TwitterButton component with a different button background color using a Twitter theme fragment. We can use the Theme utility to define a theme just for the components rendered inside it.

import { Button, Theme } from "reshaped";
import "themes/fragments/twitter/theme.css";

const TwitterButton = (buttonProps) => (
  <Theme name="twitter">
    <Button {...buttonProps} />
  </Theme>
);

This concept is called Scoped Theming, and you can learn more about it in a separate section.

Even though reshaped.config.js is a JavaScript file, you can use comments to enable type autocompletion:

/** @type {import('reshaped').ReshapedConfig} */
const config = {
  themes: {
    twitter: {
      color: {
        backgroundPrimary: { hex: "#1da1f2" },
      },
    },
  },
};

module.exports = config;
  • // @ts-check enables type checking for the config file.
  • The @type comment defines the type for the next variable. This ensures that the config format is type-checked according to the type definition from the Reshaped package.

In addition to themes, reshaped.config.js allows you to create theme fragments. A theme fragment is a subset of theme value overrides. By using theme fragments, you can save bundle size as your theme output will contain only the tokens you have changed instead of the whole theme.

const config = {
  themeFragments: {
    twitter: {
      color: {
        backgroundPrimary: { hex: "#1da1f2" },
      },
    },
  },
};

module.exports = config;

This is quite helpful when you're customizing a specific part of the product but don't need to apply this customization to the whole page. For instance, you can create a Twitter theme fragment to implement a TwitterButton component.

Another benefit is that it's easier to combine themes this way. For example, if your product has two themes and you need to render the TwitterButton in both themes, you won't have to create all combinations of themes yourself. Instead, you can create two main themes and a Twitter theme fragment that will inherit the correct token values from the currently used theme.

Reshaped semantic tokens aim to provide a limited number of tokens that should cover most use cases for building interfaces. However, there will always be edge cases where semantic tokens might feel limiting. For example, imagine you're building a chart component. You need a few custom colors for it but also want to ensure they support dark mode the same way all Reshaped components do.

To support that, you can add any custom key for all the tokens in the theme definition, and they will be compiled to CSS alongside all other theme token values.

const config = {
  themes: {
    productTheme: {
      color: {
        foregroundNeutral: { hex: "#1a1a1a", hexDark: "#fff" },
        chartTomato: { hex: "#ff6347", hexDark: "#b8412c" },
      },
    },
  },
};

If you're adding new background colors, you might also want to generate on colors for them, similar to how we handle the default tokens. You can use the themeOptions.generateOnColorsFor option in the config. This will keep all of the default generated on colors and additionally generate on colors for the tokens you list, resolving them to black or white based on the contrast ratio.

const config = {
  themes: {
    productTheme: {
      color: {
        backgroundChart: { hex: "#ff6347" },
      },
    },
  },
  themeOptions: {
    generateOnColorsFor: ["backgroundChart"],
  },
};

When customizing background color token values, Reshaped automatically generates on color values. These are resolved to white or black based on the background color's contrast ratio.

Reshaped uses the WCAG criteria to decide which onBackground colors to generate. Sometimes, it has false positives and negatives for the human eye. Because of that, we provide a way to switch to the APCA algorithm when creating themes. You can select it using the colorContrastAlgorithm option in the theme settings:

const config = {
  ...,
  themeOptions: {
    colorContrastAlgorithm: "apca"
  }
}

You can also manually change the generated color values using the themeOptions.onColorValues configuration. This allows you to pass resolved color hexLight and hexDark mode values for each of the supported theme colors:

const config = {
  ...,
  themeOptions: {
    onColorValues: {
      primary: {
        hexLight: "#...", // Replaces white color
        hexDark: "#...", // Replaces black color
      },
    }
  }
}

A theme is represented by an object that has token types as keys. Each token type contains a dictionary of token objects with their values.

module.exports = {
  themes: {
    [themeName]: {
      color: {
        backgroundNeutral: { ... },
        ...
      },
      unit: {
        base: { ... },
      },
      radius: {
        small: { ... },
        ...
      },
      fontFamily: {
        body: { ... },
        ...
      },
      fontWeight: {
        regular: { ... },
        ...
      },
      font: {
        title1: { ... },
        ...
      },
      shadow: {
        raised: { ... },
        ...
      },
      viewport: {
        s: { ... },
        ...
      }
    }
  },
  themeFragments: {
    [fragmentName]: { ... }
  },
  themeOptions: {
    generateOnColorsFor: ["backgroundChart"],
    colorContrastAlgorithm: "apca";
    onColorValues: {
      primary: {
        hexLight: "#...",
        hexDark: "#...",
      },
      critical: { ... },
      positive: { ... },
      neutral: { ... }
    },
  }
}

In addition to the tokens in the theme definition, we also automatically generate dynamic token values. You can find more about them in the Design Tokens section.

Format:

{
  "color": {
    "foregroundNeutral": {
      "hex": "#000",
      "hexDark": "#fff"
    }
  }
}

Available token names:

foregroundNeutral
foregroundNeutralFaded
foregroundDisabled
foregroundPrimary
foregroundCritical
foregroundPositive

borderNeutral
borderNeutralFaded
borderDisabled
borderPrimary
borderPrimaryFaded
borderCritical
borderCriticalFaded
borderPositive
borderPositiveFaded

backgroundNeutral
backgroundNeutralFaded
backgroundDisabled
backgroundDisabledFaded
backgroundPrimary
backgroundPrimaryFaded
backgroundCritical
backgroundCriticalFaded
backgroundPositive
backgroundPositiveFaded

backgroundPage
backgroundPageFaded

backgroundElevationBase
backgroundElevationRaised
backgroundElevationOverlay

black
white
  • All onBackground color tokens are generated automatically.
  • The hexDark value is optional and can be omitted if you're not using dark mode or if the values are the same in both modes.
  • black and white tokens should preserve their values in both light and dark modes.

Format:

{
  "unit": {
    "base": {
      "px": 4
    }
  }
}

Available token names:

base

Format:

{
  "radius": {
    "small": {
      "px": 2
    }
  }
}

Available token names:

small
medium
large
  • The base unit controls how condensed your UI is.
  • x1 - x10 unit tokens will be auto-generated based on your base token px value.

Format:

{
  "fontFamily": {
    "body": {
      "family": "Arial, sans-serif"
    }
  }
}

Available token names:

body
title
  • Two font family types let you differentiate between regular text and headings while keeping the product styles consistent.
  • If you're using a custom font in this token, don't forget to include your font file in the product.

Format:

{
  "fontWeight": {
    "regular": { "weight": 400 }
  }
}

Available token names:

regular
medium
semibold
bold
extrabold
black

Format:

{
  "font": {
    "title3": {
      "fontSize": { "px": 40 },
      "lineHeight": { "px": 44 },
      "letterSpacing": { "px": 1 },
      "fontWeightToken": "bold",
      "fontFamilyToken": "display"
    }
  }
}

Available token names:

title1
title2
title3
title4
title5
title6

featured1
featured2
featured3

body1
body2
body3

caption1
caption2
  • fontWeightToken refers to the font weight token names.
  • fontFamilyToken refers to the font family token names.
  • letterSpacing is optional and defaults to normal value

Format:

{
  "shadow": {
    "raised": [
      {
        "offsetX": 0,
        "offsetY": 1,
        "blurRadius": 3,
        "colorToken": "black",
        "opacity": 0.08
      },
      {
        "offsetX": 0,
        "offsetY": 2,
        "blurRadius": 2,
        "colorToken": "black",
        "opacity": 0.06
      }
    ]
  }
}

Available token names:

raised
overlay
  • Uses an array of values to apply multiple shadows to the same element.
  • colorToken refers to a color token name.

Format:

{
  "viewport": {
    "m": { "minPx": 660 }
  }
}

Available token names:

m
l
xl
  • Browsers don't support theming media queries natively, so we're handling it with the help of a PostCSS plugin. You'll need to use the getConfig function and pass the path to the generated file with custom media queries.