diff --git a/CONTRIBUTORS.rst b/CONTRIBUTORS.rst
index c5613c920e..4d63a21fa8 100644
--- a/CONTRIBUTORS.rst
+++ b/CONTRIBUTORS.rst
@@ -492,6 +492,7 @@ Contributors
* Bohreromir
* Fernando Cordeiro
* Matthias Rohmer
+* Joshua Marantz
Translators
===========
diff --git a/client/scss/_tools.scss b/client/scss/_tools.scss
index 3fa1ccd6d8..3bd20aa5ca 100644
--- a/client/scss/_tools.scss
+++ b/client/scss/_tools.scss
@@ -8,3 +8,4 @@ No CSS should be produced by these files.
@import 'tools/mixins.fonts';
@import 'tools/mixins.general';
@import 'tools/mixins.grid';
+@import 'tools/various.colors';
diff --git a/client/scss/components/_listing.scss b/client/scss/components/_listing.scss
index 254b07fbb3..e7196c6748 100644
--- a/client/scss/components/_listing.scss
+++ b/client/scss/components/_listing.scss
@@ -414,7 +414,6 @@ ul.listing {
&.bicolor {
background: $color-teal-darker;
- border: solid 1px darken($color-teal-darker, 10%);
&:active {
color: $color-white;
diff --git a/client/scss/components/_tabs.scss b/client/scss/components/_tabs.scss
index 64d035ed27..6302492705 100644
--- a/client/scss/components/_tabs.scss
+++ b/client/scss/components/_tabs.scss
@@ -37,7 +37,7 @@
&:hover {
color: $color-white;
- border-top-color: darken($color-teal-darker, 8);
+ border-top-color: rgba(0, 0, 0, 0.35);
}
}
diff --git a/client/scss/elements/_root.scss b/client/scss/elements/_root.scss
new file mode 100644
index 0000000000..7c6bf55f30
--- /dev/null
+++ b/client/scss/elements/_root.scss
@@ -0,0 +1,8 @@
+:root {
+ @include define-color('color-primary', #007d7e);
+ @include define-color('color-primary-darker', css-darken(css-adjust-hue(get-color('color-primary'), 1), 4%));
+ @include define-color('color-primary-dark', css-darken(css-adjust-hue(get-color('color-primary'), 1), 7%));
+
+ @include define-color('color-input-focus', css-lighten(css-desaturate(get-color('color-primary'), 40%), 72%));
+ @include define-color('color-input-focus-border', css-lighten(css-saturate(get-color('color-primary'), 12%), 10%));
+}
diff --git a/client/scss/settings/_variables.scss b/client/scss/settings/_variables.scss
index f78ac1ea6d..30fd84a412 100644
--- a/client/scss/settings/_variables.scss
+++ b/client/scss/settings/_variables.scss
@@ -28,9 +28,9 @@ $breakpoints: (
);
// colours
-$color-teal: #007d7e;
-$color-teal-darker: darken(adjust-hue($color-teal, 1), 4);
-$color-teal-dark: darken(adjust-hue($color-teal, 1), 7);
+$color-teal: var(--color-primary);
+$color-teal-darker: var(--color-primary-darker);
+$color-teal-dark: var(--color-primary-dark);
$color-blue: #71b2d4;
$color-red: #cd3238;
@@ -59,8 +59,8 @@ $color-header-bg: $color-teal;
$color-fieldset-hover: $color-grey-5;
$color-input-border: $color-grey-4;
-$color-input-focus: lighten(desaturate($color-teal, 40), 72);
-$color-input-focus-border: lighten(saturate($color-teal, 12), 10);
+$color-input-focus: var(--color-input-focus);
+$color-input-focus-border: var(--color-input-focus-border);
$color-input-error-bg: lighten(saturate($color-red, 28), 45);
$color-button: $color-teal;
diff --git a/client/scss/styles.scss b/client/scss/styles.scss
index ddf4c8e1a0..1a23584d13 100644
--- a/client/scss/styles.scss
+++ b/client/scss/styles.scss
@@ -74,6 +74,7 @@ These are base styles for bare HTML elements.
@import 'elements/elements';
@import 'elements/typography';
@import 'elements/forms';
+@import 'elements/root';
/* OBJECTS
diff --git a/client/scss/tools/_various.colors.scss b/client/scss/tools/_various.colors.scss
new file mode 100644
index 0000000000..37183e4486
--- /dev/null
+++ b/client/scss/tools/_various.colors.scss
@@ -0,0 +1,62 @@
+// $color is either a color or an hsl tuple
+@mixin define-color($name, $color) {
+ $h: null;
+ $s: null;
+ $l: null;
+
+ @if type-of($color) == color {
+ $h: hue($color) / 1deg; // Cast to unitless
+ $s: saturation($color);
+ $l: lightness($color);
+ } @else {
+ $h: nth($color, 1);
+ $s: nth($color, 2);
+ $l: nth($color, 3);
+ }
+
+ --#{$name}-hue: #{$h};
+ --#{$name}-saturation: #{$s};
+ --#{$name}-lightness: #{$l};
+ --#{$name}: hsl(#{ var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness) });
+}
+
+@function get-color($name) {
+ @return (var(--#{$name}-hue), var(--#{$name}-saturation), var(--#{$name}-lightness));
+}
+
+@function css-darken($hsl-tuple, $darken-by) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return ($h, $s, calc(#{$l} - #{$darken-by + 0%}));
+}
+@function css-lighten($hsl-tuple, $lighten-by) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return ($h, $s, calc(#{$l} + #{$lighten-by + 0%}));
+}
+@function css-saturate($hsl-tuple, $saturate-by) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return ($h, calc(#{$s} + #{$saturate-by + 0%}), $l);
+}
+@function css-desaturate($hsl-tuple, $desaturate-by) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return ($h, calc(#{$s} - #{$desaturate-by + 0%}), $l);
+}
+@function css-adjust-hue($hsl-tuple, $adjust-by) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return (calc(#{$h} + #{$adjust-by}), $s, $l);
+}
+@function css-transparentize($hsl-tuple, $alpha) {
+ $h: nth($hsl-tuple, 1);
+ $s: nth($hsl-tuple, 2);
+ $l: nth($hsl-tuple, 3);
+ @return ($h, $s, $l, $alpha);
+}
diff --git a/client/src/components/StreamField/StreamField.scss b/client/src/components/StreamField/StreamField.scss
index f52eee75b1..fa2e88684b 100644
--- a/client/src/components/StreamField/StreamField.scss
+++ b/client/src/components/StreamField/StreamField.scss
@@ -2,7 +2,10 @@ $header-padding-vertical: 6px;
$action-font-size: 18px;
-@import '../../../../node_modules/react-streamfield/src/scss/index';
+@use '../../../../node_modules/react-streamfield/src/scss/index' with (
+ $teal: $color-teal,
+ $error-color: $color-red,
+);
.c-sf-container {
diff --git a/docs/advanced_topics/customisation/admin_templates.rst b/docs/advanced_topics/customisation/admin_templates.rst
index 17ad37a685..a793477962 100644
--- a/docs/advanced_topics/customisation/admin_templates.rst
+++ b/docs/advanced_topics/customisation/admin_templates.rst
@@ -91,6 +91,53 @@ To replace the welcome message on the dashboard, create a template file ``dashbo
{% block branding_welcome %}Welcome to Frank's Site{% endblock %}
+.. _custom_user_interface_colors:
+
+Custom user interface colors
+============================
+
+
+.. warning::
+ CSS variables are not supported in Internet Explorer, so the admin will appear with the default colors when viewed in that browser.
+
+ The default Wagtail colors conform to the WCAG2.1 AA level color contrast requirements. When customizing the admin colors you should test the contrast using tools like `Axe `_.
+
+To customize the primary color used in the admin user interface, inject a CSS file using the hook :ref:`insert_global_admin_css` and override the variables within the ``:root`` selector:
+
+.. code-block:: text
+
+ :root {
+ --color-primary-hue: 25;
+ }
+
+``color-primary`` is an `hsl color `_ composed of 3 CSS variables - ``--color-primary-hue`` (0-360 with no unit), ``--color-primary-saturation`` (a percentage), and ``--color-primary-lightness`` (also a percentage). Separating the color into 3 allows us to calculate variations on the color to use alongside the primary color. If needed, you can also control those variations manually by setting ``hue``, ``saturation``, and ``lightness`` variables for the following colors: ``color-primary-darker``, ``color-primary-dark``, ``color-input-focus``, and ``color-input-focus-border``:
+
+.. code-block:: text
+
+ :root {
+ --color-primary-hue: 25;
+ --color-primary-saturation: 100%;
+ --color-primary-lightness: 25%;
+ --color-primary-darker-hue: 24;
+ --color-primary-darker-saturation: 100%;
+ --color-primary-darker-lightness: 20%;
+ --color-primary-dark-hue: 23;
+ --color-primary-dark-saturation: 100%;
+ --color-primary-dark-lightness: 15%;
+ }
+
+If instead you intend to set all available colors, you can use any valid css colors:
+
+.. code-block:: text
+
+ :root {
+ --color-primary: mediumaquamarine;
+ --color-primary-darker: rebeccapurple;
+ --color-primary-dark: hsl(330, 100%, 70%);
+ --color-input-focus: rgb(204, 0, 102);
+ --color-input-focus-border: #4d0026;
+ }
+
Specifying a site or page in the branding
=========================================
diff --git a/docs/releases/2.12.rst b/docs/releases/2.12.rst
index 4166148111..5ddda869d7 100644
--- a/docs/releases/2.12.rst
+++ b/docs/releases/2.12.rst
@@ -21,6 +21,10 @@ In-place StreamField updating
StreamField values now formally support being updated in-place from Python code, allowing blocks to be inserted, modified and deleted rather than having to assign a new list of blocks to the field. For further details, see :ref:`modifying_streamfield_data`. This feature was developed by Matt Westcott.
+Admin color themes
+~~~~~~~~~~~~~~~~~~
+
+Wagtail’s admin now uses CSS custom properties for its primary teal color. Applying brand colors for the whole user interface only takes a few lines of CSS, and third-party extensions can reuse Wagtail’s CSS variables to support the same degree of customization. Read on :ref:`custom_user_interface_colors`. This feature was developed by Joshua Marantz.
Other features
~~~~~~~~~~~~~~
diff --git a/gulpfile.js/tasks/styles.js b/gulpfile.js/tasks/styles.js
index ee322b263f..5f86619088 100644
--- a/gulpfile.js/tasks/styles.js
+++ b/gulpfile.js/tasks/styles.js
@@ -4,6 +4,8 @@ var sass = require('gulp-dart-sass');
var postcss = require('gulp-postcss');
var autoprefixer = require('autoprefixer');
var cssnano = require('cssnano');
+var postcssCustomProperties = require('postcss-custom-properties');
+var postcssCalc = require('postcss-calc');
var sourcemaps = require('gulp-sourcemaps');
var size = require('gulp-size');
var config = require('../config');
@@ -69,6 +71,8 @@ gulp.task('styles:sass', function () {
.pipe(postcss([
cssnano(cssnanoConfig),
autoprefixer(autoprefixerConfig),
+ postcssCustomProperties(),
+ postcssCalc(),
]))
.pipe(size({ title: 'Wagtail CSS' }))
.pipe(config.isProduction ? gutil.noop() : sourcemaps.write())
diff --git a/package-lock.json b/package-lock.json
index ef0ddfafdc..bd154222bf 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3499,8 +3499,7 @@
"cssesc": {
"version": "3.0.0",
"resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz",
- "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==",
- "dev": true
+ "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="
},
"cssnano": {
"version": "4.1.10",
@@ -7308,8 +7307,7 @@
"indexes-of": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/indexes-of/-/indexes-of-1.0.1.tgz",
- "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc=",
- "dev": true
+ "integrity": "sha1-8w9xbI4r00bHtn0985FVZqfAVgc="
},
"inflight": {
"version": "1.0.6",
@@ -7654,6 +7652,12 @@
"unc-path-regex": "^0.1.2"
}
},
+ "is-url-superb": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/is-url-superb/-/is-url-superb-4.0.0.tgz",
+ "integrity": "sha512-GI+WjezhPPcbM+tqE9LnmsY5qqjwHzTvjJ36wxYX5ujNXefSUJ/T17r5bqDV8yLhcgB59KTPNOc9O9cmHTPWsA==",
+ "dev": true
+ },
"is-utf8": {
"version": "0.2.1",
"resolved": "https://registry.npmjs.org/is-utf8/-/is-utf8-0.2.1.tgz",
@@ -11778,7 +11782,6 @@
"version": "7.0.35",
"resolved": "https://registry.npmjs.org/postcss/-/postcss-7.0.35.tgz",
"integrity": "sha512-3QT8bBJeX/S5zKTTjTCIjRF3If4avAT6kqxcASlTWEtAFCb9NH0OUxNDfgZSWdP5fJnBYCMEWkIFfWeugjzYMg==",
- "dev": true,
"requires": {
"chalk": "^2.4.2",
"source-map": "^0.6.1",
@@ -11788,14 +11791,12 @@
"source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
- "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
- "dev": true
+ "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="
},
"supports-color": {
"version": "6.1.0",
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-6.1.0.tgz",
"integrity": "sha512-qe1jfm1Mg7Nq/NSh6XE24gPXROEVsWHxC1LIx//XNlD9iw7YZQGjZNjYN7xGaEG6iKdA8EtNFW6R0gjnVXp+wQ==",
- "dev": true,
"requires": {
"has-flag": "^3.0.0"
}
@@ -11803,10 +11804,9 @@
}
},
"postcss-calc": {
- "version": "7.0.2",
- "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.2.tgz",
- "integrity": "sha512-rofZFHUg6ZIrvRwPeFktv06GdbDYLcGqh9EwiMutZg+a0oePCCw1zHOEiji6LCpyRcjTREtPASuUqeAvYlEVvQ==",
- "dev": true,
+ "version": "7.0.5",
+ "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-7.0.5.tgz",
+ "integrity": "sha512-1tKHutbGtLtEZF6PT4JSihCHfIVldU72mZ8SdZHIYriIZ9fh9k9aWSppaT8rHsyI3dX+KSR+W+Ix9BMY3AODrg==",
"requires": {
"postcss": "^7.0.27",
"postcss-selector-parser": "^6.0.2",
@@ -11852,6 +11852,16 @@
}
}
},
+ "postcss-custom-properties": {
+ "version": "10.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-10.0.0.tgz",
+ "integrity": "sha512-55BPj5FudpCiPZzBaO+MOeqmwMDa+nV9/0QBJBfhZjYg6D9hE+rW9lpMBLTJoF4OTXnS5Po4yM1nMlgkPbCxFg==",
+ "dev": true,
+ "requires": {
+ "postcss": "^7.0.17",
+ "postcss-values-parser": "^4.0.0"
+ }
+ },
"postcss-discard-comments": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-4.0.2.tgz",
@@ -12355,7 +12365,6 @@
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.2.tgz",
"integrity": "sha512-36P2QR59jDTOAiIkqEprfJDsoNrvwFei3eCqKd1Y0tUsBimsq39BLp7RD+JWny3WgB1zGhJX8XVePwm9k4wdBg==",
- "dev": true,
"requires": {
"cssesc": "^3.0.0",
"indexes-of": "^1.0.1",
@@ -12402,8 +12411,26 @@
"postcss-value-parser": {
"version": "4.1.0",
"resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.1.0.tgz",
- "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ==",
- "dev": true
+ "integrity": "sha512-97DXOFbQJhk71ne5/Mt6cOu6yxsSfM0QGQyl0L25Gca4yGWEGJaig7l7gbCX623VqTBNGLRLaVUCnNkcedlRSQ=="
+ },
+ "postcss-values-parser": {
+ "version": "4.0.0",
+ "resolved": "https://registry.npmjs.org/postcss-values-parser/-/postcss-values-parser-4.0.0.tgz",
+ "integrity": "sha512-R9x2D87FcbhwXUmoCXJR85M1BLII5suXRuXibGYyBJ7lVDEpRIdKZh4+8q5S+/+A4m0IoG1U5tFw39asyhX/Hw==",
+ "dev": true,
+ "requires": {
+ "color-name": "^1.1.4",
+ "is-url-superb": "^4.0.0",
+ "postcss": "^7.0.5"
+ },
+ "dependencies": {
+ "color-name": {
+ "version": "1.1.4",
+ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
+ "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==",
+ "dev": true
+ }
+ }
},
"prelude-ls": {
"version": "1.2.1",
@@ -15389,8 +15416,7 @@
"uniq": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/uniq/-/uniq-1.0.1.tgz",
- "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8=",
- "dev": true
+ "integrity": "sha1-sxxa6CVIRKOoKBVBzisEuGWnNP8="
},
"uniqs": {
"version": "2.0.0",
diff --git a/package.json b/package.json
index d84b80ab07..307abf8e44 100644
--- a/package.json
+++ b/package.json
@@ -73,6 +73,7 @@
"gulp-util": "~3.0.8",
"jest": "^26.6.0",
"npm-run-all": "^4.1.5",
+ "postcss-custom-properties": "^10.0.0",
"react-axe": "^3.1.0",
"react-test-renderer": "^16.13.1",
"redux-mock-store": "^1.3.0",
@@ -89,6 +90,7 @@
"draftail": "^1.2.1",
"element-closest": "^2.0.2",
"focus-trap-react": "^3.1.0",
+ "postcss-calc": "^7.0.5",
"prop-types": "^15.6.2",
"react": "^16.4.0",
"react-dom": "^16.4.0",
diff --git a/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss b/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss
index b2283153f9..d0a4fd2aa9 100644
--- a/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss
+++ b/wagtail/admin/static_src/wagtailadmin/scss/panels/streamfield.scss
@@ -1 +1,2 @@
+@import 'wagtailadmin/scss/helpers';
@import '../../../../../../client/src/components/StreamField/StreamField';