/ astro-integrations / How to Integrate Open Props with Astro: Complete Guide
astro-integrations 11 min read

How to Integrate Open Props with Astro: Complete Guide

Step-by-step guide to integrating Open Props with your Astro website.

How to Integrate Open Props with Astro: Complete Guide

Open Props is a collection of CSS custom properties (variables) that provide a consistent design system out of the box. Instead of defining your own spacing scale, color palette, font sizes, shadows, and animations from scratch, you import Open Props and use pre-defined variables like --size-3, --blue-7, or --shadow-2. Think of it as a design token library, not a framework.

The difference from Tailwind or UnoCSS is that Open Props does not generate utility classes. It gives you CSS variables that you use in your own stylesheets, however you write them. This makes it a perfect fit for Astro projects that use scoped styles, plain CSS, or any CSS methodology.

Prerequisites

  • Node.js 22 or later. Astro 6 (current major, 6.4.2 as of this writing) dropped support for Node 18 and 20, so make sure you are on Node 22+ before you start.
  • An Astro project (npm create astro@latest)

Versions referenced in this guide, all checked against npm on 2026-05-29:

  • astro 6.4.2
  • open-props 1.7.23
  • postcss-jit-props 1.0.16

Installation

Open Props ships as a single dependency with zero build-time requirements:

npm install open-props

For the PostCSS plugin that removes unused variables from production builds:

npm install -D postcss-jit-props

Both package names are current and unchanged. Pin them if you want reproducible installs:

npm install open-props@1.7.23
npm install -D postcss-jit-props@1.0.16

Configuration

There are two supported ways to wire Open Props into an Astro project, and they are mutually exclusive for any given import. Pick one per stylesheet.

Option A: Precompiled CSS (zero configuration)

Import the prebuilt stylesheets in your global CSS. These map to the minified files shipped in the package, confirmed in the exports map of open-props@1.7.23:

/* src/assets/styles/global.css */
@import "open-props/style";
@import "open-props/normalize";

The style import (which resolves to open-props.min.css) gives you every CSS custom property. The normalize import (resolving to normalize.min.css) is an optional reset that uses Open Props variables for sensible defaults. This approach ships all the variables whether you use them or not, which is fine for prototyping but heavier than necessary for production.

To ship only the variables you actually reference, use postcss-jit-props. Astro processes PostCSS through its built-in Vite pipeline and automatically applies a postcss.config.cjs at the project root, so no change to astro.config.mjs is needed.

// postcss.config.cjs
const postcssJitProps = require("postcss-jit-props");
const OpenProps = require("open-props");

module.exports = {
  plugins: [postcssJitProps(OpenProps)],
};

With JIT enabled, you do NOT add the @import "open-props/style" line. The plugin watches your authored CSS for var(--...) references and injects only the matching Open Props definitions into :root, so unused tokens never reach the bundle. If you still want the normalize reset alongside JIT, import the unminified PostCSS variant so the plugin can resolve its variables:

/* src/assets/styles/global.css */
@import "open-props/postcss/normalize";

The .cjs extension matters. Astro project scaffolds set "type": "module" in package.json, which makes plain .js files ES modules. PostCSS config loading still expects CommonJS require() here, so name the file postcss.config.cjs rather than postcss.config.js to avoid a module-type error.

Import the global stylesheet in your layout:

---
// src/layouts/BaseLayout.astro
import "../assets/styles/global.css";
---

<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title><slot name="title" /></title>
  </head>
  <body>
    <slot />
  </body>
</html>

Basic Usage

Use Open Props variables in your Astro component scoped styles:

---
// src/components/Card.astro
export interface Props {
  title: string;
  description: string;
}

const { title, description } = Astro.props;
---

<div class="card">
  <h3 class="card-title">{title}</h3>
  <p class="card-description">{description}</p>
</div>

<style>
  .card {
    background: var(--surface-1);
    border-radius: var(--radius-3);
    padding: var(--size-5);
    box-shadow: var(--shadow-2);
    transition: box-shadow 0.2s var(--ease-2);
  }

  .card:hover {
    box-shadow: var(--shadow-4);
  }

  .card-title {
    font-size: var(--font-size-4);
    font-weight: var(--font-weight-7);
    color: var(--text-1);
    margin-bottom: var(--size-2);
  }

  .card-description {
    font-size: var(--font-size-2);
    color: var(--text-2);
    line-height: var(--font-lineheight-3);
  }
</style>

Available Properties

