Creating custom themes

You can start using Reshaped with the default theme, but eventually you might want to customize the design tokens to match your brand. To help with that, Reshaped includes a command-line tool for generating new themes.

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

// @ts-check
/** @type {import('reshaped').ReshapedConfig} */
const config = {
  themes: {
    productTheme: {
      color: {
        foregroundNeutral: { hex: "#1a1a1a", hexDark: "#fff" },
      },
    },
  },
};

module.exports = config;

In this example, we've defined a theme that overrides only the foregroundNeutral token. All other values fall back to the default Reshaped theme. You can find the full list of supported tokens and their formats at the bottom of this page.

  • // @ts-check enables type checking for the config file.
  • The @type comment tells TypeScript the type of the next variable, so your config is validated against the Reshaped types.

When creating new themes, you need to override each color token manually. If you don't need specific color values, check out our docs on generating a color palette automatically from a single color.

If you're updating the default viewport values in your theme, use the getConfig function in your postcss.config.js instead of the default config, and pass the path to 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"),
});

Now that you've added a config file with your theme definitions, you can use the Reshaped CLI to generate the 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 creates 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. Start by picking the productTheme 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>
);

Your product now uses a custom theme and has a new foregroundNeutral token value available. It still uses other tokens from the default Reshaped theme, so the Button component uses a violet background by default.

Let's create a TwitterButton component with a different background color using a Twitter theme fragment. We'll use the Theme utility to apply this theme only to 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.

Reshaped semantic tokens aim to provide a limited set of tokens that cover most interface use cases. But there will always be edge cases where semantic tokens feel limiting. For example, say you're building a chart component. You need a few custom colors, but still want them to support dark mode like all other Reshaped components.

To handle this, you can add any custom key to the theme definition. It'll be compiled to CSS along with the rest of the theme tokens.

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

Reshaped uses the WCAG criteria to decide which onBackground colors to generate. Sometimes it gives false positives or negatives compared to what looks good to the human eye. To improve that, you can switch to the APCA algorithm when creating themes. Just set the colorContrastAlgorithm option in the theme settings:

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

By default, Reshaped uses oklch() for CSS color values. If some of the browsers you support don't work with oklch(), you can switch the output format to hex:

const config = {
  ...,
  themeOptions: {
    colorOutputFormat: 'hex'
  }
}

When you customize 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.

You can also override these values manually using the themeOptions.onColorValues config. This lets you pass resolved hexLight and hexDark values for each supported theme color:

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

If you're adding new background colors, you might also want to generate on colors for them—just like we do for the default tokens.

Use the themeOptions.generateOnColorsFor option in the config. It keeps all default generated on colors and adds new ones for the tokens you list, resolving them to black or white based on contrast ratio.

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

In addition to themes, reshaped.config.js lets you create theme fragments. A theme fragment is a subset of theme value overrides.

Using fragments helps reduce bundle size since the output will only include the tokens you’ve changed, not the entire theme.

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

module.exports = config;

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

Another benefit is easier theme composition. If your product has two themes and you need to render TwitterButton in both, you don't have to create theme combinations yourself. Just create two main themes and a Twitter fragment that inherits the correct token values from the active theme.

A theme is an object where token types are the keys. Each token type contains a dictionary of token names 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 you define in your theme, Reshaped also generates dynamic token values automatically. You can learn 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.

Professionally crafted React & Figma components for building beautiful products or starting your own design system
Built with Reshaped in Amsterdam ❤️
Contact us·License agreement·FAQ·
© Reshaped 2025