@react-hive/honey-css
v1.15.0
Published
A lightweight CSS tokenizer and parser that produces a simple AST for custom CSS processing.
Maintainers
Readme
@react-hive/honey-css
A lightweight CSS tokenizer and structural parser that generates a minimal, predictable AST for custom CSS processing.
The honey-css is designed as a focused foundation for building:
- CSS transformers
- Lightweight preprocessors
- CSS-in-JS compilers
- Custom styling engines
All without pulling in heavyweight ecosystems like PostCSS.
It gives you structure — not policy.
✨ Why honey-css?
Modern CSS tooling is incredibly powerful — but often excessively complex.
Many parsers aim to support the entire CSS specification, plugin systems, validation layers, and edge-case semantics. That’s valuable — but sometimes unnecessary.
Often, what you actually need is:
- A small tokenizer
- A predictable AST
- Nested rule support
- Clean grammar boundaries
- Composable parsing primitives
The honey-css focuses on the practical sweet spot:
- 🍯 Minimal surface area
- 🎯 Deterministic, structural output
- 🌳 Small and easy-to-traverse AST
- 🧩 Designed for composition and transformation
- ⚡ Fast and dependency-light
It intentionally performs structural parsing only — not full CSS validation.
That makes it ideal for transformation pipelines where predictability and simplicity matter more than spec completeness.
If you're building your own styling layer, compiler, or design-system engine, honey-css gives you the core building blocks — without unnecessary overhead or abstraction.
✨ Features
- ✅ Tokenizes raw CSS into a structured, readable token stream
- ✅ Parses tokens into a minimal, predictable AST
- ✅ Fully supports nested rules and nested at-rules
- ✅ Correctly handles real-world CSS constructs:
- Declarations (
color: red;) - Complex selectors (
.btn:hover,:is(...), attribute selectors) - At-rules (
@media,@layer,@keyframes,@import, etc.) - Parameter groups (
url(...),var(...), nested functions) - Quoted strings (
content: "hello") - Block comments (
/* ... */)
- Declarations (
- ✅ Distinguishes directive at-rules (
body: null) from block at-rules - ✅ Resilient parsing with safe recovery (skips unknown or malformed tokens)
- ✅ Deterministic output — no hidden transformations during parsing
- ✅ Small surface area with composable low-level parsing utilities
- ✅ Designed for transformation pipelines and CSS-in-JS compilers
- ✅ Tiny, fast, dependency-light, and easy to extend
📦 Installation
Install with pnpm (recommended):
pnpm add @react-hive/honey-css🚀 Quick Start
Tokenizing CSS
import { tokenizeCss } from "@react-hive/honey-css";
const tokens = tokenizeCss(`
.btn {
color: red;
padding: 12px;
}
`);
console.log(tokens);Output:
[
{ "type": "text", "value": ".btn" },
{ "type": "braceOpen" },
{ "type": "text", "value": "color" },
{ "type": "colon" },
{ "type": "text", "value": "red" },
{ "type": "semicolon" },
{ "type": "text", "value": "padding" },
{ "type": "colon" },
{ "type": "text", "value": "12px" },
{ "type": "semicolon" },
{ "type": "braceClose" }
]🧠 parseCss
Parses a full CSS string into a minimal Honey AST.
This is the high-level entry point that wires together:
tokenizeCsscreateCssTokenCursorparseCssNodes
It performs structural parsing only — it does not validate CSS identifiers or enforce full CSS specification rules. The goal is predictable AST generation suitable for transformations and CSS-in-JS engines.
What It Supports
At the root level, parseCss recognizes:
- Declarations →
color: red; - Rules →
.btn { ... } - At-rules →
@media (...) { ... } - Directive at-rules →
@import url("x.css"); - Nested structures inside rules and at-rules
Stray semicolons and unknown tokens are skipped safely.
Recovery Behavior
The parseCss is designed to be resilient:
- Unknown tokens are skipped
- Malformed constructs do not throw
- Parsing continues whenever possible
- Infinite loops are prevented via forward progress
This makes it ideal for transformation pipelines where partial input may occur.
Example
import { parseCss } from "@react-hive/honey-css";
const ast = parseCss(`
color: red;
.btn {
padding: 8px;
@media (max-width: 600px) {
display: none;
}
}
`);
console.log(ast);Output:
{
"type": "stylesheet",
"body": [
{ "type": "declaration", "prop": "color", "value": "red" },
{
"type": "rule",
"selector": ".btn",
"body": [
{ "type": "declaration", "prop": "padding", "value": "8px" },
{
"type": "atRule",
"name": "media",
"params": "(max-width: 600px)",
"body": [
{ "type": "declaration", "prop": "display", "value": "none" }
]
}
]
}
]
}How It Works Internally
The parseCss delegates all grammar handling to:
parseCssNodes(cursor, { stopAtBraceClose: false })This means:
- Root-level and block-level parsing share the same engine
- Grammar logic lives in one place
- The parser stays small and composable
When to Use It
Use parseCss when:
- You want a complete AST for transformation
- You are building a CSS-in-JS engine
- You are implementing custom at-rule processors
- You need a structured CSS without heavy dependencies
If you need finer control, you can directly use:
parseCssNodesparseCssBlockparseCssRuleparseCssAtRule
🧭 Token Cursor Utilities
For writing your own parser logic, transformers, or custom readers, honey-css provides a small helper:
createCssTokenCursor
The cursor is a lightweight abstraction over the token stream that enables:
- lookahead (
peek) - sequential reading (
next) - safe assertions (
expect) - speculative parsing (
mark+reset) - reading chunks (
readUntil) - skipping tokens for recovery (
skipUntil)
Cursor API
The createCssTokenCursor(tokens) returns an object with:
peek()- Look at the current token without consuming itnext()- Consume the current token and advanceisEof()- Returns true when the token stream is finishedexpect(type)- Assert the next token type (throws if mismatch)mark()/reset(mark)- Create checkpoints for speculative parsingreadUntil([...])- Read combinedtext/string/paramsuntil a stop tokenskipUntil([...])- Skip tokens until a stop token is found
Example: Reading a Declaration Manually
import { tokenizeCss, createCssTokenCursor } from "@react-hive/honey-css";
const tokens = tokenizeCss('color: red;');
const cursor = createCssTokenCursor(tokens);
// Read property name
const prop = cursor.readUntil(['colon']);
cursor.expect('colon');
// Read value
const value = cursor.readUntil(['semicolon']);
cursor.expect('semicolon');
console.log(prop); // "color"
console.log(value); // "red"Example: Skipping Until a Block Ends
cursor.skipUntil(["braceClose"]);
cursor.expect("braceClose");This is extremely useful for:
- parser error recovery
- ignoring unsupported syntax
- skipping unknown nested blocks
🔎 readCssSelector
When building custom rule parsing logic, you often need to read a selector safely until {.
The readCssSelector reconstructs the selector from tokens while preserving:
- pseudo selectors (
:hover) - pseudo elements (
::before) - attribute selectors (
[data-id="x"]) - combinators (
>,+,~) - pseudo functions (
:not(...),:nth-child(...))
It stops before consuming the { token.
Example
import {
tokenizeCss,
createCssTokenCursor,
readCssSelector,
} from "@react-hive/honey-css";
const tokens = tokenizeCss(`
button:not(:disabled):hover {
opacity: 0.5;
}
`);
const cursor = createCssTokenCursor(tokens);
const selector = readCssSelector(cursor);
cursor.expect("braceOpen");
console.log(selector);
// "button:not(:disabled):hover"🔁 readCssKeyOrSelector
Inside a block, grammar becomes ambiguous:
selector { ... }
property: value;To resolve this, use readCssKeyOrSelector.
It:
- Speculatively parses a selector
- Accepts it only if { follows
- Otherwise, rewinds and reads a declaration key
Example
import {
tokenizeCss,
createCssTokenCursor,
readCssKeyOrSelector,
} from "@react-hive/honey-css";
const tokens = tokenizeCss(`
.btn { color: red; }
`);
const cursor = createCssTokenCursor(tokens);
const keyOrSelector = readCssKeyOrSelector(cursor);
console.log(keyOrSelector); // ".btn"🧱 parseCssDeclaration
When building a custom parser on top of the token cursor, you often need to parse a single declaration inside a rule block.
The parseCssDeclaration handles this safely and consistently.
It:
- Expects the cursor to be positioned at the
:token - Reads the value until
;or} - Supports missing trailing semicolons
- Preserves strings and nested params like
var(...)orurl(...)
Example
import {
tokenizeCss,
createCssTokenCursor,
parseCssDeclaration,
} from "@react-hive/honey-css";
const tokens = tokenizeCss(`
color: var(--primary, red);
`);
const cursor = createCssTokenCursor(tokens);
// Read property name
const prop = cursor.readUntil(["colon"]);
// Parse declaration
const declaration = parseCssDeclaration(cursor, prop);
console.log(declaration);Output:
{
type: "declaration",
prop: "color",
value: "var(--primary, red)"
}🧩 resolveCssSelector
When implementing nested rules (like in CSS-in-JS engines), child selectors must be resolved against their parent selector.
The resolveCssSelector helper performs this safely and predictably.
It:
- Performs full Cartesian expansion of comma-separated selector lists
- Replaces explicit parent references (
&) - Creates descendant relationships when
&is not present - Preserves complex selectors, including:
- Pseudo-classes (
:hover) - Pseudo-elements (
::before) - Attribute selectors (
[data-x="a,b"]) - Nested selector functions (
:is(...),:not(...)) - Combinators (
>,+,~)
- Pseudo-classes (
Basic Example
import { resolveCssSelector } from "@react-hive/honey-css";
resolveCssSelector(".child", ".scope");
// → ".scope .child"
resolveCssSelector("&:hover", ".btn");
// → ".btn:hover"Comma Expansion
Both parent and child selectors may contain comma-separated lists.
resolveCssSelector(".a, .b", ".scope");
// → ".scope .a, .scope .b"
resolveCssSelector(".x, .y", ".a, .b");
// → ".a .x, .a .y, .b .x, .b .y"Parent Reference (&)
If the child selector contains &, it is replaced with the parent selector.
resolveCssSelector("& + &", ".item");
// → ".item + .item"
resolveCssSelector("&:hover, .icon", ".btn, .card");
// → ".btn:hover, .btn .icon, .card:hover, .card .icon"Complex Selectors
The resolver safely handles nested commas inside functions and attribute selectors.
resolveCssSelector(':is(.a, .b)', '.scope');
// → ".scope :is(.a, .b)"
resolveCssSelector('[data-x="a,b"]', '.scope');
// → ".scope [data-x=\"a,b\"]"🧾 stringifyCss
After transforming or generating a CSS AST, you can convert it back into a compact CSS string using stringifyCss.
This is the final stage of the honey-css pipeline.
What It Does:
- Converts the AST back into valid compact CSS
- Removes empty declarations (
value.trim() === "") - Removes empty rules (rule body becomes empty after stringification)
- Removes empty block at-rules (at-rules with
body: []that stringify to nothing) - Preserves directive at-rules (
body: null) and prints them with;
Example
import { stringifyCss } from "@react-hive/honey-css";
const ast = {
type: "stylesheet",
body: [
{
type: "rule",
selector: ".btn",
body: [
{ type: "declaration", prop: "padding", value: "8px" },
{ type: "declaration", prop: "color", value: "red" },
],
},
],
};
const css = stringifyCss(ast);
console.log(css);
// ".btn{padding:8px;color:red;}"Directive VS block at-rules
The stringifyCss treats at-rules with body === null as directive-style rules and serializes them with a trailing semicolon.
import { stringifyCss } from "@react-hive/honey-css";
const ast = {
type: "stylesheet",
body: [
{
type: "atRule",
name: "charset",
params: '"UTF-8"',
body: null,
},
],
};
console.log(stringifyCss(ast));
// '@charset "UTF-8";'Block at-rules (body !== null) are serialized using curly braces: @name params{...}.
import { stringifyCss } from "@react-hive/honey-css";
const ast = {
type: "stylesheet",
body: [
{
type: "atRule",
name: "media",
params: "(min-width: 100px)",
body: [],
},
],
};
console.log(stringifyCss(ast));
// ""🧱 Low-Level Block & Rule Parsers
When building your own parser pipeline on top of createCssTokenCursor, you can use the lower-level block and rule utilities directly.
🧩 parseCssAtRule
Parses an at-rule from the token stream.
Supports both block-style and directive-style at-rules:
@media (max-width: 768px) { ... }
@keyframes spin { ... }
@import url("file.css");What is does:
- Merges space-delimited and params tokens correctly
- Returns
body: nullfor directive at-rules - Returns
body: []for empty block at-rules
Example
import {
tokenizeCss,
createCssTokenCursor,
parseCssAtRule,
} from "@react-hive/honey-css";
const tokens = tokenizeCss(`
@media (max-width: 768px) {
color: red;
}
`);
const cursor = createCssTokenCursor(tokens);
const atRule = parseCssAtRule(cursor);
console.log(atRule);Output:
{
type: "atRule",
name: "media",
params: "(max-width: 768px)",
body: [
{
type: "declaration",
prop: "color",
value: "red"
}
]
}🧱 parseCssBlock
Parses the contents of a { ... } block.
Resolves grammar ambiguity inside blocks:
selector { ... }
property: value;
@rule ...It:
- Stops at the matching
} - Skips stray semicolons (
;) safely - Delegates:
- declarations →
parseCssDeclaration - nested rules →
parseCssRule - nested at-rules →
parseCssAtRule
- declarations →
Example
import {
tokenizeCss,
createCssTokenCursor,
} from "@react-hive/honey-css";
import { parseCssBlock } from "@react-hive/honey-css";
const tokens = tokenizeCss(`
{
color: red;
.child { padding: 8px; }
}
`);
const cursor = createCssTokenCursor(tokens);
cursor.expect("braceOpen");
const nodes = parseCssBlock(cursor);
console.log(nodes);Output:
[
{ type: "declaration", prop: "color", value: "red" },
{
type: "rule",
selector: ".child",
body: [
{ type: "declaration", prop: "padding", value: "8px" }
]
}
]🧾 parseCssRule
Parses a CSS rule body for a given selector.
- Expects the cursor to be positioned at
{ - Delegates body parsing to
parseCssBlock - Preserves the selector exactly as provided
Example
import {
tokenizeCss,
createCssTokenCursor,
} from "@react-hive/honey-css";
import { parseCssRule } from "@react-hive/honey-css";
const tokens = tokenizeCss(`
{
color: red;
}
`);
const cursor = createCssTokenCursor(tokens);
const rule = parseCssRule(cursor, ".btn");
console.log(rule);Output:
{
type: "rule",
selector: ".btn",
body: [
{ type: "declaration", prop: "color", value: "red" }
]
}🌳 AST Overview
The AST is intentionally minimal and easy to traverse.
Stylesheet Root
{
type: "stylesheet",
body: HoneyCssNode[]
}Declaration Node
{
type: "declaration",
prop: "padding",
value: "12px"
}Represents:
padding: 12px;Rule Node
{
type: "rule",
selector: ".child:hover",
body: [...]
}Represents:
.child:hover {
opacity: 0.5;
}At-Rule Node
{
type: "atRule",
name: "media",
params: "(max-width: 768px)",
body: [...]
}Represents:
@media (max-width: 768px) {
color: red;
}🎯 Use Cases
The honey-css intentionally exposes low-level parsing primitives so you can build exactly the tooling you need — without inheriting a large opinionated ecosystem.
It is a foundation, not a framework.
You can use it to:
- 🛠 Build your own CSS compiler or transformer pipeline
- 🧩 Implement custom at-rules (e.g.
@honey-media,@theme, etc.) - 🔁 Flatten nested rules for CSS-in-JS engines
- 🎨 Create design-system or token processors
- 🔍 Perform AST transformations before stringification
- 🧠 Build domain-specific styling languages on top of CSS
- 📦 Implement scoped or isolated CSS engines
- ⚡ Generate minimal production-ready CSS from structured input
Because the parser is structural (not validating), it is predictable and easy to control.
There are no hidden behaviors — transformations are explicit and composable.
What honey-css Is Not
- ❌ Not a full CSS spec implementation
- ❌ Not a PostCSS replacement
- ❌ Not a validation engine
- ❌ Not a plugin-heavy ecosystem
Instead, it is a focused, lightweight foundation for people who want to build their own styling engines — cleanly, deterministically, and without unnecessary abstraction.
📄 License
MIT © Mike Aliinyk
Part of the React Hive ecosystem 🐝
