An Introduction to Open Props: What It Ships, What It Costs, and Who Should Skip It

What Open Props ships as design tokens via custom properties, how it differs from utility-class frameworks, and who should skip it.

You are mid-refactor on a module that has shipped nine shades of gray and four spacing values that sort of mean the same thing, and someone on the team just pasted a URL into the chat with the words “no build step” attached. That URL is Open Props, and the promise is real: a CSS file of design tokens as custom properties you can drop into any stylesheet. No PostCSS config, no npm install, no purge step. Your module can start using –size-3 and –color-blue-5 in the next commit. The short version: Open Props gives you a well-curated set of design tokens in the form of CSS custom properties, the full bundle costs about 5 KB gzipped, and it is not a utility-class framework and not a component library. Skip it if you wanted either of those things.

The moment that sells most people is watching a colleague delete hand-written SCSS variables and replace them with a single @import. The old system had –spacing-md and –spacing-lg and –spacing-xl, but nobody could remember which one was 16px and which one was 24px. The answer changed depending on which file you opened. Open Props replaces that with a number scale: –size-1 is 0.25rem, –size-2 is 0.5rem, –size-3 is 1rem, and so on up to –size-15. You do not have to agree with the scale. You do have to admit it is easier to reason about than a dictionary of names that have drifted out of sync with their values.

Using Open Props In Your Stylesheet

Here is what you actually do. Import the minified CSS file, or import the individual modules from npm if you want to stay lean, and then use the custom properties in your own CSS. The classic pattern is a rule that needs spacing, a color, and a motion effect, all in one declaration block:

/* No build step. This is plain CSS. */
@import "https://unpkg.com/open-props/open-props.min.css";

.card {
  padding: var(--size-3);           /* 1rem */
  background: var(--color-blue-5);  /* oklch(55.5% 0.2 255) */
  animation: var(--animation-fade-in);
}

That is the entire integration story. You configure nothing. You generate nothing. You run no watch task. The custom properties are defined on :where(:root), which gives them specificity of 0,0,0. That is a deliberate design choice: it makes overrides trivial. To get a different blue for your brand, re-declare it on your own :root or on a local container.

How Overrides Work In The Cascade

The override behaviour is where Open Props differs from utility-class frameworks in a way that matters for real projects. With Tailwind, changing the spacing scale means editing a configuration file and rebuilding. With Open Props, you write a custom property declaration that wins the cascade through inheritance. Here is a concrete example:

/* Open Props defines --color-blue-5 on :where(:root).
   Your override can live on a local scoping element. */
:root {
  --color-blue-5: oklch(45% 0.25 255); /* brand blue, darker */
}

.dark-card {
  --color-blue-5: oklch(30% 0.2 255); /* even darker for this context */
  background: var(--color-blue-5);
}

The cascade does the work. The custom property inherits down to .dark-card, the local declaration overrides the :root one, and the element picks up the value without state management, without class toggling, without JavaScript. Utility-class frameworks cannot do this. A utility class is a single selector with a fixed value; you cannot re-scope it. This is the design token approach at its best: the token is a live variable, not a compiled constant.

Bundle Size And The JIT Plugin

Now the part the marketing page skips: what does it cost? The full Open Props bundle is a single CSS file, open-props.min.css. It is roughly 25 KB minified and about 5 KB gzipped. Brotli gets it a little smaller still. But that 5 KB is the full bundle with every colour, every size, every motion preset, every shadow, every z-index layer. If you need only a handful of tokens, you ship unused CSS unless you reach for the PostCSS plugin called postcss-jit-props. That plugin scans your source files for var() references and includes only the custom properties you actually use. Without it, the 5 KB is the floor, and the ceiling is whatever your own CSS adds on top.

Here is the comparison that matters. A table, because numbers are the only honest way to make this point.

Delivery methodSource sizeGzipped sizeWhat you get
Open Props full bundle~25 KB minified~5 KBAll design tokens: 247 colours, 16 sizes, 6 shadows, 12 animations, aspect ratios
Open Props with postcss-jit-propsDepends on usageOften under 1 KBOnly the tokens you reference in var()
Tailwind (purged, typical component set)~50-100 KB raw~10-20 KBUtility classes you used, plus a few you did not
Tailwind (unpurged)~200-400 KB~30-50 KBThe entire utility class surface, most unused

The shipped bytes are the honest cost, and here is the surprise for people coming from utility-first: Open Props is often smaller than a purged Tailwind output for the same set of elements. The reason is that design tokens are naturally compressible. Colour values repeat the same oklch() structure. Size values are variants of a few base units. Gzip and brotli both exploit that repetition aggressively. Utility classes are also repetitive, but they repeat entire selectors, which are longer strings than a custom property value. The practical result: Open Props wins on raw download size for most projects, and it wins even more when you use the JIT plugin.

