
# Color palette

> Kesko Design System’s color palette is a selection of colors that work together to create consistency in digital products.

> Warning: Kesko Color Utilities are in active development and not yet ready for production use. APIs documented may change without notice.

## Dark mode Experimental

Our color system includes a dedicated `neutral-dark` ramp alongside the standard light `neutral` ramp. Together they provide the surface and text colors needed to support both light and dark modes without manual color picking. Each step in the dark neutral ramp is the semantic counterpart of the same step in the light neutral ramp:

### Choosing colors for dark mode

Saturated colors can be converted from a light theme to a dark theme using symmetry. The 12-step palette can be divided in half, and each half becomes a mirror of the other:

- If an element uses step `800` in light mode, it uses step `500` in dark mode.
- If a surface uses step `200` in light mode, it uses step `1100` in dark mode.
- The midpoint sits between steps `600` and `700`.

**Please note:** While this approach provides a great starting point, it does not work for every single use case. An example could be e.g. specific brand colors that you want to keep uniform across different theme modes.

## Installation

To install Kesko’s color utilities as a dependency in your project, run:

```sh [npm]
npm install @kesko/color
```

```sh [pnpm]
pnpm add @kesko/color
```

```sh [yarn]
yarn add @kesko/color
```

```sh [bun]
bun add @kesko/color
```

> Kesko’s color utilities are meant for developing our digital color palettes. If you just want to use the colors, they’re exposed as semantic tokens through @kesko/tokens.

## Usage

Import from @kesko/color directly only when you need to create a new color palette family or compose your own theme. The package exposes two helper functions, keskoPalette() and keskoTheme(), for exactly this.

### `keskoPalette()`

Creates a new Kesko color palette family with built-in defaults. The `name` is automatically prefixed with `--k-color-`, and the 12 default contrast ratios match the rest of the Kesko palette so a custom family slots in seamlessly alongside the built-in ones:

```js
import { keskoPalette } from "@kesko/color";

const palette = keskoPalette({
  name: "palette",
  colorKeys: ["#f86800"],
});
```

Pass two or more `colorKeys` to interpolate between multiple hues across the scale:

```js
const palette = keskoPalette({
  name: "palette",
  colorKeys: ["#f86800", "#cb4b00"],
});
```

The full list of provided options includes:

```js
const palette = keskoPalette({
  /**
   * Color palette name
   * @type {string}
   */
  name: "palette",

  /**
   * Source color(s) as hex values
   * @type {string[]}
   */
  colorKeys: ["#f86800"],

  /**
   * WCAG contrast ratios for each generated color
   * @type {number[]}
   */
  ratios: [1.06, 1.12, 1.22, 1.32, 1.45, 1.7, 2.5, 4.6, 6, 9, 16, 18],

  /**
   * Interpolation colorspace
   * @type {"RGB" | "HSL" | "HSV" | "HSLuv" | "LAB" | "LCH" | "OKLAB" | "OKLCH" | "CAM02" | "CAM02p"}
   */
  colorspace: "RGB",

  /**
   * Smooth interpolation between hues
   * @type {boolean}
   */
  smooth: true,

  /**
   * Global saturation adjustment (0–100)
   * @type {number}
   */
  saturation: 100,
});
```

### `keskoTheme()`

Combines one or more color palette families against a background and resolves them into a full theme. This is the step that actually generates the 12 colors per family. Until a palette is part of a theme, only its source keys and target ratios are defined.

```js
import { keskoTheme, background, neutral, orange } from "@kesko/color";

const theme = keskoTheme({
  colors: [neutral, orange],
  backgroundColor: background,
});
```

The full list of provided options includes:

```js
const theme = keskoTheme({
  /**
   * Array of color palettes to include in the theme
   * @type {Color[]}
   */
  colors: [orange, blue],

  /**
   * Reference background for contrast calculations
   * @type {BackgroundColor}
   */
  backgroundColor: background,

  /**
   * Background lightness (0–100)
   * @type {number}
   */
  lightness: 100,

  /**
   * Global contrast multiplier
   * @type {number}
   */
  contrast: 1,

  /**
   * Global saturation adjustment (0–100)
   * @type {number}
   */
  saturation: 100,

  /**
   * Output color format
   * @type {"HEX" | "RGB" | "HSL" | "HSV" | "LAB" | "LCH" | "CAM02" | "CAM02p"}
   */
  output: "HEX",

  /**
   * Contrast formula
   * @type {"wcag2" | "wcag3"}
   */
  formula: "wcag2",
});
```

