Style adjustments for easier theme customizations
Revise our current CSS setup to make it easier to customize the theme.
Tasks
- [ ] design a set of CSS variables (consider using HSL/OKLCH and a set of derived colors; see example below)
- [x] decide if we're going to keep using Tailwind typography plugin yes, we're keeping it for now
- [ ] adjust our docs on customizing the theme
Notes
Minimal example implementation with HSL:
/* Channel tokens in HSL so we can derive related colors */
:root {
--fg-h: 240; /* bluish hue (0â360°) */
--fg-s: 5%; /* saturation */
--fg-l: 12%; /* lightness */
/* Derived */
--fg: var(--fg-h) var(--fg-s) var(--fg-l);
--muted-fg: var(--fg-h) var(--fg-s) calc(var(--fg-l) + 30%);
}
With this, if a user wants to re-skin with a different color family, they can just change --fg-h value (the hue).
Example user custom.css:
:root {
--fg-h: 120; /* green hue */
}
The current way I implemented this is to set the accent color in RGB values (we can change that to HEX) and then generate the lighter/darker shades with OKLCH.
/* COLORS - ACCENT COLOR */
--color-secondary: 32 150 255;
/* GENERATE LIGHT/DARK SHADES FROM ACCENT COLOR */
--color-secondary-light: oklch(from rgb(var(--color-secondary)) calc(l + 0.15) c h);
--color-secondary-dark: oklch(from rgb(var(--color-secondary)) calc(l - 0.1) c h);
I think allowing the user to customize the color value in a familiar format like HEX would make it easier for them. Or is it just me?
@axraph I'd investigate how Obsidian does this first as they have a solid and battle-tested system. They have their CSS variables well documented. Here is the majority that works for both the desktop app and the published website - https://docs.obsidian.md/Reference/CSS+variables/CSS+variables and here is a handful of other publish-specific ones https://docs.obsidian.md/Reference/CSS+variables/Publish/Publish
@olayway I have adjusted our variables as follows:
We set the "Base" variables in any CSS color format.
:root {
/* LIGHT THEME */
--color-l-background: #FFFFFF; /* BACKGROUND */
--color-l-primary: #373737; /* TEXT */
--color-l-secondary: #2096ff; /* ACCENT */
}
We can optionally specify the dark theme variables.
:root {
/* DARK THEME */
--color-d-background: #171717; /* BACKGROUND */
--color-d-primary: #EEEEEE; /* TEXT */
--color-d-secondary: #2096ff; /* ACCENT */
}
Or simply switch the background and primary colors for the dark theme.
:root {
/* DARK THEME FALLBACK
SWITCH BACKGROUND AND PRIMARY IF YOU WON'T SPICIFY DARK THEME COLORS */
--color-d-background: var(--color-l-primary); /* BACKGROUND */
--color-d-primary: var(--color-l-background); /* TEXT */
--color-d-secondary: var(--color-l-secondary); /* ACCENT */
}
Then the color shades are generated from the base colors using OKLCH by adjusting the lightness channel only.
/* COLOR VARS - GENERATED FROM BASE COLORS
YOU DON'T HAVE TO OVERRIDE THESE
UNLESS YOU REALLY WANT TO! */
/* BACKWARD COMPATIBILITY FOR OLDER BROWSERS THAT DON'T SUPPORT light-dark() */
:root,
:root[data-theme="light"] {
--color-background: var(--color-l-background);
--color-primary: var(--color-l-primary);
--color-primary-strong: oklch(from var(--color-primary) calc(l - 0.1684) c h);
--color-primary-emphasis: oklch(from var(--color-primary) calc(l - 0.1018) c h);
--color-primary-subtle: oklch(from var(--color-primary) calc(l + 0.2118) c h);
--color-primary-muted: oklch(from var(--color-primary) calc(l + 0.3722) c h);
--color-primary-faint: oklch(from var(--color-primary) calc(l + 0.5881) c h);
--color-secondary: var(--color-l-secondary);
--color-secondary-lighter: oklch(from var(--color-secondary) calc(l + 0.15) c h);
--color-secondary-darker: oklch(from var(--color-secondary) calc(l - 0.1) c h);
}
:root[data-theme="dark"] {
--color-background: var(--color-d-background);
--color-primary: var(--color-d-primary);
--color-primary-strong: oklch(from var(--color-primary) calc(l + 0.1684) c h);
--color-primary-emphasis: oklch(from var(--color-primary) calc(l + 0.1018) c h);
--color-primary-subtle: oklch(from var(--color-primary) calc(l - 0.2118) c h);
--color-primary-muted: oklch(from var(--color-primary) calc(l - 0.3722) c h);
--color-primary-faint: oklch(from var(--color-primary) calc(l - 0.5881) c h);
--color-secondary: var(--color-d-secondary);
--color-secondary-lighter: oklch(from var(--color-secondary) calc(l + 0.15) c h);
--color-secondary-darker: oklch(from var(--color-secondary) calc(l - 0.1) c h);
}
OKLCH allows us to also adjust the transparency of any of these generated colors whenever we need by adjusting the alpha channel only.
.primary-color-10-percent {
color: oklch(from var(--color-primary) l c h / 10%);
}
IMPORTANT NOTE: Since we're now using full color values, we can no longer apply the colors like we used to rgb(var(--color-primary-subtle)) instead, to apply this color variable we'd simply use var(--color-primary-subtle).
@axraph looks awesome, just some minor suggestions for naming:
- get rid of
-l-for light (default) theme (let's have pairs likecolor-background+color-d-backgroundspecifically for dark, as dark variables will only require adjustments if sb builds a theme that should support both color variants) - rename
primarytoforeground - rename
secondarytoaccent
Like so:
:root {
/* LIGHT THEME (DEFAULT) */
--color-background: #FFFFFF;
--color-foreground: #373737;
--color-accent: #2096ff;
/* DARK THEME */
--color-d-background: #171717;
--color-d-foreground: #EEEEEE;
--color-d-accent: #2096ff;
}
@olayway Regarding our variables, I have been working on a system that would be useful for theme development, and also would be easy for users to customize.
My proposal is to have 2 sets of variables:
- Utility Variables
- Theme Variables
The utility vars are kind of abstract values that we normally wouldn't need to change nor override, they'd be used for the theme vars, and then we would use the theme vars in the theme CSS. If a user wants to customize their theme, they'd only need to override the theme vars.
1. Typography
For the typography variables, we have font families, weights, sizes, and line-heights.
Font Weights
The weight utility variables are:
--font-weight-thin: 100;
--font-weight-extralight: 200;
--font-weight-light: 300;
--font-weight-normal: 400;
--font-weight-medium: 500;
--font-weight-semibold: 600;
--font-weight-bold: 700;
--font-weight-extrabold: 800;
--font-weight-black: 900;
In the theme, I use the utility vars to populate my theme vars:
--font-weight-body: var(--font-weight-normal);
--font-weight-bold-body: var(--font-weight-bold);
--font-weight-headings: var(--font-weight-bold);
--font-weight-bold-headings: var(--font-weight-black);
Now if the theme designer or the user wishes to use different weights for the body text, they'd need to override these theme variables:
--font-weight-body: var(--font-weight-extralight);
--font-weight-bold-body: var(--font-weight-medium);
Line Heights
Similarly, our line-height utility vars are:
--line-height-none: 1;
--line-height-tight: 1.2;
--line-height-snug: 1.4;
--line-height-normal: 1.6;
--line-height-relaxed: 1.8;
--line-height-loose: 2;
And the theme vars:
--font-line-height-body: var(--line-height-normal);
--font-line-height-headings: var(--line-height-tight);
Font Sizes
For the font sizes, I wanted to give the users the ability to customize them if they wanted to, but at the same time assist them in choosing the sizes that would look good with minimal effort.
The idea is to have font-size calculations baked-in the utility vars, using popular type scales.
Our utility variables are:
/* FONT SIZE SCALES */
--scale-minor-second: 1.067;
--scale-major-second: 1.125;
--scale-minor-third: 1.200;
--scale-major-third: 1.250;
--scale-perfect-fourth: 1.333;
--scale-augmented-fourth: 1.414;
--scale-perfect-fifth: 1.500;
--scale-golden-ratio: 1.618;
/* FONT SIZES */
--font-size-xx-small: calc(var(--font-size-x-small) / var(--font-size-scale));
--font-size-x-small: calc(var(--font-size-small) / var(--font-size-scale));
--font-size-small: calc(var(--font-size-base) / var(--font-size-scale));
--font-size-medium: var(--font-size-base);
--font-size-large: calc(var(--font-size-base) * var(--font-size-scale));
--font-size-x-large: calc(var(--font-size-large) * var(--font-size-scale));
--font-size-xx-large: calc(var(--font-size-x-large) * var(--font-size-scale));
--font-size-xxx-large: calc(var(--font-size-xx-large) * var(--font-size-scale));
--font-size-huge: calc(var(--font-size-xxx-large) * var(--font-size-scale));
--font-size-gigantic: calc(var(--font-size-huge) * var(--font-size-scale));
And in our theme we'd set the base size and scale variables that would generate these harmonious sizes:
--font-size-base: 1.25rem; /* VALUE IN REM RECOMMENDED */
--font-size-scale: var(--scale-minor-third);
And pick the font sizes we want for our elements from our utility vars:
--font-size-body: var(--font-size-medium);
--font-size-h1: var(--font-size-xxx-large);
--font-size-h2: var(--font-size-xxx-large);
--font-size-h3: var(--font-size-xx-large);
--font-size-h4: var(--font-size-x-large);
--font-size-h5: var(--font-size-large);
--font-size-caption: var(--font-size-small);
--font-size-smaller: var(--font-size-x-small);
--font-size-smallest: var(--font-size-xx-small);
Now if the user wants to have smaller font size for everything, they'd need to override one variable:
--font-size-base: 1rem;
If they need a more dramatic change between the sizes, they can a choose a different scale:
--font-size-scale: var(--scale-perfect-fourth);
And if they only want bigger H1 headings:
--font-size-h1: var(--font-size-huge);
2. Colors
Following the same logic, our color variables are also divided between utility and theme vars. The utility vars are where the calculations happen, and the theme vars are the ones that set the base colors we use in the calculations.
Foreground
The utility vars for the foreground color are:
/* LIGHT MODE */
--color-foreground-50: oklch(from var(--color-foreground) 0.975 c h);
--color-foreground-100: oklch(from var(--color-foreground) 0.925 c h);
--color-foreground-200: oklch(from var(--color-foreground) 0.825 c h);
--color-foreground-300: oklch(from var(--color-foreground) 0.725 c h);
--color-foreground-400: oklch(from var(--color-foreground) 0.625 c h);
--color-foreground-500: oklch(from var(--color-foreground) 0.525 c h);
--color-foreground-600: oklch(from var(--color-foreground) 0.425 c h);
--color-foreground-700: oklch(from var(--color-foreground) 0.325 c h);
--color-foreground-800: oklch(from var(--color-foreground) 0.225 c h);
--color-foreground-900: oklch(from var(--color-foreground) 0.125 c h);
/* DARK MODE */
--color-foreground-50: oklch(from var(--color-foreground) 0.125 c h);
--color-foreground-100: oklch(from var(--color-foreground) 0.175 c h);
--color-foreground-200: oklch(from var(--color-foreground) 0.275 c h);
--color-foreground-300: oklch(from var(--color-foreground) 0.375 c h);
--color-foreground-400: oklch(from var(--color-foreground) 0.475 c h);
--color-foreground-500: oklch(from var(--color-foreground) 0.575 c h);
--color-foreground-600: oklch(from var(--color-foreground) 0.675 c h);
--color-foreground-700: oklch(from var(--color-foreground) 0.775 c h);
--color-foreground-800: oklch(from var(--color-foreground) 0.875 c h);
--color-foreground-900: oklch(from var(--color-foreground) 0.975 c h);
Background
I propose we derive a "surface" color from the background color instead of using the foreground-50, so we are not worried about contrast with the background color.
/* LIGHT MODE */
--color-background-surface: oklch(from var(--color-background) calc(l - 0.03) c h); /* 3% darker */
/* DARK MODE */
--color-background-surface: oklch(from var(--color-background) calc(l + 0.05) c h); /* 5% lighter */
Accent
Nothing changed here, we'll simply derive one lighter shade and one darker shade from the accent color to use in instances like button hover color or quote block border color.
--color-accent-lighter: oklch(from var(--color-accent) calc(l + 0.15) c h);
--color-accent-darker: oklch(from var(--color-accent) calc(l - 0.1) c h);
Re: Font weights
In the theme, I use the utility vars to populate my theme vars:
--font-weight-body: var(--font-weight-normal); --font-weight-bold-body: var(--font-weight-bold); --font-weight-headings: var(--font-weight-bold); --font-weight-bold-headings: var(--font-weight-black);
- It duplicates abstractions we already have
HTML tags and our internal class names already provide a semantic layer. For example:
h1, h2, h3, h4, h5 {
font-weight: var(--font-weight-bold);
}
is simpler and more direct than:
--font-weight-headings: var(--font-weight-bold);
h1, h2, h3, h4, h5 {
font-weight: var(--font-weight-headings);
}
The extra variable doesnât add much clarityâit just adds another layer to reason about.
- Leads to either explosion of variables or semantic confusion
There are two ways this typically plays out:
Option A: Many, many variables
--font-weight-cta: var(--font-weight-bold);
--font-weight-nav-title: var(--font-weight-black);
--font-weight-footer: var(--font-weight-thin);
--font-weight-footer-links: var(--font-weight-semibold);
Problems:
- The list of variables quickly becomes unmanageable.
- It adds a redundant mapping layer (.cta â --font-weight-cta â --font-weight-bold).
- When reading a CSS rule, itâs harder to know what weight is actually being applied without chasing variables.
Option B: Reuse variables semantically in inconsistent places
h1, h2, h3, h4, h5, .hero-cta {
font-weight: var(--font-weight-headings);
}
Problems:
- Hard to find a variable name that fits multiple, unrelated elements.
- It hides the actual intent (e.g. âmake CTA boldâ) behind a variable that was defined for something else.
- Often youâre just expressing âthese should both be boldââwhich you can already do directly:
h1, h2, h3, h4, h5, .hero-cta {
font-weight: var(--font-weight-bold);
}
- Theme-specific and harder to adjust
This setup might make sense in one theme, but not in another.
For example this:
h1, h2, h3, h4, h5, .hero-cta {
font-weight: var(--font-weight-headings);
}
...may work in one theme, if you want to make sure that both headings and the CTA share the same weight. But the other theme may want CTA to be styled differently.
Because of the extra indirection, itâs harder to quickly reason about and adjust font weights across a theme.
My suggestion
Keep a set of utility variables (e.g. --font-weight-normal, --font-weight-bold, --font-weight-black), and use them directly in CSS rules. This is:
- Easier to reason about.
- Less abstraction / indirection.
- More flexible across different themes.
Also: please don't hyper focus on making themes customizable. Our main goal is to provide excellent, ready-to-use themes. If anyone wants to modify them â it's possible without extra set of CSS variables.
Re: Line heights and font sizes
My recommendation and reasoning are exactly the same as in case of font weights. Keep utilities, get rid of extra variables. But you can keep them in the theme if you really want. I'm just not going to use them in the default theme or document them. We agreed on having themes self-contained, so in theory you can do whatever you want. Maybe in one theme having a set of additional helper variables makes sense, or it just makes it easier for you to create such theme â great. By all means, do so :)
Re: Background
I propose we derive a "surface" color from the background color instead of using the foreground-50, so we are not worried about contrast with the background color.
I wouldn't do that. It will be surprising to see that the background color I picked doesn't hold the shade I wanted, and drifts in one direction. And again - another abstraction layer, extra complexity. Better keep things simple and just provide guidance in the docs on how to pick great background+foreground sets.
That being said, I'd also stick to the narrower lightness scale for foreground that I proposed last time (or similar), so:
- Light theme:
0.9-0.125alpha scale + suggest to pick background that is > 90% alpha - Dark theme:
0.2-0.975alpha scale + suggest to pick background that is <20% alpha
I think the title should be more clear about the purpose of this shaping: it's about making it easier to customize the default theme = to customize your otherwise unstyled, basic Flowershow site ;) And the most important and most basic aspects of it are:
- foreground color
- background color
- accent color
- font family
- font size (larger/smaller in general)
We want to make sure that the theme can be easily tweaked by changing just this handful of variables.
If you want to customize some details, e.g. you don't like the font weight that we apply to headings -> you'll need to make you're hands more dirty with CSS, one way or the other.