Using color-scheme and light-dark() to implement dark mode (#154)

* Lint fixes.

* Implement light-dark().

* Lint css with biome, remove stylelint.

* Biome lint fixes.

* Dark mode adjustments.

* Style bug fixes.

* Colour adjustments.

* Move header and footer styles to base.

* Site header adjustments.

* Removing jQuery and umbrella libs. They where great but not needed anymore.

* Add colorScheme to optionally lock light/dark.

* Update README.

* Biome adjustments.
This commit is contained in:
Fredrik Jonsson 2026-05-28 08:18:04 +02:00 • committed by GitHub
commit 16c47eea9d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
30 changed files with 276 additions and 2130 deletions

View file

@ -1,97 +1,152 @@
/* biome-ignore-all format: aligned columns for readability */
/* Theme colours. */
/* Override css variables in "assets/css/_custom.css" */
:root {
/* Brand colours. */
--color-brand: var(--color-grey-900);
--color-brand-alt: var(--color-grey-500);
/* Brand variations */
--color-brand-900: oklch(from var(--color-brand) calc(l - 0.5) c h);
--color-brand-800: oklch(from var(--color-brand) calc(l - 0.4) c h);
--color-brand-700: oklch(from var(--color-brand) calc(l - 0.3) c h);
--color-brand-600: oklch(from var(--color-brand) calc(l - 0.2) c h);
--color-brand-500: oklch(from var(--color-brand) calc(l - 0.1) c h);
--color-brand-400: oklch(from var(--color-brand) l c h);
--color-brand-300: oklch(from var(--color-brand) calc(l + 0.1) c h);
--color-brand-200: oklch(from var(--color-brand) calc(l + 0.2) c h);
--color-brand-100: oklch(from var(--color-brand) calc(l + 0.3) c h);
/* Brand alt variations */
--color-brand-alt-900: oklch(from var(--color-brand-alt) calc(l - 0.5) c h);
--color-brand-alt-800: oklch(from var(--color-brand-alt) calc(l - 0.4) c h);
--color-brand-alt-700: oklch(from var(--color-brand-alt) calc(l - 0.3) c h);
--color-brand-alt-600: oklch(from var(--color-brand-alt) calc(l - 0.2) c h);
--color-brand-alt-500: oklch(from var(--color-brand-alt) calc(l - 0.1) c h);
--color-brand-alt-400: oklch(from var(--color-brand-alt) l c h);
--color-brand-alt-300: oklch(from var(--color-brand-alt) calc(l + 0.1) c h);
--color-brand-alt-200: oklch(from var(--color-brand-alt) calc(l + 0.2) c h);
--color-brand-alt-100: oklch(from var(--color-brand-alt) calc(l + 0.3) c h);
/* Base colours. */
--color-black: oklch(1% 0.01 0);
--color-nearblack: oklch(25% 0.01 0);
--color-grey-700: oklch(38% 0 0);
--color-grey-600: oklch(49% 0 0);
--color-grey-500: oklch(60% 0 0);
--color-grey-400: oklch(71% 0 0);
--color-grey-300: oklch(80% 0 0);
--color-grey-200: oklch(89% 0 0);
--color-grey-900: oklch(21% 0.01 0);
--color-grey-800: oklch(28% 0.01 0);
--color-grey-700: oklch(37% 0.01 0);
--color-grey-600: oklch(45% 0.01 0);
--color-grey-500: oklch(55% 0.01 0);
--color-grey-400: oklch(71% 0.01 0);
--color-grey-300: oklch(87% 0.01 0);
--color-grey-200: oklch(93% 0.01 0);
--color-grey-100: oklch(95% 0 0);
--color-nearwhite: oklch(98% 0 0);
--color-white: oklch(100% 0 0);
--color-white: oklch(99% 0 0);
--color-red: oklch(65% 0.2 24);
--color-yellow: oklch(65% 0.12 80);
--color-green: oklch(65% 0.19 138);
--color-blue: oklch(65% 0.16 247);
/* Transparent colours. */
--color-black-60: oklch(0% 0 0 / 0.6);
--color-black-40: oklch(0% 0 0 / 0.4);
--color-black-20: oklch(0% 0 0 / 0.2);
--color-black-10: oklch(0% 0 0 / 0.1);
--color-white-20: oklch(100% 0 0 / 0.2);
--color-white-10: oklch(100% 0 0 / 0.1);
/* Brand colours. */
--color-brand: var(--color-nearblack);
--color-brand-alt: var(--color-grey-500);
--color-brand-light: var(--color-grey-300);
--color-brand-dark: var(--color-grey-700);
--color-black-90: oklch(1% 0.01 0 / 0.9);
--color-black-80: oklch(1% 0.01 0 / 0.8);
--color-black-70: oklch(1% 0.01 0 / 0.7);
--color-black-60: oklch(1% 0.01 0 / 0.6);
--color-black-50: oklch(1% 0.01 0 / 0.5);
--color-black-40: oklch(1% 0.01 0 / 0.4);
--color-black-30: oklch(1% 0.01 0 / 0.3);
--color-black-20: oklch(1% 0.01 0 / 0.2);
--color-black-10: oklch(1% 0.01 0 / 0.1);
--color-white-20: oklch(99% 0 0 / 0.2);
--color-white-10: oklch(99% 0 0 / 0.1);
/* Text colours. */
--color-text: var(--color-black);
--color-text-bg: var(--color-white);
--color-text-meta: var(--color-grey-600);
--color-headings: var(--color-nearblack);
--color-code: var(--color-nearblack);
--color-text: light-dark(var(--color-black), var(--color-white));
--color-text-bg: light-dark(var(--color-white), var(--color-black));
--color-text-meta: light-dark(var(--color-grey-600), var(--color-grey-300));
--color-headings: light-dark(var(--color-grey-900), var(--color-grey-100));
--color-code: light-dark(var(--color-grey-900), var(--color-grey-100));
/* Link colours. */
--color-link: var(--color-brand);
--color-link-visited: var(--color-brand);
--color-link-hover: var(--color-brand-alt);
--color-link-active: var(--color-red);
--color-link-inverted: var(--color-white);
--color-link-visited-inverted: var(--color-white);
--color-link-hover-inverted: var(--color-white);
--color-link-active-inverted: var(--color-red);
--color-link: light-dark(var(--color-brand-500), var(--color-grey-100));
--color-link-visited: light-dark(var(--color-brand-500), var(--color-grey-100));
--color-link-hover: light-dark(var(--color-brand-200), var(--color-brand-400));
--color-link-active: light-dark(var(--color-red), var(--color-red));
/* Button colours. */
--color-button: var(--color-brand);
--color-button-hover: var(--color-brand-alt);
--color-button-text: var(--color-white);
--color-button-text-hover: var(--color-white);
--color-button-disabled: var(--color-grey-400);
--color-button: light-dark(var(--color-brand-400), var(--color-brand-100));
--color-button-text: light-dark(var(--color-white), var(--color-white));
--color-button-hover: light-dark(var(--color-brand-200), var(--color-brand-300));
--color-button-text-hover: light-dark(var(--color-white), var(--color-white));
/* Disabled colours. */
--color-disabled: light-dark(var(--color-grey-400), var(--color-grey-600));
--color-disabled-alt: light-dark(var(--color-grey-100), var(--color-grey-800));
/* Border colours. */
--color-border: var(--color-brand);
--color-border-light: var(--color-brand-light);
--color-border-dark: var(--color-brand-dark);
--color-border: light-dark(var(--color-brand-400), var(--color-brand-200));
--color-border-light: light-dark(var(--color-brand-200), var(--color-brand-400));
--color-border-dark: light-dark(var(--color-brand-600), var(--color-brand-300));
/* Background colours. */
--color-header-bg: var(--color-white);
--color-body-bg: var(--color-white);
--color-footer-bg: var(--color-white);
--color-backdrop: var(--color-grey-100);
--color-body-bg: light-dark(var(--color-white), var(--color-black));
--color-page-bg: light-dark(transparent, transparent);
--color-header-bg: light-dark(transparent, transparent);
--color-footer-bg: light-dark(transparent, transparent);
--color-backdrop: light-dark(var(--color-black-60), var(--color-black-80));
/* Mobil menu colours. */
--color-mobile-menu: var(--color-brand);
--color-mobile-menu-cover: var(--color-black-20);
--color-mobile-menu: light-dark(var(--color-brand-400), var(--color-brand-400));
--color-mobile-menu-cover: light-dark(var(--color-black-20), var(--color-black-20));
/* Form colours. */
--color-accent: var(--color-brand);
--color-accent: light-dark(var(--color-brand-400), var(--color-brand-200));
/* Row (e.g. in table) colours. */
--color-row-header: transparent;
--color-row-odd: var(--color-nearwhite);
--color-row-even: transparent;
--color-row-header: light-dark(transparent, transparent);
--color-row-odd: light-dark(var(--color-grey-100), var(--color-grey-900));
--color-row-even: light-dark(transparent, transparent);
/* Highlight colours. */
--color-highlight: light-dark(var(--color-brand-400), var(--color-brand-100));
--color-mark-highlight: light-dark(var(--color-red), var(--color-red));
/* Status colours. */
--color-info: var(--color-blue);
--color-info-bg: oklch(from var(--color-blue) 100% 0.03 h);
--color-success: var(--color-green);
--color-success-bg: oklch(from var(--color-green) 100% 0.03 h);
--color-success: var(--color-green);
--color-warning: var(--color-yellow);
--color-warning-bg: oklch(from var(--color-yellow) 100% 0.03 h);
--color-error: var(--color-red);
--color-error-bg: oklch(from var(--color-red) 100% 0.03 h);
/* Highlight colours. */
--color-highlight: var(--color-brand-alt);
--color-mark-highlight: var(--color-red);
/* Follow user system dark/light setting. */
color-scheme: light dark;
/* Initialize the theme variable */
--theme: light;
@media (prefers-color-scheme: dark) {
--theme: dark;
}
}
[data-theme="light"] {
color-scheme: light;
}
[data-theme="dark"] {
color-scheme: dark;
}
@container style(--theme: light) {
[data-theme="inverted"] {
color-scheme: dark;
}
}
@container style(--theme: dark) {
[data-theme="inverted"] {
color-scheme: light;
}
}

View file

@ -1,3 +1,5 @@
/* biome-ignore-all format: aligned columns for readability */
:root {
/* Set theme css variables.
Override css variables in "assets/css/_custom.css" */
@ -39,8 +41,6 @@
monospace) hints at what type of font to use if the web browser doesn't
find any of the fonts in the list. */
/* stylelint-disable value-keyword-case */
/* Emojis font stack. Add to end of other stacks when needed. */
--emojis: 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji';
@ -69,7 +69,6 @@
/* Monospace font stacks. */
--ui-monospace: ui-monospace, 'Cascadia Code', 'Source Code Pro', Menlo, Consolas, 'DejaVu Sans Mono', monospace;
/* stylelint-enable value-keyword-case */
/* Font family. */
--ff-body: var(--ui-serif);

View file

@ -2,14 +2,6 @@
/* Page */
body {
background-color: var(--color-backdrop);
}
.page {
background-color: var(--color-body-bg);
}
.footer {
border-block-start: 2px solid var(--color-border);
}

View file

@ -1,21 +1,31 @@
/* Document */
html {
/* Change the default font family in all browsers. */
font-family: var(--ff-body);
/* Set base font size in percent. */
font-size: calc(var(--base-font-size) / 16 * 100%);
/* Set text colour. */
color: var(--color-text);
}
body {
/* Change the default font family in all browsers. */
font-family: var(--ff-body);
/* Correct the line height in all browsers. */
line-height: var(--base-line-height);
/* Set text colour. */
color: var(--color-text);
/* Accent colour for forms. */
accent-color: var(--color-accent);
/* Set background colour. */
background-color: var(--color-body-bg);
}
.page {
background-color: var(--color-page-bg);
}
::backdrop {
background-color: var(--color-backdrop);
}

View file

@ -0,0 +1,11 @@
/* Branding footer */
.footer {
padding-block: var(--gutters);
background-color: var(--color-footer-bg);
text-align: center;
p {
margin: 0;
}
}

View file

@ -16,7 +16,7 @@ label {
/* Display as block if before input. */
&:has(+ input),
&:has(+ textarea){
&:has(+ textarea) {
display: block;
}

View file

@ -0,0 +1,55 @@
/* Branding header */
.header {
--header-gap: var(--spacing-s);
padding-block: min(var(--gutters), var(--spacing-m));
display: flex;
flex-direction: row;
align-items: center;
gap: var(--header-gap);
background-color: var(--color-header-bg);
@media (min-width: 666px) {
flex-direction: row;
align-items: center;
}
}
/* Wrapping link for logo. */
.header__logo {
width: fit-content;
}
/* Logo image. */
.header__logo-image {
vertical-align: bottom;
min-width: 32px;
}
/* The name of the website. */
.header__site-name {
margin: 0;
line-height: 1;
}
/* The link around the name of the website. */
.header__site-link {
&:link,
&:visited {
color: var(--color-text);
text-decoration: none;
}
&:hover,
&:focus {
text-decoration: underline;
}
}
/* Wrapper for any blocks placed in the header region. */
.header__region {
@media (min-width: 666px) {
margin-inline-start: auto;
}
}

View file

@ -8,7 +8,7 @@ h3,
h4,
h5,
h6 {
margin-block: 1.5em .5em;
margin-block: 1.5em 0.5em;
color: var(--color-headings);
font-family: var(--ff-headings);
font-weight: var(--fw-headings);
@ -17,7 +17,7 @@ h6 {
/* Smaller margin when a headeing is right after another heading. */
& + & {
margin-block-start: .75em;
margin-block-start: 0.75em;
}
/* Set max text line width in main for readability. */
@ -31,17 +31,17 @@ h1 {
margin-block-start: var(--spacing-m);
margin-block-end: var(--spacing-xs);
font-size: var(--fs-xxl);
letter-spacing: -.01em;
letter-spacing: -0.01em;
}
h2 {
font-size: var(--fs-xl);
letter-spacing: -.01em;
letter-spacing: -0.01em;
}
h3 {
font-size: var(--fs-l);
letter-spacing: -.01em;
letter-spacing: -0.01em;
}
h4 {

View file

@ -3,7 +3,7 @@
figure,
img {
&.image {
--image-padding: .75rem;
--image-padding: 0.75rem;
@media (min-width: 666px) {
&.center {

View file

@ -55,25 +55,3 @@ a:active {
}
}
}
/* Inverted links, for dark backgrounds. */
.link-inverted {
a {
&:link {
color: var(--color-link-inverted);
}
&:visited {
color: var(--color-link-visited-inverted);
}
&:hover,
&:focus {
color: var(--color-link-hover-inverted);
}
&:active {
color: var(--color-link-active-inverted);
}
}
}

View file

@ -1,8 +1,8 @@
/* Anchor */
.anchor {
--anchor-gap: .5rem;
--anchor-opacity: .4;
--anchor-gap: 0.5rem;
--anchor-opacity: 0.4;
display: grid;
grid-template-columns: auto auto;

View file

@ -33,32 +33,3 @@
.box--gutter {
padding-inline: var(--gutters);
}
.box--inverted {
background: var(--color-border);
color: var(--color-text-bg);
/* Make sure headings etc. also gets inverted. */
* {
color: var(--color-text-bg);
}
a {
&:link {
color: var(--color-link-inverted);
}
&:visited {
color: var(--color-link-visited-inverted);
}
&:hover,
&:focus {
color: var(--color-link-hover-inverted);
}
&:active {
color: var(--color-link-active-inverted);
}
}
}

View file

@ -1,5 +1,3 @@
/* stylelint-disable no-duplicate-selectors */
/* Button */
.button,
@ -11,7 +9,7 @@
text-decoration: none;
text-align: center;
width: fit-content;
padding: .2rem 1rem;
padding: 0.2rem 1rem;
/* Improve usability and consistency of cursor style between image-type `input` and others. */
cursor: pointer;
@ -36,7 +34,7 @@
.button--small {
font-size: var(--fs-xs);
padding: .2rem .75rem;
padding: 0.2rem 0.75rem;
}
.button--alt {
@ -65,7 +63,7 @@
&:active {
color: var(--color-button-text);
background: var(--color-success);
}
}
}
.button--decline {
@ -98,18 +96,19 @@ other variations. */
[type='button'],
[type='submit'] {
&:disabled {
background-color: var(--color-grey-200);
border: 1px solid var(--color-button-disabled);
color: var(--color-disabled);
background-color: var(--color-disabled-alt);
border: 1px solid var(--color-disabled);
background-image: none;
text-shadow: none;
cursor: default;
color: var(--color-button-disabled);
&:hover,
&:focus,
&:active {
/* Override any link underlines and color changes. */
color: var(--color-button-disabled);
color: var(--color-disabled);
background-color: var(--color-disabled-alt);
text-decoration: none;
}
}
@ -127,7 +126,7 @@ textarea {
@media (min-width: 666px) {
input:only-of-type {
&:has(+ [type='submit']) {
margin-inline-end: .5rem;
margin-inline-end: 0.5rem;
}
}
}

View file

@ -42,27 +42,3 @@ A simple cards style. */
.card--gutter {
padding-inline: var(--gutters);
}
.card--inverted {
background: var(--color-border);
color: var(--color-text-bg);
a {
&:link {
color: var(--color-link-inverted);
}
&:visited {
color: var(--color-link-visited-inverted);
}
&:hover,
&:focus {
color: var(--color-link-hover-inverted);
}
&:active {
color: var(--color-link-active-inverted);
}
}
}

View file

@ -4,7 +4,7 @@
display: flex;
flex-flow: wrap;
justify-content: center;
gap: .5rem;
gap: 0.5rem;
position: sticky;
bottom: 0;
width: 100%;
@ -19,7 +19,7 @@
.cookieconsent__actions {
display: flex;
gap: .5rem;
gap: 0.5rem;
}
&[hidden] {

View file

@ -1,13 +1,13 @@
.disabled {
/* Re-set default cursor for disabled elements. */
cursor: default;
color: var(--color-button-disabled);
color: var(--color-disabled);
&:hover,
&:focus,
&:active {
/* Override any link underlines and color changes. */
color: var(--color-button-disabled);
color: var(--color-disabled);
text-decoration: none;
}
}

View file

@ -1,5 +1,5 @@
.flex-inline {
--flex-inline-gap: .5rem;
--flex-inline-gap: 0.5rem;
display: flex;
flex-wrap: wrap;

View file

@ -0,0 +1,45 @@
.htmx-indicator {
position: fixed;
inset: 0;
width: 100%;
height: 4px;
background: linear-gradient(
90deg,
transparent,
var(--color-accent),
transparent,
var(--color-accent),
transparent
);
opacity: 0;
visibility: hidden;
&.htmx-request {
opacity: 1;
visibility: visible;
animation:
--fadein 2s linear forwards,
--slide 1.2s ease-in-out infinite;
}
}
@keyframes --slide {
0% {
transform: translateX(-100%);
}
100% {
transform: translateX(100%);
}
}
@keyframes --fadein {
0% {
opacity: 0;
}
50% {
opacity: 0;
}
100% {
opacity: 1;
}
}

View file

@ -10,7 +10,7 @@
&:focus-visible,
&:hover {
.icon-link {
opacity: .3;
opacity: 0.3;
&:focus-visible,
&:hover {

View file

@ -1,33 +1,42 @@
/* Messages */
.message {
padding: .5rem;
--color-message: var(--color-grey-600);
--color-message-text: light-dark(
oklch(from var(--color-message) l c h),
oklch(from var(--color-message) 100% 0.03 h)
);
--color-message-bg: light-dark(
oklch(from var(--color-message) 100% 0.03 h),
oklch(from var(--color-message) calc(l + 0.07) c h)
);
--color-message-border: light-dark(
oklch(from var(--color-message) calc(l + 0.2) c h),
oklch(from var(--color-message) calc(l - 0.07) c h)
);
padding: 0.5rem;
color: var(--color-message-text);
background-color: var(--color-message-bg);
outline-color: var(--color-message-border);
outline-width: 2px;
outline-style: solid;
width: 95%;
&.success {
background-color: var(--color-success-bg);
color: var(--color-success);
outline-color: var(--color-success);
&.info {
--color-message: var(--color-info);
}
&.info {
background-color: var(--color-info-bg);
color: var(--color-info);
outline-color: var(--color-info);
&.success {
--color-message: var(--color-success);
}
&.warning {
background-color: var(--color-warning-bg);
color: var(--color-warning);
outline-color: var(--color-warning);
--color-message: var(--color-warning);
}
&.error {
background-color: var(--color-error-bg);
color: var(--color-error);
outline-color: var(--color-error);
--color-message: var(--color-error);
}
}
@ -37,6 +46,6 @@
@keyframes --outline-highlight {
50% {
outline-width: .3rem;
outline-width: 0.3rem;
}
}

Some files were not shown because too many files have changed in this diff Show more