2
0
Эх сурвалжийг харах

Switch Tailwind color theme to use customisable CSS variables

Co-authored-by: Scott Cranfill <scott@scottcranfill.com>
Thibaud Colas 2 жил өмнө
parent
commit
8f96a5669d

+ 90 - 0
client/src/tokens/colorVariables.js

@@ -0,0 +1,90 @@
+/**
+ * Generate a CSS calc() expression so one HSL component is derived from another.
+ * For example, with a reference color of `hsl(66 50% 25%)`, and a derived color of hsl(66 65% 20%),
+ * - The hue components are identical, so the derived hue should just refer to the reference hue.
+ * - Saturation for the derived color is higher, so should be `calc(var(--ref-saturation) + 15%)`
+ * - Lightness for the derived color is lower, so should be `calc(var(--ref-lightness) - 5%)`.
+ */
+const calcHSLDifference = (refVariable, refValue, value, unit = '') => {
+  const ref = Number(refValue);
+  const val = Number(value);
+
+  // If the value is equal to the reference, there is nothing to calc.
+  if (ref === val) {
+    return `var(${refVariable})`;
+  }
+
+  // Either add or remove the difference based on whether it’s positive or negative.
+  const diff = ref - val;
+  const operation = `${diff > 0 ? '-' : '+'} ${Math.abs(diff)}${unit}`;
+
+  return `calc(var(${refVariable}) ${operation})`;
+};
+
+/**
+ * Generate customisable CSS variables for a color palette, with override-able HSL components.
+ *
+ * For each shade of a color, we want to generate four variables:
+ * - One for each HSL component of the color (Hue, Saturation, Lightness).
+ * - A valid HSL color value combining the three components.
+ *
+ * A shade’s HSL components need to be derived from the reference color’s HSL components,
+ * so site implementers can change all shades of a color at once by setting the HSL components of the "reference" color.
+ *
+ * For example, for a "light red" color derived from "red", this will create:
+ * --red-light-hue: var(--red-hue);
+ * --red-light-saturation: var(--red-saturation + 15%);
+ * --red-light-lightness: calc(var(--red-lightness) - 5%);
+ * --red-light: hsl(var(--red-light-hue) var(--red-light-saturation) var(--red-light-lightness));
+ *
+ * For the red reference color defined as `hsl(66 50% 25%)`, this will create:
+ * --red-hue: 66;
+ * --red-saturation: 50%;
+ * --red-lightness: 25%;
+ * --red: hsl(var(--red-hue) var(--red-saturation) var(--red-lightness));
+ *
+ */
+const generateColorVariables = (colors) => {
+  /* eslint-disable no-param-reassign, id-length */
+  const colorVariables = Object.values(colors).reduce((root, hues) => {
+    // Use the DEFAULT hue as a reference to derive others from, or the darkest if there is no defined default.
+    const darkestHue = Object.keys(hues).sort((a, b) => b - a)[0];
+    const reference = hues.DEFAULT || hues[darkestHue];
+    const [refH, refS, refL] = reference.hsl.match(/\d+/g);
+    const refVar = reference.cssVariable;
+
+    // Generate color variables for all individual color shades, based on the reference.
+    Object.values(hues).forEach((shade) => {
+      // CSS variables will we generate.
+      const vars = {
+        hsl: shade.cssVariable,
+        h: `${shade.cssVariable}-hue`,
+        s: `${shade.cssVariable}-saturation`,
+        l: `${shade.cssVariable}-lightness`,
+      };
+      const [h, s, l] = shade.hsl.match(/\d+/g);
+      const isReferenceShade = reference.hex === shade.hex;
+
+      if (isReferenceShade) {
+        // If this is the reference shade, we use its HSL values as-is.
+        root[vars.h] = h;
+        root[vars.s] = `${s}%`;
+        root[vars.l] = `${l}%`;
+      } else {
+        // If this is a derived shade, we will derive its HSL values from the reference.
+        root[vars.h] = calcHSLDifference(`${refVar}-hue`, refH, h);
+        root[vars.s] = calcHSLDifference(`${refVar}-saturation`, refS, s, '%');
+        root[vars.l] = calcHSLDifference(`${refVar}-lightness`, refL, l, '%');
+      }
+
+      root[vars.hsl] = `hsl(var(${vars.h}) var(${vars.s}) var(${vars.l}))`;
+    });
+
+    return root;
+  }, {});
+  return colorVariables;
+};
+
+module.exports = {
+  generateColorVariables,
+};