Open Props provides variables across several categories:

Sizes and Spacing:

var(--size-1)   /* 0.25rem */
var(--size-3)   /* 1rem */
var(--size-5)   /* 1.5rem */
var(--size-7)   /* 2rem */
var(--size-10)  /* 5rem */

Colors (19 color scales, 13 shades each):

var(--blue-1)   /* lightest */
var(--blue-7)   /* mid */
var(--blue-12)  /* darkest */
var(--green-5)
var(--red-9)
var(--gray-3)

Shadows:

var(--shadow-1) /* subtle */
var(--shadow-3) /* medium */
var(--shadow-6) /* dramatic */

Animations and Easings:

var(--ease-1)        /* ease-in */
var(--ease-out-3)    /* ease-out */
var(--animation-fade-in)
var(--animation-slide-in-up)

Typography:

var(--font-size-0)        /* 0.75rem */
var(--font-size-3)        /* 1.25rem */
var(--font-size-6)        /* 2.5rem */
var(--font-weight-4)      /* 400 */
var(--font-weight-7)      /* 700 */
var(--font-lineheight-3)  /* 1.5 */

Dark Mode

Open Props includes adaptive color properties that respond to prefers-color-scheme:

/* These adapt automatically in dark mode */
var(--surface-1) /* white in light, dark gray in dark */
var(--surface-2)
var(--text-1)    /* near-black in light, near-white in dark */
var(--text-2)    /* muted text, adapts to both modes */

If you control dark mode with a class:

/* These reference the standard color tokens from open-props/style */
.dark {
  --surface-1: var(--gray-9);
  --surface-2: var(--gray-8);
  --text-1: var(--gray-1);
  --text-2: var(--gray-4);
}

Building a Component Library

Open Props is ideal for building reusable Astro components with consistent styling:

---
// src/components/Button.astro
export interface Props {
  variant?: "primary" | "secondary" | "ghost";
  size?: "sm" | "md" | "lg";
}

const { variant = "primary", size = "md" } = Astro.props;
const className = "btn btn-" + variant + " btn-" + size;
---

<button class={className}>
  <slot />
</button>

<style>
  .btn {
    display: inline-flex;
    align-items: center;
    justify-content: center;
    gap: var(--size-2);
    border: none;
    border-radius: var(--radius-2);
    font-weight: var(--font-weight-6);
    cursor: pointer;
    transition: all 0.2s var(--ease-2);
    text-decoration: none;
  }

  .btn-sm { padding: var(--size-1) var(--size-3); font-size: var(--font-size-0); }
  .btn-md { padding: var(--size-2) var(--size-4); font-size: var(--font-size-1); }
  .btn-lg { padding: var(--size-3) var(--size-5); font-size: var(--font-size-2); }

  .btn-primary {
    background: var(--blue-7);
    color: white;
  }
  .btn-primary:hover { background: var(--blue-8); }

  .btn-secondary {
    background: var(--surface-2);
    color: var(--text-1);
    border: 1px solid var(--gray-3);
  }
  .btn-secondary:hover { background: var(--surface-3); }

  .btn-ghost {
    background: transparent;
    color: var(--text-1);
  }
  .btn-ghost:hover { background: var(--surface-2); }
</style>

Animations

Open Props includes pre-built animation keyframes:

---
// src/components/FadeIn.astro
---

<div class="fade-in">
  <slot />
</div>

<style>
  .fade-in {
    animation: var(--animation-fade-in) forwards;
    animation-duration: 0.5s;
    animation-timing-function: var(--ease-out-3);
  }
</style>

Available animations include fade-in, fade-out, slide-in-up, slide-in-down, slide-in-left, slide-in-right, scale-up, scale-down, shake-x, shake-y, spin, ping, and blink.

Combining with Tailwind

You can use Open Props alongside Tailwind CSS. Note that the integration story changed with Tailwind v4 (the current major). The old @astrojs/tailwind integration plus tailwind.config.mjs JavaScript config is deprecated. Astro 5.2 and later add native support for Tailwind's first-party Vite plugin, and astro add tailwind now wires @tailwindcss/vite into your Astro config and creates a CSS entry that imports Tailwind. Tailwind v4 is configured in CSS with the @theme directive rather than a JavaScript config file.

So to map Open Props tokens into Tailwind v4, reference them inside @theme in the same CSS file where you import Tailwind:

/* src/styles/app.css */
@import "tailwindcss";
@import "open-props/style";