## Included utilities

Alongside the two helpers, `@kesko/color` ships the default Kesko color palette families and fully composed themes as named exports, so you can drop them straight into a custom `keskoTheme()` call:

- **`keskoPalette()`:** Helper to create a new color palette family
- **`keskoTheme()`:** Helper to compose one or more palettes into a theme
- **`theme`:** Fully composed light theme combining all 9 families against a white background
- **`darkTheme`:** Dark theme with the neutral-dark palette against a dark background
- **`background`:** White (`#fff`) reference background for light-mode contrast math
- **`darkBackground`:** Dark reference background for dark-mode contrast math
- **`neutral`:** Neutral palette, pinned to white/black at extremes
- **`neutralDark`:** Neutral-dark palette, pinned to near-black/white at extremes (inverse of neutral)
- **`orange`:** Orange palette
- **`beige`:** Beige palette
- **`yellow`:** Yellow palette
- **`purple`:** Purple palette
- **`green`:** Green palette
- **`teal`:** Teal palette
- **`blue`:** Blue palette
- **`red`:** Red palette

## Accessibility

Every step in the light palette is generated by targeting a specific WCAG 2 contrast ratio against `#fff`. The neutral-dark palette targets the same ratios against its dark background. As a rule of thumb:

**Light mode (neutral and saturated palettes):**

- **Steps 800 and above** meet WCAG 2 AA (4.5:1) for normal-size text on white.
- **Step 700** meets WCAG 2 AA (3:1) for large text and non-text elements (UI components, graphical objects).
- **Steps 100–600** are intended for backgrounds, surfaces, and decorative use, not for text on white.

**Dark mode (neutral-dark palette):**

- **Steps 800 and above** meet WCAG 2 AA (4.5:1) for normal-size text on the dark background.
- The same ratio thresholds apply meaning that the palette is generated with identical contrast targets, just inverted.

When pairing colors against backgrounds other than the palette's reference background, always re-verify contrast with a tool such as the [WebAIM contrast checker](https://webaim.org/resources/contrastchecker/).

## Color palette


### Neutral 

- `var(--k-color-neutral-100)`: #ffffff (1:1 contrast)
- `var(--k-color-neutral-200)`: #f0f0f0 (1.14:1 contrast)
- `var(--k-color-neutral-300)`: #dddddd (1.36:1 contrast)
- `var(--k-color-neutral-400)`: #cdcdcd (1.58:1 contrast)
- `var(--k-color-neutral-500)`: #c0c0c0 (1.82:1 contrast)
- `var(--k-color-neutral-600)`: #a4a4a4 (2.49:1 contrast)
- `var(--k-color-neutral-700)`: #888888 (3.54:1 contrast)
- `var(--k-color-neutral-800)`: #757575 (4.61:1 contrast)
- `var(--k-color-neutral-900)`: #565656 (7.34:1 contrast)
- `var(--k-color-neutral-1000)`: #3e3e3e (10.7:1 contrast)
- `var(--k-color-neutral-1100)`: #282828 (14.74:1 contrast)
- `var(--k-color-neutral-1200)`: #000000 (21:1 contrast)


### Beige 

- `var(--k-color-beige-100)`: #fcf8f5 (1.06:1 contrast)
- `var(--k-color-beige-200)`: #f6f1ed (1.12:1 contrast)
- `var(--k-color-beige-300)`: #ede5dd (1.25:1 contrast)
- `var(--k-color-beige-400)`: #dacbbc (1.58:1 contrast)
- `var(--k-color-beige-500)`: #c9b6a1 (1.96:1 contrast)
- `var(--k-color-beige-600)`: #b9a38c (2.42:1 contrast)
- `var(--k-color-beige-700)`: #a7917a (3.01:1 contrast)
- `var(--k-color-beige-800)`: #83705c (4.73:1 contrast)
- `var(--k-color-beige-900)`: #6a5a4a (6.56:1 contrast)
- `var(--k-color-beige-1000)`: #5c4e40 (8:1 contrast)
- `var(--k-color-beige-1100)`: #2b241e (15.18:1 contrast)
- `var(--k-color-beige-1200)`: #191612 (18:1 contrast)


