CSS Variables
CSS variables, also called custom properties, give a name to a value that is reused across the site.
--color-neutral-100: #F2F0E5;You can then use that variable anywhere in the CSS:
body { color: var(--color-neutral-100);}This is useful for keeping a coherent and consistent visual design.
I keep the shared variables in var.css. It loads after the CSS reset and before global.css, utilities and component styles.
The variables I use
Section titled “The variables I use”:root { color-scheme: dark;
/* Color */
--color-neutral-100: #F2F0E5; --color-neutral-200: #CECDC3; --color-neutral-300: #B7B5AC; --color-neutral-400: #9F9D96; --color-neutral-500: #878580; --color-neutral-600: #6F6E69; --color-neutral-700: #575653; --color-neutral-800: #403E3C; --color-neutral-900: #1C1B1A;
--color-red-100: #FFCABB; --color-red-200: #F89A8A; --color-red-300: #E8705F; --color-red-400: #D14D41; --color-red-500: #C03E35; --color-red-600: #AF3029; --color-red-700: #942822; --color-red-800: #6C201C; --color-red-900: #3E1715;
--color-orange-100: #FED3AF; --color-orange-200: #F9AE77; --color-orange-300: #EC8B49; --color-orange-400: #DA702C; --color-orange-500: #CB6120; --color-orange-600: #BC5215; --color-orange-700: #9D4310; --color-orange-800: #71320D; --color-orange-900: #40200D;
--color-yellow-100: #F6E2A0; --color-yellow-200: #ECCB60; --color-yellow-300: #DFB431; --color-yellow-400: #D0A215; --color-yellow-500: #BE9207; --color-yellow-600: #AD8301; --color-yellow-700: #8E6B01; --color-yellow-800: #664D01; --color-yellow-900: #3A2D04;
--color-green-100: #DDE2B2; --color-green-200: #BEC97E; --color-green-300: #A0AF54; --color-green-400: #879A39; --color-green-500: #768D21; --color-green-600: #66800B; --color-green-700: #536907; --color-green-800: #3D4C07; --color-green-900: #252D09;
--color-cyan-100: #BFE8D9; --color-cyan-200: #87D3C3; --color-cyan-300: #5ABDAC; --color-cyan-400: #3AA99F; --color-cyan-500: #2F968D; --color-cyan-600: #24837B; --color-cyan-700: #1C6C66; --color-cyan-800: #164F4A; --color-cyan-900: #122F2C;
--color-blue-100: #C6DDE8; --color-blue-200: #92BFDB; --color-blue-300: #66A0C8; --color-blue-400: #4385BE; --color-blue-500: #3171B2; --color-blue-600: #205EA6; --color-blue-700: #1A4F8C; --color-blue-800: #163B66; --color-blue-900: #12253B;
--color-purple-100: #E2D9E9; --color-purple-200: #C4B9E0; --color-purple-300: #A699D0; --color-purple-400: #8B7EC8; --color-purple-500: #735EB5; --color-purple-600: #5E409D; --color-purple-700: #4F3685; --color-purple-800: #3C2A62; --color-purple-900: #261C39;
--color-magenta-100: #FCCFDA; --color-magenta-200: #F4A4C2; --color-magenta-300: #E47DA8; --color-magenta-400: #CE5D97; --color-magenta-500: #B74583; --color-magenta-600: #A02F6F; --color-magenta-700: #87285E; --color-magenta-800: #641F46; --color-magenta-900: #39172B;
--color-black: #100F0F; --color-white: #FFFCF0;
/* Semantic color tokens */
--color-accent-100: var(--color-blue-100); --color-accent-200: var(--color-blue-200); --color-accent-300: var(--color-blue-300); --color-accent-400: var(--color-blue-400); --color-accent-500: var(--color-blue-500); --color-accent-600: var(--color-blue-600); --color-accent-700: var(--color-blue-700); --color-accent-800: var(--color-blue-800); --color-accent-900: var(--color-blue-900);
--color-accent-alt-100: var(--color-cyan-100); --color-accent-alt-200: var(--color-cyan-200); --color-accent-alt-300: var(--color-cyan-300); --color-accent-alt-400: var(--color-cyan-400); --color-accent-alt-500: var(--color-cyan-500); --color-accent-alt-600: var(--color-cyan-600); --color-accent-alt-700: var(--color-cyan-700); --color-accent-alt-800: var(--color-cyan-800); --color-accent-alt-900: var(--color-cyan-900);
--color-background: var(--color-neutral-900); --color-surface: var(--color-neutral-800); --color-surface: color-mix(in srgb, var(--color-neutral-800) 35%, var(--color-background)); --color-surface-muted: var(--color-neutral-700); --color-text: var(--color-neutral-100); --color-text-subtle: var(--color-neutral-300); --color-text-muted: var(--color-neutral-400); --color-border: var(--color-neutral-700); --color-link: var(--color-accent-300); --color-link-hover: var(--color-accent-200); --color-link-active: var(--color-accent-100); --color-focus-ring: var(--color-accent-300); --color-eyebrow: var(--color-accent-200);
/* Typography */ --font-family: ui-sans-serif, "Helvetica Neue", "Helvetica", Arial; --font-mono: ui-monospace, Menlo, monospace;
--font-size-s: 0.875rem; --font-size-code: 1rem; --font-size-base: 1.125rem; /* or 16px for base*/ --font-size-l: 1.25rem;
--font-size-xl: 1.375rem; --font-size-2xl: 1.5rem; --font-size-3xl: 1.75rem; /* mobile h2 */ --font-size-4xl: 2.25rem; /* mobile h1; desktop h2 */ --font-size-5xl: 2.75rem; /* desktop h1*/ --font-size-6xl: 4rem;
--font-weight-light: 300; --font-weight-regular: 400; --font-weight-semi-bold: 600; --font-weight-bold: 700;
--font-line-height: 1.5; --font-line-height-heading: 1.2;
/* Spacing */ --space-2xs: 0.375rem; --space-xs: 0.5rem; --space-s: 0.75rem; --space-m: 1.25rem; --space-l: 2rem; --space-xl: 3.25rem; --space-2xl: 5.25rem; --space-3xl: 8.5rem;
--flow-space: 1em;
/* Border */ --border-radius-s: 4px; --border-radius-m: 8px; --border-radius-l: 16px;
--border-width-s: 1px; --border-width-m: 2px; --border-width-l: 4px;
/* Shadows */ --text-shadow-s: 0 2px 4px rgba(0, 0, 0, 0.1); --text-shadow-m: 0 4px 4px rgba(0, 0, 0, 0.15); --text-shadow-l: 0 6px 12px rgba(0, 0, 0, 0.2);
--box-shadow-s: 0px 2px 4px 0px rgba(0, 0, 0, 0.2); --box-shadow-m: 0px 4px 4px 0px rgba(0, 0, 0, 0.25); --box-shadow-l: 0px 4px 8px 0px rgba(0, 0, 0, 0.25);
/* Transitions */ --transition-normal: 0.2s ease;
/* consider adding animation var if needed */
/* Z index*/
--z-navbar: 100; --z-dropdown: 200; --z-overlay: 300; --z-modal: 400; --z-toast: 500;
/* Icon sizes*/
--icon-size-s: 1rem; --icon-size-m: 1.25rem; --icon-size-l: 1.5rem;
/* Other */ --navbar-height: 3.5rem; --footer-height: 22rem; --side-gap-padding: var(--space-s);
--site-max-width: 84rem; --content-max-width: 45rem;
--opacity-weak: 0.3; --opacity-strong: 0.7;
}
[data-theme="light"] { color-scheme: light;
--color-background: var(--color-neutral-100); --color-surface: var(--color-neutral-200); --color-surface: color-mix(in srgb, var(--color-neutral-200) 35%, var(--color-background)); --color-surface-muted: var(--color-neutral-300); --color-text: var(--color-neutral-900); --color-text-subtle: var(--color-neutral-700); --color-text-muted: var(--color-neutral-700); --color-border: var(--color-neutral-300); --color-link: var(--color-accent-700); --color-link-hover: var(--color-accent-800); --color-link-active: var(--color-accent-900); --color-focus-ring: var(--color-accent-700); --color-eyebrow: var(--color-accent-700);}Palette colors and semantic colors
Section titled “Palette colors and semantic colors”The numbered colors are the palette. They say what a color is. The semantic variables say where it is used.
For example, component CSS should usually use --color-text, --color-surface or --color-border. It should not need to know that the current text color is --color-neutral-100. This is what makes the light theme small, only the semantic variables need new values.
--color-surface is declared twice in each theme. The plain color is a fallback. Browsers that support color-mix() use the following declaration.
--color-eyebrow uses a lighter accent in the dark theme and a darker one in the light theme.
You can change the accent without touching each component:
--color-accent-100: var(--color-purple-100);--color-accent-200: var(--color-purple-200);/* Continue through 900. */Pick and test your colors as described on the Colors page. A complete palette is useful, but that does not mean every project needs to use every color.
Themes
Section titled “Themes”Dark is the default theme. Add data-theme="light" to the <html> element to use the light values:
<html lang="en" data-theme="light">The attribute can also be used on a section that needs a different theme. global.css applies the matching text and background colors to any element with data-theme.
Type, spacing and layout
Section titled “Type, spacing and layout”The type scale is mobile-first. global.css maps it to the heading elements and increases the main heading sizes at 48rem.
The spacing variables are shared by components, the .flow class, the Markdown styles and the margin and padding utilities. The spacing guide explains how I choose between the values.
--site-max-width is the widest limit for page-level layouts and components. In my Starter example it is used for the content inside the header and footer, the hero and large card grids.
--content-max-width sets the narrower inner column. Regular page content uses this width, this includes text which should not grow too large (for accessibility and clear UI reasons).
Both widths can therefore be used on the same page. They are not two alternative page sizes. One limits the wider site structure; the other limits the content placed inside it. The .base-layout utility connects them without requiring a different grid for each width.
The z-index values name the layers I commonly need. Keeping them in one place avoids a component fixing an overlap with an arbitrary value such as 9999.
Sources and references
- Flexoki color scheme: https://stephango.com/flexoki
- Starlight CSS variables: https://github.com/withastro/starlight/blob/main/packages/starlight/style/props.css
- Kevin Powell’s Astro project styling: https://youtu.be/Thudicbgqtg?t=1163
- Rules for good spacing: https://youtu.be/9ElrcTtAxzA