+ 121 - 0
client/src/tokens/colorVariables.test.js

@@ -0,0 +1,121 @@
+const colors = require('./colors');
+const { generateColorVariables } = require('./colorVariables');
+
+describe('generateColorVariables', () => {
+  it('generates all variables', () => {
+    const colorVariables = generateColorVariables(colors);
+    const generatedVariables = Object.keys(colorVariables);
+    Object.values(colors).forEach((hues) => {
+      Object.values(hues).forEach((shade) => {
+        expect(generatedVariables).toContain(shade.cssVariable);
+      });
+    });
+  });
+
+  /**
+   * If this test breaks, it means we’ve either changed our color palette, or changed how we make each of the colors customisable.
+   * If the change is intentional, we will then need to update our `custom_user_interface_colours` documentation.
+   * - Open Storybook’s color customisation story in a browser
+   * - Use your browser’s DevTools to copy the relevant story markup to our Markdown documentation.
+   * - Leave the copied content exactly as-is when pasting, to avoid any Markdown formatting issues.
+   */
+  it('is stable (update custom_user_interface_colours documentation when this changes)', () => {
+    const colorVariables = generateColorVariables(colors);
+    expect(colorVariables).toMatchInlineSnapshot(`
+      Object {
+        "--w-color-black": "hsl(var(--w-color-black-hue) var(--w-color-black-saturation) var(--w-color-black-lightness))",
+        "--w-color-black-hue": "0",
+        "--w-color-black-lightness": "0%",
+        "--w-color-black-saturation": "0%",
+        "--w-color-critical-100": "hsl(var(--w-color-critical-100-hue) var(--w-color-critical-100-saturation) var(--w-color-critical-100-lightness))",
+        "--w-color-critical-100-hue": "calc(var(--w-color-critical-200-hue) + 355)",
+        "--w-color-critical-100-lightness": "calc(var(--w-color-critical-200-lightness) + 13%)",
+        "--w-color-critical-100-saturation": "calc(var(--w-color-critical-200-saturation) + 40%)",
+        "--w-color-critical-200": "hsl(var(--w-color-critical-200-hue) var(--w-color-critical-200-saturation) var(--w-color-critical-200-lightness))",
+        "--w-color-critical-200-hue": "0",
+        "--w-color-critical-200-lightness": "54%",
+        "--w-color-critical-200-saturation": "58%",
+        "--w-color-critical-50": "hsl(var(--w-color-critical-50-hue) var(--w-color-critical-50-saturation) var(--w-color-critical-50-lightness))",
+        "--w-color-critical-50-hue": "var(--w-color-critical-200-hue)",
+        "--w-color-critical-50-lightness": "calc(var(--w-color-critical-200-lightness) + 41%)",
+        "--w-color-critical-50-saturation": "calc(var(--w-color-critical-200-saturation) + 25%)",
+        "--w-color-grey-100": "hsl(var(--w-color-grey-100-hue) var(--w-color-grey-100-saturation) var(--w-color-grey-100-lightness))",
+        "--w-color-grey-100-hue": "var(--w-color-grey-600-hue)",
+        "--w-color-grey-100-lightness": "calc(var(--w-color-grey-600-lightness) + 73%)",
+        "--w-color-grey-100-saturation": "var(--w-color-grey-600-saturation)",
+        "--w-color-grey-200": "hsl(var(--w-color-grey-200-hue) var(--w-color-grey-200-saturation) var(--w-color-grey-200-lightness))",
+        "--w-color-grey-200-hue": "var(--w-color-grey-600-hue)",
+        "--w-color-grey-200-lightness": "calc(var(--w-color-grey-600-lightness) + 42%)",
+        "--w-color-grey-200-saturation": "var(--w-color-grey-600-saturation)",
+        "--w-color-grey-400": "hsl(var(--w-color-grey-400-hue) var(--w-color-grey-400-saturation) var(--w-color-grey-400-lightness))",
+        "--w-color-grey-400-hue": "var(--w-color-grey-600-hue)",
+        "--w-color-grey-400-lightness": "calc(var(--w-color-grey-600-lightness) + 21%)",
+        "--w-color-grey-400-saturation": "var(--w-color-grey-600-saturation)",
+        "--w-color-grey-50": "hsl(var(--w-color-grey-50-hue) var(--w-color-grey-50-saturation) var(--w-color-grey-50-lightness))",
+        "--w-color-grey-50-hue": "calc(var(--w-color-grey-600-hue) + 240)",
+        "--w-color-grey-50-lightness": "calc(var(--w-color-grey-600-lightness) + 82%)",
+        "--w-color-grey-50-saturation": "calc(var(--w-color-grey-600-saturation) + 12%)",
+        "--w-color-grey-600": "hsl(var(--w-color-grey-600-hue) var(--w-color-grey-600-saturation) var(--w-color-grey-600-lightness))",
+        "--w-color-grey-600-hue": "0",
+        "--w-color-grey-600-lightness": "15%",
+        "--w-color-grey-600-saturation": "0%",
+        "--w-color-info-100": "hsl(var(--w-color-info-100-hue) var(--w-color-info-100-saturation) var(--w-color-info-100-lightness))",
+        "--w-color-info-100-hue": "194",
+        "--w-color-info-100-lightness": "36%",
+        "--w-color-info-100-saturation": "66%",
+        "--w-color-info-50": "hsl(var(--w-color-info-50-hue) var(--w-color-info-50-saturation) var(--w-color-info-50-lightness))",
+        "--w-color-info-50-hue": "calc(var(--w-color-info-100-hue) + 2)",
+        "--w-color-info-50-lightness": "calc(var(--w-color-info-100-lightness) + 58%)",
+        "--w-color-info-50-saturation": "calc(var(--w-color-info-100-saturation) + 15%)",
+        "--w-color-positive-100": "hsl(var(--w-color-positive-100-hue) var(--w-color-positive-100-saturation) var(--w-color-positive-100-lightness))",
+        "--w-color-positive-100-hue": "162",
+        "--w-color-positive-100-lightness": "32%",
+        "--w-color-positive-100-saturation": "66%",
+        "--w-color-positive-50": "hsl(var(--w-color-positive-50-hue) var(--w-color-positive-50-saturation) var(--w-color-positive-50-lightness))",
+        "--w-color-positive-50-hue": "calc(var(--w-color-positive-100-hue) + 2)",
+        "--w-color-positive-50-lightness": "calc(var(--w-color-positive-100-lightness) + 61%)",
+        "--w-color-positive-50-saturation": "calc(var(--w-color-positive-100-saturation) + 11%)",
+        "--w-color-primary": "hsl(var(--w-color-primary-hue) var(--w-color-primary-saturation) var(--w-color-primary-lightness))",
+        "--w-color-primary-200": "hsl(var(--w-color-primary-200-hue) var(--w-color-primary-200-saturation) var(--w-color-primary-200-lightness))",
+        "--w-color-primary-200-hue": "var(--w-color-primary-hue)",
+        "--w-color-primary-200-lightness": "calc(var(--w-color-primary-lightness) - 5%)",
+        "--w-color-primary-200-saturation": "var(--w-color-primary-saturation)",
+        "--w-color-primary-hue": "254",
+        "--w-color-primary-lightness": "25%",
+        "--w-color-primary-saturation": "50%",
+        "--w-color-secondary": "hsl(var(--w-color-secondary-hue) var(--w-color-secondary-saturation) var(--w-color-secondary-lightness))",
+        "--w-color-secondary-100": "hsl(var(--w-color-secondary-100-hue) var(--w-color-secondary-100-saturation) var(--w-color-secondary-100-lightness))",
+        "--w-color-secondary-100-hue": "var(--w-color-secondary-hue)",
+        "--w-color-secondary-100-lightness": "calc(var(--w-color-secondary-lightness) + 10%)",
+        "--w-color-secondary-100-saturation": "var(--w-color-secondary-saturation)",
+        "--w-color-secondary-400": "hsl(var(--w-color-secondary-400-hue) var(--w-color-secondary-400-saturation) var(--w-color-secondary-400-lightness))",
+        "--w-color-secondary-400-hue": "calc(var(--w-color-secondary-hue) + 2)",
+        "--w-color-secondary-400-lightness": "calc(var(--w-color-secondary-lightness) - 7%)",
+        "--w-color-secondary-400-saturation": "var(--w-color-secondary-saturation)",
+        "--w-color-secondary-50": "hsl(var(--w-color-secondary-50-hue) var(--w-color-secondary-50-saturation) var(--w-color-secondary-50-lightness))",
+        "--w-color-secondary-50-hue": "var(--w-color-secondary-hue)",
+        "--w-color-secondary-50-lightness": "calc(var(--w-color-secondary-lightness) + 72%)",
+        "--w-color-secondary-50-saturation": "calc(var(--w-color-secondary-saturation) - 37%)",
+        "--w-color-secondary-600": "hsl(var(--w-color-secondary-600-hue) var(--w-color-secondary-600-saturation) var(--w-color-secondary-600-lightness))",
+        "--w-color-secondary-600-hue": "calc(var(--w-color-secondary-hue) + 2)",
+        "--w-color-secondary-600-lightness": "calc(var(--w-color-secondary-lightness) - 11%)",
+        "--w-color-secondary-600-saturation": "var(--w-color-secondary-saturation)",
+        "--w-color-secondary-hue": "180",
+        "--w-color-secondary-lightness": "25%",
+        "--w-color-secondary-saturation": "100%",
+        "--w-color-warning-100": "hsl(var(--w-color-warning-100-hue) var(--w-color-warning-100-saturation) var(--w-color-warning-100-lightness))",
+        "--w-color-warning-100-hue": "40",
+        "--w-color-warning-100-lightness": "49%",
+        "--w-color-warning-100-saturation": "100%",
+        "--w-color-warning-50": "hsl(var(--w-color-warning-50-hue) var(--w-color-warning-50-saturation) var(--w-color-warning-50-lightness))",
+        "--w-color-warning-50-hue": "calc(var(--w-color-warning-100-hue) - 3)",
+        "--w-color-warning-50-lightness": "calc(var(--w-color-warning-100-lightness) + 42%)",
+        "--w-color-warning-50-saturation": "calc(var(--w-color-warning-100-saturation) - 21%)",
+        "--w-color-white": "hsl(var(--w-color-white-hue) var(--w-color-white-saturation) var(--w-color-white-lightness))",
+        "--w-color-white-hue": "0",
+        "--w-color-white-lightness": "100%",
+        "--w-color-white-saturation": "0%",
+      }
+    `);
+  });
+});

