grammar-style
v0.6.1
Published
A zero-runtime token resolution engine for design systems.
Maintainers
Readme
Build your design tokens once, and map them instantly into your favorite styling tools with strictly typed, auto-completing safety. Stop writing runtime token interpolations and embrace statically generated, strictly verified CSS variables.
📖 Table of Contents
- Getting Started
- Concept:
defineGrammar - Built-in Primitives:
size - Options
- Primitives
- Semantics
- Modes
- Responsive
- The Power of
token() - Media Queries:
media&breakpoint - Adapters
🚀 Getting Started
npm install grammar-styleGenerate a grammar.config.ts boilerplate file right in your project:
npx grammar-initIf you are using strict zero-runtime sandboxed libraries (like Linaria or Vanilla Extract) and want to use the global media exports, generate your cached tokens:
npx grammar-style generate💡 Pro-Tip: Chain the Generator! Because
mediabreakpoints are globally cached in yournode_modules, you should automate this. Add it to yourpackage.jsonscripts so they regenerate automatically every time you start your development server!{ "scripts": { "dev": "grammar-style generate && <your-framework-dev-command>", "build": "grammar-style generate && <your-framework-build-command>" } }
📓 Concept: defineGrammar
The core of grammar-style is defineGrammar, the typed definition engine. Design systems often suffer from disconnected tokens across different surfaces. defineGrammar forces your config to strongly adhere to two layers: Primitives and Semantics.
- Primitives are raw values (your Hex hues, absolute spacings, etc).
- Semantics are contextual mappings describing how those primitives apply to your UI (e.g.
brand.primary).
By enforcing this separating structure, any invalid mapping simply fails your TS compiler!
import { defineGrammar } from "grammar-style"
export const config = defineGrammar({
primitives: {
color: {
stone: {
900: "#1A1A1A",
100: "#E6E6E6",
},
brand: "#FF007F",
},
},
semantics: {
color: {
background: "color.stone.100",
surface: "color.stone.900",
primary: "color.brand",
},
},
})
// Binds autocomplete safely everywhere
declare module "grammar-style" {
export interface Register {
theme: typeof config
}
}⬆️ Back to Top
📏 Built-in Primitives: size
The size primitive is special. It acts as a pre-populated grid scale built perfectly around strict UI layouts.
The px to rem Bridge
You access tokens using developer-friendly pixel numbers (e.g. size.16), but the compiled output strictly emits natively responsive rems (1rem). This gives you the mental clarity of working with absolute layouts without sacrificing the fluid scalability and accessibility of generic rem units.
Allowed Constraints
Grid sizes map tightly. Small sizes allowed are: 1, 2, 4, 6, 8, 10, 12, 16. Beyond that, all sizes must be multiples of 4 up to 3000. You can't enter size.15 as it breaks the rules of structural scaling!
Mathematical Negatives
grammar-style uniquely supports dynamic negative polarity without duplicating tokens.
-size.16 natively compiles directly to literal math without CSS variables: -1rem
Zero-Bloat CSS
Instead of injecting 700+ size variables into global CSS which balloons performance, or relying on complex background file scanners, grammar-style inherently bypasses the CSS Variable map completely for dimension primitives. It perfectly evaluates your target layout integers directly into strict literal math inline natively (token("size.16") evaluates instantly to strictly "1rem"). Your :root CSS stays completely empty of size primitives, ensuring lightning-fast static performance.
⬆️ Back to Top
🎛️ Options
The options block lets you overwrite the core rules of your token validation.
| Option | Default Value | Description |
| :--------------- | :--------------------------------------------------------------------------------------------------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| opacities | [10, 20, 40, 60, 80, 100] | Restricts allowed transparency fractions. Supply an array of mapping numerals (e.g. [10, 50]) to redefine opacity strings natively (/10, /50) across your semantic object bindings. |
| breakpoints | sm: "size.640"md: "size.768"lg: "size.1024"xl: "size.1280"xxl: "size.1536" | Standard responsive layout endpoints. Implicitly generates Max variants for every key natively (e.g. lgMax safely maps to scaling rem math emitting max-width: calc(64rem - 1px) to avoid cross-breakpoint layout collisions natively). Map exclusively to custom size.* primitives to override. |
| modes | ["dark", "light"] | Strings enforcing structurally validated root wrapper themes mapping safely toward conditional CSS elements natively like [data-theme="dark"]. |
| useStrictSizes | true | Enforces mathematical constraints (size.4, size.16). Passing false removes geometric compilation locks, allowing arbitrary tokens like size.15 to be safely passed anywhere inside your configuration or token("size.15") utility seamlessly without breaking Type-Checking. |
import { defineGrammar } from "grammar-style"
export const config = defineGrammar({
options: {
// Customizes the opacity scale to allow /5 and /50 fractions
opacities: [5, 10, 20, 40, 50, 60, 80, 100],
// Allows deep validation for explicit alternative root subsets
modes: ["dark", "light", "high-contrast"],
// Overrides default layout boundaries perfectly mapped to structural `size.*` grids
breakpoints: {
md: "size.800",
lg: "size.1000",
xl: "size.1200",
},
},
// ...
})Overriding Breakpoints
If you override a natively defined breakpoint key (e.g. lg: "size.1400"), grammar-style perfectly preserves all other structural keys while safely calculating and generating your new lgMax fluid mapping fallback locally!
However, if your configuration injects custom keys completely ignoring the native boundaries (e.g. palm: "size.600"), the compiler intelligently infers that you want to rewrite your layout rules from scratch. It effortlessly builds and spins up your custom palmMax scaling, but fully purges the standard sm, md, lg targets. This gives you a pristine Typescript autocomplete interface completely devoid of bloat or leftover unused defaults.
⬆️ Back to Top
🧱 Primitives
The foundation structural layer. Here you dictate your hardcoded absolute properties—like your hex swatches (#ff0000), spacing logic, or unconstrained radii layers. You map these as standard nested objects (e.g. border: { radius: "size.24" }).
⚠️ Size is Geometrically Locked: The built-in
sizeprimitive dictionary serves as a structural foundation explicitly designed to keep scaling aligned perfectly to UI layout box models. Passing{ size: { ... } }internally inside yourprimitivesoverrides will strictly trip an architectural compiler lock forcing a local build crash.
export const config = defineGrammar({
// ...
primitives: {
color: {
stone: {
900: "#1A1A1A",
500: "#808080",
100: "#E6E6E6",
},
brand: "#FF007F",
},
shadow: {
soft: "0 size.4 size.12 -size.4 color.stone.900/10",
hard: "0 size.8 size.24 color.stone.900/25",
},
},
// ...
})⬆️ Back to Top
🧠 Semantics
Your contextual intent mapping. Semantic mappings cannot resolve to hardcoded string strings—they must actively route to valid underlying Primitive dot-paths.
Deep Nesting
Configure categorized hierarchies to build logical taxonomies like primitives.color.blue.900. The TS compiler elegantly tracks every deeply nested layer natively, strictly enforcing your semantic lookups via concatenated dot.path string identifiers (e.g. "color.blue.900").
Dynamic Color Opacities
You can invoke opacity fractions directly on your token paths. For example, mapping "color.brand/50" automatically converts the underlying primitive to an rgba() CSS variable with 0.5 opacity statically.
export const config = defineGrammar({
//...
semantics: {
color: {
surface: {
// Evaluate into deep mapping layers effortlessly!
elevated: "color.stone.900",
inset: "color.stone.100",
},
// You can mix and match custom deeply nested paths alongside logic tokens seamlessly
text: {
muted: "color.stone.500",
accent: "color.brand/50",
},
},
},
})⬆️ Back to Top
🌓 Modes
Use the modes object to map dark mode, high contrast, or unique theme variants natively. Simply provide a TypeScript-enforced deep-partial of your semantics object inside modes: { dark: { ... } }. grammar-style will automatically emit these overrides bound tightly behind native [data-theme="dark"] { ... } wrappers in your CSS output.
export const config = defineGrammar({
//...
modes: {
// Automatically wraps CSS emissions in [data-theme="dark"]
// Deep validations bind ensuring you match the identical nested structure found in semantics!
dark: {
color: {
surface: {
elevated: "color.stone.100", // Strict primitive dot-path boundary checks
inset: "color.stone.900",
},
},
},
},
})⬆️ Back to Top
📱 Responsive
Handle media queries across standardized boundaries by partially overriding semantics exactly like modes.
export const config = defineGrammar({
//...
semantics: {
spacing: {
base: "size.24",
half: "size.12",
double: "size.48",
},
},
responsive: {
// The `<key>Max` sub-variant was natively mapped and injected!
mdMax: {
spacing: {
base: "size.20",
half: "size.10",
double: "size.40",
},
},
// The explicit endpoint targets `@media (min-width: ...)` maps exactly as expected!
lg: {
spacing: {
base: "size.32",
half: "size.16",
double: "size.64",
},
},
},
})⬆️ Back to Top
🎨 The Power of token()
Once your grammar is defined, you'll need to safely consume those tokens inside your standard components. The token() utility translates your Typescript semantic paths exactly into the native CSS variables grammar-style constructs under the hood.
Why use it?
- 100% Type-Safe: It throws compilation errors if you misspell a path, preventing "silent styling failures."
- Zero Runtime Overhead: It evaluates tokens synchronously into native declarative CSS
var()maps. - Dynamic Opacities & Native Math: You can composite standard CSS expressions natively inside a single string (
token("color.brand/50"),token("-size.400"), or even isolated functions liketoken("calc(size.400 * 2)")). The engine safely parses, resolves, and constructs the necessary variable structures during compilation without bloating your DOM with extra utility classes!
1. Template Strings (Linaria, Styled Components, Emotion)
const Box = styled.div`
/* Emits: rgba(var(--color-surface-rgb), 0.5) */
color: ${token("color.surface/50")};
/* Emits: calc(25rem * 2) */
margin: ${token("calc(size.400 * 2)")};
/* Emits: -25rem */
bottom: ${token("-size.400")};
`2. Object Styles (Vanilla Extract, StyleX, Panda CSS, Emotion)
export const boxStyle = style({
// Emits: rgba(var(--color-surface-rgb), 0.5)
color: token("color.surface/50"),
// Emits: calc(25rem * 2)
margin: token("calc(size.400 * 2)"),
// Emits: -25rem
bottom: token("-size.400"),
})3. Inline React Styling
export const Box = () => (
<div
style={{
// Emits: rgba(var(--color-brand-rgb), 0.5)
color: token("color.brand/50"),
}}
>
Safely typed static inline styles!
</div>
)Grammar Style validates any token strings natively at compile time enforcing literal string compliance instantly!
// Typos in root primitives or paths are caught instantly with suggestions
/* Error: invalid token: 'sz.12' Did you mean 'size.12'? */
const p = token("sz.12")
// Trying to access mapped variables that don't exist in your semantics
/* Error: invalid token: 'color.foo' */
const bg = token("color.foo")
// Breaking geometric scaling constraints unless `options.useStrictSizes` was set to false
/* Error: invalid token: 'size.15' */
const width = token("size.15")⬆️ Back to Top
📐 Media Queries: media & breakpoint
In addition to token(), grammar-style natively exposes a media object directly from the root package. It is a Proxy that lazily inspects your grammar.config breakpoints and automatically evaluates them into properly formatted, reusable @media query strings.
⚠️ Strict Sandbox Warning (Linaria, Vanilla Extract, Next.js): If your styling framework restricts Node built-ins (
fs,jiti) during compilation, importingmediadirectly will throw a Sandbox Error. To fix this, runnpx grammar-style generate(or add it to yourpackage.jsondev script) to dump a statically readable token cache!
1. Template Strings (Linaria, Styled Components)
import { media, token } from "grammar-style"
export const Footer = styled.footer`
/* Emits correctly formatted native `@media` block! */
${media.lg} {
padding: ${token("size.16")};
}
`2. Object Syntax (Vanilla Extract, StyleX, Emotion)
For CSS-in-JS libraries that use object syntax and expect raw condition strings (like Vanilla Extract or StyleX) instead of full @media wrappers, we also export a native breakpoint object.
import { breakpoint, token } from "grammar-style"
export const boxStyle = style({
"@media": {
/* Emits strictly the condition block: `(min-width: 62.5rem)` */
[breakpoint.lg]: {
padding: token("size.32"),
},
},
})Want to see full architecture breakdowns? Check out the Media Queries Documentation for full implementation guides spanning Vanilla Extract, StyleX, Emotion, Linaria, and more alongside deep dives into Client-Side restrictions and explicit lazy-caching rules!
⬆️ Back to Top
⚙️ Adapters
grammar-style is framework-agnostic. We don't care what compiler you use to handle your resulting string literals. We offer out-of-the-box Adapters to inject your grammar.config.ts into multiple major styling frameworks seamlessly.
Why adapters?
Most styling tools (like Tailwind, Panda, Linaria, StyleX) require tokens to be formatted and nested into their specific compiler shape. Instead of rewriting your tokens four times for four different compilers over the lifetime of a project, build your dictionary heavily once in defineGrammar and export it into your runtime compiler framework instantly.
Available natively out-of-the-box:
Just drop the adapter directly into your framework's provider or config plugin layer and move on to building your application!
🔒 Strict Sandbox Mode (Optional)
If your framework evaluates code in a strictly isolated AST sandbox (like Linaria or Vanilla-Extract inside Next.js/Vite) that bans Node.js disk reading, you can optionally pass your config object explicitly into any adapter to completely bypass dynamic disk reads!
import config from "../../grammar.config.ts"
const grammar = createLinariaTheme(config)Example 1: Tailwind CSS
Drop grammar-style natively into your tailwind.config.ts configuration to seamlessly map all semantic variables and base primitives into standard Tailwind utility classes (e.g., text-primary, mt-brand, rounded-soft).
// tailwind.config.ts
import createTailwindTheme from "grammar-style/tailwind"
// grammar-style automatically reads your local grammar.config.ts!
const grammar = createTailwindTheme()
export default {
content: ["./src/**/*.{js,ts,jsx,tsx}"],
theme: grammar.theme,
}Example 2: Panda CSS
Consume natively transformed objects safely into Panda's strict static definitions mapping. Because Panda compiles natively at build-time, you securely extend your panda.config.ts effortlessly!
// panda.config.ts
import { defineConfig } from "@pandacss/dev"
import createPandaTheme from "grammar-style/panda"
const grammar = createPandaTheme()
export default defineConfig({
// ...
theme: {
extend: {
tokens: grammar.panda.primitives,
semanticTokens: grammar.panda.semantics,
},
},
})Using something else? Check out the Adapters Documentation for full implementation guides spanning Styled Components, Vanilla Extract, StyleX, Emotion, and Linaria!
⬆️ Back to Top
🏗️ Building Strict Custom Utilities
Since defining an entire internal design system is inherently contextual, you'll likely want to create custom opinionated abstractions (like complex shadows or layout builders). Grammar Style natively exports a powerful generic types module making it mathematically proven to secure custom utilities:
import { token, type TokenPath } from "grammar-style"
// Dynamically extracts autocomplete strictly explicitly targeting colors!
const border = (
color: TokenPath<"color">,
placement: "top" | "bottom" | "all" = "all",
): string => {
if (placement === "top") {
return `inset 0 ${token("size.1")} 0 ${token(color)}`
}
if (placement === "bottom") {
return `inset 0 ${token("-size.1")} 0 ${token(color)}`
}
return `inset 0 0 0 ${token("size.1")} ${token(color)}`
}
// Strictly Typed ✅
border("color.danger.400", "top")
// Type Error: Argument of type '"size.4"' is not assignable to "color.*" ❌
border("size.4", "bottom")