@theme {
  --color-primary: var(--blue-7);
  --color-surface: var(--surface-1);
  --radius-card: var(--radius-3);
  --shadow-card: var(--shadow-2);
}

This generates Tailwind utility classes (such as bg-primary and rounded-card) that resolve to Open Props tokens. If you are still on a Tailwind v3 project that uses the JavaScript config, you can keep extending theme.extend with var(--...) values, but new projects should follow the CSS-first v4 approach above.

Production Tips

  1. Prefer postcss-jit-props for production. Open Props ships hundreds of variables. The JIT plugin injects only the ones you reference, so you avoid shipping the full token set. Remember that JIT and the precompiled @import "open-props/style" are alternatives: if you enable JIT, drop the full-bundle import or you will ship everything anyway.

  2. Use the normalize. Open Props normalize sets sensible defaults using the design tokens. It replaces traditional resets like normalize.css with one that already uses your design system.

  3. Stick to the scale. Open Props sizes follow a consistent scale. Using --size-3 then --size-5 looks intentional. Jumping from --size-1 to --size-10 looks accidental. Follow the progression.

  4. Extend, do not override. If the provided colors or sizes do not fit your brand, create your own custom properties that reference Open Props as a base rather than overriding them.

  5. Use HSL variants for custom palettes. Import open-props/colors-hsl to get HSL versions of all colors. This lets you create transparent variants with the HSL function.

A Note on Astro Rendering Modes

Open Props is pure CSS, so it works the same whether your Astro pages are statically generated or rendered on demand. One thing to get right on current Astro: the output: 'hybrid' setting no longer exists. Astro v5 merged output: 'hybrid' and output: 'static' into a single 'static' mode, which is also the default. If you copied an older tutorial that sets output: 'hybrid', remove that line. The valid values are now 'static' (the default) and 'server'.

With the default static output you can still render individual routes on demand simply by adding an adapter and setting export const prerender = false on the pages that need it. Astro v5 also removed dynamic values in prerender exports, so only the literal true and false are accepted. None of this changes how you import or use Open Props, but it is the most common stale-config trap people hit when following older Astro plus Open Props guides.

Alternatives to Consider

  • Tailwind CSS if you prefer utility classes over writing CSS with custom properties.
  • UnoCSS if you want an atomic CSS engine with custom rule support.
  • Vanilla Extract if you want type-safe CSS-in-TypeScript with build-time generation.

Common Errors and Fixes

postcss.config.js is treated as an ES module and throws on module.exports. Astro scaffolds set "type": "module" in package.json, which makes plain .js files ES modules. PostCSS config here expects CommonJS. Rename the file to postcss.config.cjs (the name used throughout this guide) and keep using require() and module.exports. This is the exact fix Astro's styling docs call out for "type": "module" projects.

Variables show up as the literal text var(--size-3) with no value applied. The custom property was never defined. Either you forgot the global stylesheet import, or you enabled postcss-jit-props but the file referencing the variable is not being processed by PostCSS (for example a raw <style> in HTML outside Astro's pipeline). Confirm the stylesheet is imported in a layout or component that Astro compiles.

Everything works in dev but the production bundle still ships the full token set. You have both approaches active at once: the precompiled @import "open-props/style" AND postcss-jit-props. The JIT plugin only saves bytes if the full-bundle import is removed. Pick one. For production, keep JIT and drop the full style import.

@import "open-props/normalize" resolves but its variables are empty under JIT. The minified normalize.min.css references Open Props variables that JIT does not see because they live inside an already-compiled file. When using JIT, import the PostCSS source variant instead, @import "open-props/postcss/normalize", so the plugin can resolve and inject the referenced tokens.

OKLCH or HD colors do not appear. The high-definition color tokens are in separate entry points (for example open-props/colors-hd and open-props/oklch-hues). They are not included by open-props/style. Import the specific pack you need.

Build fails on an old Node version. Astro 6 requires Node 22 or later. Node 18 and 20 support was dropped. Upgrade Node if your build errors out immediately on astro build.

Official Docs and Examples

Wrapping Up

Open Props gives you a professional design system without imposing any framework opinions. For Astro projects, it works with scoped styles, global CSS, or any other approach you prefer. The variables are well-named, the scales are thoughtful, and the dark mode support works out of the box. If you want consistent design tokens without the overhead of a utility class framework, Open Props is the simplest path to a polished design system.

Sources

All versions and facts below were checked on 2026-05-29.