### Red 

- `var(--k-color-red-100)`: #fdf7f7 (1.06:1 contrast)
- `var(--k-color-red-200)`: #fbeeef (1.12:1 contrast)
- `var(--k-color-red-300)`: #f9e6e7 (1.2:1 contrast)
- `var(--k-color-red-400)`: #f5d0d2 (1.41:1 contrast)
- `var(--k-color-red-500)`: #eda6aa (1.96:1 contrast)
- `var(--k-color-red-600)`: #e88d92 (2.42:1 contrast)
- `var(--k-color-red-700)`: #e27378 (3.01:1 contrast)
- `var(--k-color-red-800)`: #d13c44 (4.73:1 contrast)
- `var(--k-color-red-900)`: #b9161e (6.56:1 contrast)
- `var(--k-color-red-1000)`: #a40910 (8:1 contrast)
- `var(--k-color-red-1100)`: #540001 (15.18:1 contrast)
- `var(--k-color-red-1200)`: #370001 (18:1 contrast)


### Orange 

- `var(--k-color-orange-100)`: #fdf7f0 (1.06:1 contrast)
- `var(--k-color-orange-200)`: #fcf0e0 (1.12:1 contrast)
- `var(--k-color-orange-300)`: #fae2c4 (1.25:1 contrast)
- `var(--k-color-orange-400)`: #f9c48c (1.58:1 contrast)
- `var(--k-color-orange-500)`: #fba656 (1.96:1 contrast)
- `var(--k-color-orange-600)`: #fd8726 (2.42:1 contrast)
- `var(--k-color-orange-700)`: #f86800 (3.01:1 contrast)
- `var(--k-color-orange-800)`: #cb4700 (4.73:1 contrast)
- `var(--k-color-orange-900)`: #a53900 (6.56:1 contrast)
- `var(--k-color-orange-1000)`: #903100 (8:1 contrast)
- `var(--k-color-orange-1100)`: #451700 (15.18:1 contrast)
- `var(--k-color-orange-1200)`: #2b0e00 (18:1 contrast)


### Yellow 

- `var(--k-color-yellow-100)`: #fff9e5 (1.05:1 contrast)
- `var(--k-color-yellow-200)`: #fff5d2 (1.09:1 contrast)
- `var(--k-color-yellow-300)`: #ffeaa2 (1.2:1 contrast)
- `var(--k-color-yellow-400)`: #ffe075 (1.3:1 contrast)
- `var(--k-color-yellow-500)`: #ffd23f (1.43:1 contrast)
- `var(--k-color-yellow-600)`: #fcc10f (1.64:1 contrast)
- `var(--k-color-yellow-700)`: #e19e00 (2.3:1 contrast)
- `var(--k-color-yellow-800)`: #ab6f00 (4.2:1 contrast)
- `var(--k-color-yellow-900)`: #935d00 (5.5:1 contrast)
- `var(--k-color-yellow-1000)`: #734600 (8:1 contrast)
- `var(--k-color-yellow-1100)`: #372100 (15.18:1 contrast)
- `var(--k-color-yellow-1200)`: #211400 (18:1 contrast)


### Green 

- `var(--k-color-green-100)`: #f5f9f3 (1.06:1 contrast)
- `var(--k-color-green-200)`: #ebf4e8 (1.12:1 contrast)
- `var(--k-color-green-300)`: #daead3 (1.25:1 contrast)
- `var(--k-color-green-400)`: #b6d7a9 (1.58:1 contrast)
- `var(--k-color-green-500)`: #99c586 (1.96:1 contrast)
- `var(--k-color-green-600)`: #7eb567 (2.42:1 contrast)
- `var(--k-color-green-700)`: #66a44c (3.01:1 contrast)
- `var(--k-color-green-800)`: #408225 (4.73:1 contrast)
- `var(--k-color-green-900)`: #306a17 (6.56:1 contrast)
- `var(--k-color-green-1000)`: #275c12 (8:1 contrast)
- `var(--k-color-green-1100)`: #122b07 (15.18:1 contrast)
- `var(--k-color-green-1200)`: #0b1a05 (18:1 contrast)


### Teal 