The Real Cost: Token Vocabulary And Cascade Discipline

Size is not the only cost metric. The real cost of Open Props is the learning curve of the token vocabulary. The real risk is that you will misuse the cascade in a way that is hard to debug. The failure mode goes like this: you import Open Props, you use --size-3 in an element, and then you decide you want a smaller gap in one place. You write --size-3: 0.5rem on a local container. It works. Then someone else on the team writes the same override on a different container. Now the same token means two different sizes in two different places, and nobody knows which one is authoritative. That is the downside of inheritance: it is powerful, and it is footgun-prone. The mitigation is to treat overrides as rare and documented, or to use @layer to create a specific override layer that is verboten for day-to-day work.

Speaking of @layer, Open Props plays nicely with cascade layers. You will want this if you are integrating it into a design system that already has its own layer strategy. The @layer rule lets you declare an explicit order for your stylesheets. Put Open Props in a base layer and your element styles in a higher layer, and the cascade order becomes explicit rather than implicit. This is the modern way to handle the problem that specificity hacks used to solve. With layers, the ordinal position in the @layer list wins over specificity. You do not need to fight with !important or increasingly specific selectors. Open Props itself does not inject any layers. It is your job to decide where it sits. The fact that it uses :where(:root) means it is already at the lowest specificity and will lose to anything you write unless you deliberately put it in a lower layer.

When To Skip Open Props

Now let us talk about what Open Props is not. That is the fastest way to decide if you should skip it. Open Props is not a component library. There are no buttons, no cards, no modals, no form styles. It is a set of design tokens and a few motion presets. If you need pre-built UI elements, you are looking at the wrong tool; go get a component library or a framework with a component system. Open Props is also not a utility-class framework in the Tailwind sense. You will not write class="mt-4 bg-blue-500" and get a styled element. You will write your own CSS and use the custom properties as the raw materials. If your team has standardised on utility-class authoring, or if you want the convenience of not writing any CSS at all, Open Props will feel like extra work rather than less.

There is a third category of developer who should skip Open Props, and it is the easiest to miss: the team whose design system cannot be expressed as a set of tokens. If your brand has a colour that changes based on the time of day, or a spacing scale that is different on every page, or a set of shadows that depend on the surrounding context in a way that no single value can capture, then a fixed token set will fight you. You can override custom properties at runtime. That is complexity you are introducing into the design. The clean solution is either to have a genuinely token-based system or to not use tokens at all and write the values inline. Half-token systems are worse than either extreme.

The Token Vocabulary

The design token format itself is worth a moment, since it is a point of confusion. Open Props uses the double-dash prefix convention, the CSS standard for custom properties. Values are grouped by category: --color-* for colours, --size-* for dimensions, --ease-* for easing functions, --shadow-* for shadows, --layer-* for z-index, --ratio-* for aspect ratios, and so on. The colours are based on the oklch() colour space, which is perceptually uniform. Lightness steps are visually even, unlike the older hsl() or hex systems where the same numeric step can look like a tiny jump in one range and a huge jump in another. The colour palette is 19 base hues times 13 lightness steps, giving you 247 distinct colour custom properties. Each one is named like --color-blue-5 or --color-red-9. That is a lot of colours. Most projects will use a fraction of them, but the existence of the full scale means you do not have to invent your own names or values for edge cases.

The motion presets are another strong reason to reach for the library. Open Props ships a dozen animation custom properties: –animation-fade-in, –animation-fade-out, –animation-scale-up, –animation-scale-down, –animation-slide-in-right, –animation-shake-x, –animation-spin, –animation-pulse, and a few more. Each one is a complete animation definition, so you can use it directly: animation: var(–animation-fade-in);. Since they are custom properties, you can override the duration or the easing by redefining the underlying variables. The easing functions are also exposed as –ease-* tokens, so you can build your own effects with the same feel as the presets. This saves you from downloading a separate animation library, and it is all in that 5 KB gzipped bundle.

The aspect ratio presets solve a real problem that has nothing to do with design tokens and everything to do with layout stability. The –ratio-square, –ratio-landscape, –ratio-portrait, –ratio-widescreen, –ratio-ultrawide, and –ratio-golden tokens give you named values for the aspect-ratio property. This CSS feature reserves space before content loads and reduces layout shift. If you have ever fought with a layout that jumps when an image loads, you know why this matters. The tokens are numbers: –ratio-square is 1, –ratio-widescreen is 16/10, –ratio-golden is 1.618. Having them named means you do not have to remember or type the magic numbers.

Browser Support And Fallbacks