+ 59 - 13
client/src/tokens/colors.js

@@ -19,8 +19,10 @@ const colors = {
   black: {
     DEFAULT: {
       hex: '#000000',
+      hsl: 'hsl(0, 0%, 0%)',
       bgUtility: 'w-bg-black',
       textUtility: 'w-text-black',
+      cssVariable: '--w-color-black',
       usage: 'Shadows only',
       contrastText: 'white',
     },
@@ -28,36 +30,46 @@ const colors = {
   grey: {
     600: {
       hex: '#262626',
+      hsl: 'hsl(0, 0%, 15%)',
       bgUtility: 'w-bg-grey-600',
       textUtility: 'w-text-grey-600',
+      cssVariable: '--w-color-grey-600',
       usage: 'Body copy, user content',
       contrastText: 'white',
     },
     400: {
       hex: '#5C5C5C',
+      hsl: 'hsl(0, 0%, 36%)',
       bgUtility: 'w-bg-grey-400',
       textUtility: 'w-text-grey-400',
+      cssVariable: '--w-color-grey-400',
       usage: 'Help text, placeholders, meta text, neutral state indicators',
       contrastText: 'white',
     },
     200: {
       hex: '#929292',
+      hsl: 'hsl(0, 0%, 57%)',
       bgUtility: 'w-bg-grey-200',
       textUtility: 'w-text-grey-200',
+      cssVariable: '--w-color-grey-200',
       usage: 'Dividers, button borders',
       contrastText: 'primary',
     },
     100: {
       hex: '#E0E0E0',
+      hsl: 'hsl(0, 0%, 88%)',
       bgUtility: 'w-bg-grey-100',
       textUtility: 'w-text-grey-100',
+      cssVariable: '--w-color-grey-100',
       usage: 'Dividers, field borders, panel borders',
       contrastText: 'primary',
     },
     50: {
       hex: '#F6F6F8',
+      hsl: 'hsl(240, 12%, 97%)',
       bgUtility: 'w-bg-grey-50',
       textUtility: 'w-text-grey-50',
+      cssVariable: '--w-color-grey-50',
       usage: 'Background for panels, row highlights',
       contrastText: 'primary',
     },
@@ -65,61 +77,77 @@ const colors = {
   white: {
     DEFAULT: {
       hex: '#FFFFFF',
+      hsl: 'hsl(0, 0%, 100%)',
       bgUtility: 'w-bg-white',
       textUtility: 'w-text-white',
+      cssVariable: '--w-color-white',
       usage: 'Page backgrounds, Panels, Button text',
       contrastText: 'primary',
     },
   },
-  teal: {
+  secondary: {
     600: {
       hex: '#004345',
-      bgUtility: 'w-bg-teal-600',
-      textUtility: 'w-text-teal-600',
+      hsl: 'hsl(182, 100%, 14%)',
+      bgUtility: 'w-bg-secondary-600',
+      textUtility: 'w-text-secondary-600',
+      cssVariable: '--w-color-secondary-600',
       usage: 'Hover states for two-tone buttons',
       contrastText: 'white',
     },
     400: {
       hex: '#005B5E',
-      bgUtility: 'w-bg-teal-400',
-      textUtility: 'w-text-teal-400',
+      hsl: 'hsl(182, 100%, 18%)',
+      bgUtility: 'w-bg-secondary-400',
+      textUtility: 'w-text-secondary-400',
+      cssVariable: '--w-color-secondary-400',
       usage: 'Two-tone buttons, hover states',
       contrastText: 'white',
     },
-    200: {
+    DEFAULT: {
       hex: '#007D7E',
-      bgUtility: 'w-bg-teal-200',
-      textUtility: 'w-text-teal-200',
+      hsl: 'hsl(180, 100%, 25%)',
+      bgUtility: 'w-bg-secondary',
+      textUtility: 'w-text-secondary',
+      cssVariable: '--w-color-secondary',
       usage: 'Primary buttons, action links',
       contrastText: 'white',
     },
     100: {
       hex: '#00B0B1',
-      bgUtility: 'w-bg-teal-100',
-      textUtility: 'w-text-teal-100',
+      hsl: 'hsl(180, 100%, 35%)',
+      bgUtility: 'w-bg-secondary-100',
+      textUtility: 'w-text-secondary-100',
+      cssVariable: '--w-color-secondary-100',
       usage: 'UI element highlights',
       contrastText: 'white',
     },
     50: {
       hex: '#F2FCFC',
-      bgUtility: 'w-bg-teal-50',
-      textUtility: 'w-text-teal-50',
+      hsl: 'hsl(180, 63%, 97%)',
+      bgUtility: 'w-bg-secondary-50',
+      textUtility: 'w-text-secondary-50',
+      cssVariable: '--w-color-secondary-50',
       usage: 'Button backgrounds, highlighted fields background',
-      contrastText: 'teal-200',
+      contrastText: 'secondary',
     },
   },
   primary: {
     DEFAULT: {
       hex: '#2E1F5E',
+      hsl: 'hsl(254, 50%, 25%)',
       bgUtility: 'w-bg-primary',
       textUtility: 'w-text-primary',
+      cssVariable: '--w-color-primary',
       usage: 'Wagtail branding, Panels, Headings, Buttons, Labels',
       contrastText: 'white',
     },
     200: {
       hex: '#261A4E',
+      hsl: 'hsl(254, 50%, 20%)',
       bgUtility: 'w-bg-primary-200',
       textUtility: 'w-text-primary-200',
+      cssVariable: '--w-color-primary-200',
       usage:
         'Accent for elements used in conjunction with primary colour in sidebar',
       contrastText: 'white',
@@ -128,15 +156,19 @@ const colors = {
   info: {
     100: {
       hex: '#1F7E9A',
+      hsl: 'hsl(194, 66%, 36%)',
       bgUtility: 'w-bg-info-100',
       textUtility: 'w-text-info-100',
+      cssVariable: '--w-color-info-100',
       usage: 'Background and icons for information messages',
       contrastText: 'white',
     },
     50: {
       hex: '#E2F5FC',
+      hsl: 'hsl(196, 81%, 94%)',
       bgUtility: 'w-bg-info-50',
       textUtility: 'w-text-info-50',
+      cssVariable: '--w-color-info-50',
       usage: 'Background only, for information messages',
       contrastText: 'primary',
     },
@@ -144,15 +176,19 @@ const colors = {
   positive: {
     100: {
       hex: '#1B8666',
+      hsl: 'hsl(162, 66%, 32%)',
       bgUtility: 'w-bg-positive-100',
       textUtility: 'w-text-positive-100',
+      cssVariable: '--w-color-positive-100',
       usage: 'Positive states',
       contrastText: 'white',
     },
     50: {
       hex: '#E0FBF4',
+      hsl: 'hsl(164, 77%, 93%)',
       bgUtility: 'w-bg-positive-50',
       textUtility: 'w-text-positive-50',
+      cssVariable: '--w-color-positive-50',
       usage: 'Background only, for positive states',
       contrastText: 'primary',
     },
@@ -160,15 +196,19 @@ const colors = {
   warning: {
     100: {
       hex: '#FAA500',
+      hsl: 'hsl(40, 100%, 49%)',
       bgUtility: 'w-bg-warning-100',
       textUtility: 'w-text-warning-100',
+      cssVariable: '--w-color-warning-100',
       usage: 'Background and icons for potentially dangerous states',
       contrastText: 'primary',
     },
     50: {
       hex: '#FAECD5',
+      hsl: 'hsl(37, 79%, 91%)',
       bgUtility: 'w-bg-warning-50',
       textUtility: 'w-text-warning-50',
+      cssVariable: '--w-color-warning-50',
       usage: 'Background only, for potentially dangerous states',
       contrastText: 'primary',
     },
@@ -176,22 +216,28 @@ const colors = {
   critical: {
     200: {
       hex: '#CD4444',
+      hsl: 'hsl(0, 58%, 54%)',
       bgUtility: 'w-bg-critical-200',
       textUtility: 'w-text-critical-200',
+      cssVariable: '--w-color-critical-200',
       usage: 'Dangerous actions or states (over light background), errors',
       contrastText: 'white',
     },
     100: {
       hex: '#FD5765',
+      hsl: 'hsl(355, 98%, 67%)',
       bgUtility: 'w-bg-critical-100',
       textUtility: 'w-text-critical-100',
+      cssVariable: '--w-color-critical-100',
       usage: 'Dangerous actions or states (over dark background)',
       contrastText: 'primary',
     },
     50: {
       hex: '#FDE9E9',
+      hsl: 'hsl(0, 83%, 95%)',
       bgUtility: 'w-bg-critical-50',
       textUtility: 'w-text-critical-50',
+      cssVariable: '--w-color-critical-50',
       usage: 'Background only, for dangerous states',
       contrastText: 'primary',
     },

+ 21 - 6
client/tailwind.config.js

@@ -4,6 +4,7 @@ const vanillaRTL = require('tailwindcss-vanilla-rtl');
  * Design Tokens
  */
 const colors = require('./src/tokens/colors');
+const { generateColorVariables } = require('./src/tokens/colorVariables');
 const {
   fontFamily,
   fontSize,
@@ -32,7 +33,10 @@ const scrollbarThin = require('./src/plugins/scrollbarThin');
 const themeColors = Object.fromEntries(
   Object.entries(colors).map(([key, hues]) => {
     const shades = Object.fromEntries(
-      Object.entries(hues).map(([k, shade]) => [k, shade.hex]),
+      Object.entries(hues).map(([k, shade]) => [
+        k,
+        `var(${shade.cssVariable})`,
+      ]),
     );
     return [key, shades];
   }),
@@ -49,12 +53,22 @@ module.exports = {
     },
     colors: {
       ...themeColors,
-      inherit: 'inherit',
-      current: 'currentColor',
-      transparent: 'transparent',
+      // Fades of white and black are not customisable.
+      'white-15': 'rgba(255, 255, 255, 0.15)',
+      'white-50': 'rgba(255, 255, 255, 0.50)',
+      'white-80': 'rgba(255, 255, 255, 0.80)',
+      'white-85': 'rgba(255, 255, 255, 0.85)',
+      'black-10': 'rgba(0, 0, 0, 0.10)',
+      'black-20': 'rgba(0, 0, 0, 0.20)',
+      'black-35': 'rgba(0, 0, 0, 0.35)',
+      'black-50': 'rgba(0, 0, 0, 0.50)',
+      // Color keywords.
+      'inherit': 'inherit',
+      'current': 'currentColor',
+      'transparent': 'transparent',
       /* allow system colours https://www.w3.org/TR/css-color-4/#css-system-colors */
-      LinkText: 'LinkText',
-      ButtonText: 'ButtonText',
+      'LinkText': 'LinkText',
+      'ButtonText': 'ButtonText',
     },
     fontFamily: {
       sans: 'var(--w-font-sans)',
@@ -135,6 +149,7 @@ module.exports = {
         ':root': {
           '--w-font-sans': fontFamily.sans.join(', '),
           '--w-font-mono': fontFamily.mono.join(', '),
+          ...generateColorVariables(colors),
         },
       });
     }),