Introducing Arvia: a design system compiler for the web

Fausto95

Fausto95

2026-06-13T08:14:12Z

4 min read

Introducing Arvia

Arvia is a design system compiler. You describe your tokens, themes, and components in .arv files, and a compiler turns each one into three things before your app ever loads:

  • plain CSS — the kind you'd write by hand,
  • a typed styling API — call a function, get your class names, with autocomplete on every variant,
  • TypeScript types — so a bad variant is a red squiggle, not a bug in production.

The whole thing rests on one idea: styles are compiled, not computed. Because the compiler sees your entire design system up front, it can hand you typed props, catch typo'd token names, warn about dead variants, and power editor autocomplete — while the browser only ever gets static CSS and some class-name strings. Nothing styling-related runs at runtime.

The rest of this post is a tour of the language.

A quick taste

Here's a complete, working Arvia file: two tokens, a reusable focus-ring recipe, and a button with two variant dimensions.

theme {
  color {
    primary = #635bff;
    danger = #e5484d;
  }
}

recipe FocusRing {
  outline: none;
  &:focus-visible {
    outline: 2px solid color.primary;
    outline-offset: 2px;
  }
}

component Button {
  base {
    display: inline-flex;
    align-items: center;
    border-radius: 8px;
    cursor: pointer;
    use FocusRing;
  }

  variants {
    size {
      sm { padding: 4px 12px; }
      lg { padding: 8px 20px; }
    }
    tone {
      primary { background: color.primary; color: white; }
      danger { background: color.danger; color: white; }
    }
  }

  defaults { size: sm; tone: primary; }
}
Enter fullscreen mode Exit fullscreen mode

Import it and you get a plain function. Call it and you get class names back — one string per element the component styles — fully type-checked:

import { Button } from "./button.arv";

const styles = Button({ size: "lg", tone: "danger" });
// styles.root === "Button_root_0p4oom Button_size_lg_root_0p4oom Button_tone_danger_root_0p4oom"

<button className={styles.root}>Delete</button>;

Button({ size: "xl" });
//        ~~~~ Type error: '"xl"' is not assignable to '"sm" | "lg"'
Enter fullscreen mode Exit fullscreen mode

One thing that's deliberately missing: there's no <Button> React component. Arvia gives you styling APIs, not UI components — you keep full control of your markup, your accessibility attributes, and your event handlers. The function does no style injection, holds no context, builds no runtime theme object. It mashes strings together. That's it.

Works with your framework

The generated JavaScript has zero dependencies, so anything that can render a class attribute can use it. There are official Vite plugins for React, Preact, and Vue, and the same .arv file feeds all of them.

React (and Preact, identically):

import { Button } from "./button.arv";

export function DeleteButton() {
  const styles = Button({ tone: "danger" });
  return <button className={styles.root}>Delete</button>;
}
Enter fullscreen mode Exit fullscreen mode

Vue, in a <script setup> single-file component:

<script setup lang="ts">
import { Button } from "./button.arv";
const styles = Button({ tone: "danger" });
</script>

<template>
  <button :class="styles.root">Delete</button>
</template>
Enter fullscreen mode Exit fullscreen mode

And because the output is just strings, it's happy in Svelte, Solid, Lit, or a server-rendered template too — there's no React-specific magic to leave behind.

Multi-part components work the same way, with a class per slot:

component Card {
  slots { root {} media {} body {} }

  base {
    border-radius: 12px;
    &:hover { media { transform: scale(1.02); } }
  }
}
Enter fullscreen mode Exit fullscreen mode
const card = Card();
// card.root, card.media, card.body — one class string each
Enter fullscreen mode Exit fullscreen mode

That &:hover { media { … } } is the group-hover pattern, by the way — hovering the card scales the media slot, and it compiles down to a plain descendant selector. No extra "group" class threaded through your markup.

More than a token bag

Tokens and variants are the start. The language also covers the parts where design systems usually get messy:

  • Compound variants — rules that only kick in when several variants combine (size: sm and tone: danger).
  • Theme modes — declare modes: light | dark;, write @dark { … } overrides, and tokens compile to CSS variables that flip with data-arvia-theme or the OS setting.
  • Responsive and container variants — drive a variant from a breakpoint or a container's width.
  • States, recipes, keyframes, global resets, standalone style utilities, and component-scoped tokens — each one is a real construct, not a workaround.

And everything gets checked. Fat-finger color.primry and the compiler stops you, with a "did you mean color.primary?", before a single line of CSS is written.

Where it's at

Arvia is free and open source — docs, playground, and everything else live at arvia.dev:

  • Vite plugins for React, Preact, and Vue, with hot reload.
  • A real editor experience. The arviahq.arvia VS Code extension ships a language server: hover a token to see its value in each mode (and MDN docs when you hover a CSS property), jump to a token's definition across files, rename a token everywhere at once, see color swatches and inline hints showing resolved values, and get diagnostics with one-click fixes.
  • Types with no files on disk. arvia-tsc and the bundled TypeScript plugin give .arv imports real types through virtual declarations — no generated .d.ts junk in your tree.
  • Generators for Storybook stories and token-catalog docs, straight from your components.

Give it a spin

The playground compiles .arv right in your browser — type, and watch the preview, the CSS, and the .d.ts update live. Everything above can be pasted in.

When you're ready for a real project:

npm install -D @arviahq/vite-plugin-react
Enter fullscreen mode Exit fullscreen mode

Then head to Installation, or take the full tour.