Let me address the browser support question. Open Props requires CSS custom properties, which have been Baseline since 2017. Every current browser supports them. If you are targeting browsers older than 2016, you have a problem, but you probably have a bigger problem than your design tokens. The fallback technique for ancient browsers is to define a static fallback value before the custom property declaration, like this:

.card {
  padding: 1rem; /* fallback for ancient browsers */
  padding: var(--size-3); /* progressive enhancement */
}

That pattern works for any property that takes a value. It is the recommended approach. If you want to be more thorough, wrap the Open Props import in a @supports guard, but that is usually overkill. The fallback pattern covers the only case that matters.

The one thing to push back on is the claim that Open Props is a zero-cost drop-in. It is zero build step. It is not zero cognitive load. You have to learn the token names. You have to learn which scale is which. The size scale alone has multiple sub-scales: –size-000 through –size-15 is the base scale, but there is also –size-fluid-1 through –size-fluid-10 for fluid sizes that scale with the viewport, –size-content-1 through –size-content-3 for content width constraints, and –size-header-1 through –size-header-3 for header heights. That is a lot of names to memorise, and the documentation, while decent, is not a tutorial. Budget an afternoon to read the token list and experiment before you feel fluent.

And that is the honest trade. Open Props is not a framework that does your thinking for you. It is a box of high-quality raw materials you assemble yourself. The payoff: you keep full control of your CSS, you get a consistent design vocabulary across your project, and you can update the tokens in one place and see the changes cascade through every element that uses them. The cost: you have to do the assembly, and you have to be disciplined about not creating a mess of local overrides.

Debugging Custom Properties

Here is the thing about the cascade that most introductory articles gloss over. It is the thing that will bite you if you are not careful. Cascading custom properties inherit by default. A token defined on :root is available everywhere. A token defined on a specific element is available only inside that element's subtree. That is the feature that enables the .dark-card override shown earlier. It is the same feature that makes debugging hard. When a token does not have the value you expect, the first thing you check is the inheritance chain: where is the closest ancestor that defines this token? The second thing you check is the specificity of the selector that defines it. The third thing you check is the @layer order, if you are using layers. If all three are correct and the value is still wrong, you have a typo in the var() call or in the token name.

The typo failure is more common than you think. var(–size-3) is not the same as var(–size_3). A misspelled token silently resolves to the fallback value, or to nothing, depending on whether you provided a fallback. The reason it is silent: the CSS parser does not error on unknown custom properties. It treats the var() call as invalid at computed-value time, and the property falls back to its initial value if no fallback is specified. This is a feature of the spec, but it is a debugging nightmare when you are chasing a spacing bug that turns out to be a missing hyphen. The mitigation is to use the postcss-jit-props plugin or a linter that checks for undefined custom properties. The simplest mitigation is to be careful.

Another failure mode that bites people coming from preprocessors is the assumption that custom properties are like SCSS variables. They are not. SCSS variables are compile-time constants. Custom properties are runtime values that participate in the cascade and inheritance. That means you can do things with custom properties you cannot do with SCSS variables: change a token value in a media query or in response to a class change, and the change propagates to all descendants that use it. It also means you cannot use a custom property in a place that expects a compile-time value, like a media query itself. You cannot write @media (min-width: var(–size-3)) because media queries do not accept var(). That is a limitation of the custom property spec. It is one of the first things people hit when they try to use Open Props for responsive design. The workaround: define the breakpoints as regular CSS values, not as custom properties.

There is also a subtle interaction with color-mix() you should know about if you plan to use Open Props colours in more sophisticated ways. The oklch() colours in Open Props are full-colour values, but you can use them inside color-mix() to create tints and shades, like color-mix(in oklch, var(–color-blue-5) 70%, white). This gives you a way to derive a hover state or a disabled state from the base token. That would require a separate token in a system that only ships flat values. The color-mix() function is supported in all modern browsers. It is a good companion to Open Props because it lets you keep the token count low while still having flexibility.

Now, the FAQ. This is the part where the practical questions get answered.

If you are reading this and thinking about adopting Open Props for a serious project, the one thing to do first is a spike. Take a small element. Port it from your current system to Open Props. Measure the difference in bytes, in lines of CSS, and in your own frustration level. The answer will be personal, and the personal answer is the right one. A token system you hate using is not a win because it is smaller on the wire.

The final honest caveat is about longevity. Open Props is maintained by a single prolific developer, Adam Argyle, who is well known in the CSS community. It is not backed by a large corporation or a foundation. The project has been around for years and has a healthy community, but the maintenance risk is real. Adopting Open Props means adopting a dependency on a person, not an institution. The upside: the core technology is not proprietary. It is CSS custom properties. If the project dies, you can copy the token values into your own system in an afternoon. The tokens are data, and data is portable.