- `var(--k-color-teal-100)`: #f3f9fa (1.06:1 contrast)
- `var(--k-color-teal-200)`: #e6f3f6 (1.12:1 contrast)
- `var(--k-color-teal-300)`: #d2eaef (1.25:1 contrast)
- `var(--k-color-teal-400)`: #a3d6df (1.58:1 contrast)
- `var(--k-color-teal-500)`: #7ec4d2 (1.96:1 contrast)
- `var(--k-color-teal-600)`: #5db2c3 (2.42:1 contrast)
- `var(--k-color-teal-700)`: #40a1b4 (3.01:1 contrast)
- `var(--k-color-teal-800)`: #1b7e91 (4.73:1 contrast)
- `var(--k-color-teal-900)`: #106677 (6.56:1 contrast)
- `var(--k-color-teal-1000)`: #0b5868 (8:1 contrast)
- `var(--k-color-teal-1100)`: #042a32 (15.18:1 contrast)
- `var(--k-color-teal-1200)`: #03191e (18:1 contrast)


### Blue 

- `var(--k-color-blue-100)`: #eff8ff (1.06:1 contrast)
- `var(--k-color-blue-200)`: #e8f3fb (1.12:1 contrast)
- `var(--k-color-blue-300)`: #d9e8f4 (1.25:1 contrast)
- `var(--k-color-blue-400)`: #b9d0e3 (1.58:1 contrast)
- `var(--k-color-blue-500)`: #9fbcd6 (1.96:1 contrast)
- `var(--k-color-blue-600)`: #88aaca (2.42:1 contrast)
- `var(--k-color-blue-700)`: #7298be (3.01:1 contrast)
- `var(--k-color-blue-800)`: #4676a6 (4.73:1 contrast)
- `var(--k-color-blue-900)`: #295f96 (6.56:1 contrast)
- `var(--k-color-blue-1000)`: #18528c (8:1 contrast)
- `var(--k-color-blue-1100)`: #001f5a (15.18:1 contrast)
- `var(--k-color-blue-1200)`: #00143c (18:1 contrast)


### Purple 

- `var(--k-color-purple-100)`: #f9f7fb (1.06:1 contrast)
- `var(--k-color-purple-200)`: #f4f1f7 (1.12:1 contrast)
- `var(--k-color-purple-300)`: #e9e4f0 (1.25:1 contrast)
- `var(--k-color-purple-400)`: #d4c9e1 (1.58:1 contrast)
- `var(--k-color-purple-500)`: #c3b3d5 (1.96:1 contrast)
- `var(--k-color-purple-600)`: #b29ec9 (2.42:1 contrast)
- `var(--k-color-purple-700)`: #a28bbe (3.01:1 contrast)
- `var(--k-color-purple-800)`: #8565aa (4.73:1 contrast)
- `var(--k-color-purple-900)`: #704c9c (6.56:1 contrast)
- `var(--k-color-purple-1000)`: #643c93 (8:1 contrast)
- `var(--k-color-purple-1100)`: #330072 (15.18:1 contrast)
- `var(--k-color-purple-1200)`: #20004c (18:1 contrast)



## Dark mode (neutral-dark palette)


- `var(--k-color-neutral-dark-100)`: #1b1b1b (1:1 contrast)
- `var(--k-color-neutral-dark-200)`: #262626 (1.14:1 contrast)
- `var(--k-color-neutral-dark-300)`: #333333 (1.36:1 contrast)
- `var(--k-color-neutral-dark-400)`: #3d3d3d (1.58:1 contrast)
- `var(--k-color-neutral-dark-500)`: #464646 (1.82:1 contrast)
- `var(--k-color-neutral-dark-600)`: #5a5a5a (2.49:1 contrast)
- `var(--k-color-neutral-dark-700)`: #717171 (3.54:1 contrast)
- `var(--k-color-neutral-dark-800)`: #848484 (4.61:1 contrast)
- `var(--k-color-neutral-dark-900)`: #a9a9a9 (7.34:1 contrast)
- `var(--k-color-neutral-dark-1000)`: #cbcbcb (10.7:1 contrast)
- `var(--k-color-neutral-dark-1100)`: #ededed (14.74:1 contrast)
- `var(--k-color-neutral-dark-1200)`: #ffffff (21:1 contrast)


