- 🗄️ Real database storage - Postgres or SQLite with full SQL queries
- 🔄 Migration system - Up/down migrations with rollback support
- 🎯 Conflict detection - Catch specificity issues and shorthand/longhand conflicts
- 🔍 Query your styles -
SELECT * FROM styles WHERE resp_kind = 'media' - 📊 Full audit trail - Migration history with checksums and timestamps
- ✅ Complete CSS support - @layer, @container, @supports, @font-face, @keyframes, @property, @scope
- 🚀 Zero runtime - Compiles to plain CSS
- 🔧 Universal - Works with React, Vue, Angular, vanilla HTML (CommonJS or ESM)
-- migrations/up/001_init.sql
CREATE TOKEN 'brand/500' VALUE #2266ee;
CREATE SELECTOR btn AS AND(E('button'), C('primary'));
CREATE STYLE SELECTOR btn (
background = token('brand/500'),
color = #fff
);/* Output: dist/icbincss.css */
:root {
--brand-500: #2266ee;
}
button.primary {
background: var(--brand-500);
color: #fff;
}Requirements: Node.js 18+, Works with any project (React, Vue, vanilla HTML), Supports both CommonJS and ESM
Because CSS doesn't have migrations. You can't roll back styles. You can't query them. You can't detect conflicts before they ship. You just push changes and hope nothing breaks.
What if styles were data? You could:
- Version them properly (with migration history and checksums)
- Query them (
SELECT * FROM styles WHERE selector = 'btn') - Analyze them (find unused tokens, detect conflicts, check specificity)
- Roll them back (proper up/down migrations)
- Audit them (who changed what, when, why)
What if CSS had the tooling backend developers take for granted? Migrations, schema validation, conflict detection, query languages, database tooling, version control that actually makes sense.
That's what this is. CSS as a database. SQL as the interface. Migrations as the workflow.
If you've ever looked at a 10,000 line CSS file and thought "I wish I could just SELECT the problem," this is for you.
npm install -D i-cant-believe-its-not-css
npx icbincss initWhy install first? Installing adds it to your
package.json(version control + team collaboration), and makesnpxuse your local version. You'll also need the package installed to use Vite/Next.js integrations or to import the compiler functions.
This creates an icbincss/ directory with migrations and database folders.
Edit icbincss/migrations/up/001_init.sql:
-- Create design tokens
CREATE TOKEN 'brand/500' VALUE #2266ee;
CREATE TOKEN 'space/4' VALUE 16px;
-- Define selectors
CREATE SELECTOR card AS C('card');
CREATE SELECTOR btn AS AND(E('button'), C('primary'));
-- Create styles
CREATE STYLE SELECTOR card (
background = #fff,
padding = token('space/4'),
border_radius = 8px
);
CREATE STYLE SELECTOR btn (
background = token('brand/500'),
color = #fff,
padding = 12px 24px
);
-- Responsive styles
ALTER STYLE SELECTOR card
WHERE width >= 768px
SET padding = 24px;npx icbincss migrate up
npx icbincss buildThis compiles your SQL to dist/icbincss.css. Include it in your HTML:
<link rel="stylesheet" href="/dist/icbincss.css" /><div class="card">
<button class="primary">Click me</button>
</div>That's it! You're using SQL-generated CSS.
┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Write SQL │ ───> │ CSV DB │ ───> │ Compile CSS │ ───> │ Browser │
│ Migration │ │ Storage │ │ Output │ │ Renders │
└─────────────┘ └─────────────┘ └─────────────┘ └─────────────┘
- Write SQL: Author styles as
.sqlmigration files - Apply Migrations: Run
migrate upto update CSV database - Compile: Build CSS from CSV database (source of truth)
- Ship: Regular CSS file, zero runtime
┌──────────────────────────────────────────────────────────────┐
│ Your Project │
├──────────────────────────────────────────────────────────────┤
│ │
│ icbincss/ │
│ ├── migrations/ │
│ │ ├── up/*.sql ← You write SQL here │
│ │ └── down/*.sql │
│ │ │
│ └── db/ ← CSV database │
│ ├── migrations.csv (source of truth) │
│ ├── tokens.csv │
│ ├── selectors.csv │
│ ├── layers.csv │
│ └── styles.csv │
│ │
├──────────────────────────────────────────────────────────────┤
│ ICBINCSS Engine │
├──────────────────────────────────────────────────────────────┤
│ │
│ 1. Parser (Peggy) │
│ SQL text → AST │
│ │
│ 2. Migration Engine │
│ AST → CSV writes │
│ │
│ 3. Compiler │
│ CSV → CSS │
│ │
│ 4. Integrations │
│ ├── Vite Plugin (virtual modules + HMR) │
│ └── Next.js Plugin (webpack + file watcher) │
│ │
└──────────────────────────────────────────────────────────────┘
│
▼
┌───────────────┐
│ Browser │
│ (regular CSS) │
└───────────────┘
Design tokens compile to CSS custom properties:
CREATE TOKEN 'brand/500' VALUE #2266ee;
CREATE TOKEN 'space/lg' VALUE 32px;:root {
--brand-500: #2266ee;
--space-lg: 32px;
}Reference tokens in styles:
CREATE STYLE SELECTOR btn (
background = token('brand/500'),
padding = token('space/lg')
);button {
background: var(--brand-500);
padding: var(--space-lg);
}Define selectors using SQL functions (no CSS syntax):
-- Basic selectors
CREATE SELECTOR btn AS C('btn'); -- .btn
CREATE SELECTOR heading AS E('h1'); -- h1
CREATE SELECTOR user_id AS ID('user'); -- #user
-- Compound selectors
CREATE SELECTOR primary_btn AS AND(E('button'), C('primary')); -- button.primary
-- Parent-child relationships
CREATE SELECTOR card AS C('card');
CREATE SELECTOR card_header AS CHILD(card, C('header')); -- .card > .header
CREATE SELECTOR card_text AS DESC(card, C('text')); -- .card .text
-- Pseudo-classes and pseudo-elements
CREATE SELECTOR btn_hover AS AND(btn, P('hover')); -- .btn:hover
CREATE SELECTOR btn_before AS AND(btn, PE('before')); -- .btn::before
-- Attribute selectors
CREATE SELECTOR data_role AS ATTR('data-role', 'admin'); -- [data-role="admin"]
CREATE SELECTOR starts_with AS ATTR('href', 'https', '^='); -- [href^="https"]Create and modify styles:
-- Create styles for a selector
CREATE STYLE SELECTOR card (
padding = 16px,
background = #fff,
border = 1px solid #ddd
);
-- Add a property (only if not already set)
ALTER STYLE SELECTOR card ADD margin = 8px;
-- Set a property (overwrite if exists)
ALTER STYLE SELECTOR card SET padding = 24px;
-- Responsive styles
ALTER STYLE SELECTOR card
WHERE width >= 768px
SET padding = 32px;
-- Container queries
ALTER STYLE SELECTOR card
WHERE container main > 600px
SET border_radius = 16px;
-- Supports queries
ALTER STYLE SELECTOR grid
WHERE supports(display: grid)
SET display = grid;
-- Delete specific property
DELETE FROM style_props WHERE selector = card AND prop = 'margin';
-- Drop all styles for a selector
DROP STYLE SELECTOR card;Reversible change scripts for styling evolution:
icbincss/
├── migrations/
│ ├── up/
│ │ └── 20250118_120000__add_buttons.sql
│ └── down/
│ └── 20250118_120000__add_buttons.sql
└── db/
├── migrations.csv ← Migration history
├── tokens.csv ← Token definitions
├── selectors.csv ← Selector definitions
└── styles.csv ← Style rules (source of truth)
Create migration:
npx icbincss migrate create --name=add_buttons.sqlUP migration (migrations/up/20250118_120000__add_buttons.sql):
CREATE TOKEN 'accent/600' VALUE #1a4fd8;
ALTER STYLE SELECTOR btn
SET background = token('accent/600');DOWN migration (migrations/down/20250118_120000__add_buttons.sql):
ALTER STYLE SELECTOR btn
SET background = token('brand/500');
DROP TOKEN 'accent/600';Apply:
npx icbincss migrate up # Apply next pending migration
npx icbincss migrate down # Revert last applied migration
npx icbincss migrate status # View migration stateControl cascade order with @layer:
-- Define layer order
CREATE LAYERS (reset, base, components, utilities);
-- Set current layer
SET LAYER = components;
CREATE STYLE SELECTOR card (
padding = 16px
);
-- This style goes in the "components" layerCSS output:
@layer reset, base, components, utilities;
@layer components {
.card {
padding: 16px;
}
}# Build CSS from database
npx icbincss build
# Watch for changes and rebuild
npx icbincss watch
# Migrations
npx icbincss migrate status
npx icbincss migrate create --name=homepage.sql
npx icbincss migrate up
npx icbincss migrate down
# Analysis
npx icbincss inspect card
npx icbincss doctor
npx icbincss query "SELECT style_props WHERE selector = card;"
# Database
npx icbincss db rebuild
npx icbincss db verify
npx icbincss db export-sql --dialect=postgres// vite.config.ts
import { defineConfig } from "vite";
import { icbincssVitePlugin } from "i-cant-believe-its-not-css";
export default defineConfig({
plugins: [icbincssVitePlugin()],
});// src/main.ts
import "virtual:icbincss.css";Edits to icbincss/**/*.sql trigger HMR updates in development. In production, CSS is emitted as a static asset.
ESM (next.config.mjs):
import { withICBINCSS } from "i-cant-believe-its-not-css";
export default withICBINCSS(
{ reactStrictMode: true },
{ outFile: "public/icbincss.css" }
);CommonJS (next.config.js):
const { withICBINCSS } = require("i-cant-believe-its-not-css");
module.exports = withICBINCSS(
{ reactStrictMode: true },
{ outFile: "public/icbincss.css" }
);Include in layout:
// app/layout.tsx (App Router)
export default function RootLayout({ children }) {
return (
<html>
<head>
<link rel="stylesheet" href="/icbincss.css" />
</head>
<body>{children}</body>
</html>
);
}The CSV files in icbincss/db/ are the authoritative data:
migrations.csv- Migration history (id, checksum, timestamp)tokens.csv- Design tokens with UUIDsselectors.csv- Selector definitions (stored as JSON)styles.csv- All style rules with context (media, container, supports)layers.csv- Layer definitions and order
UUIDs everywhere: Every entity has a deterministic UUIDv5 as its immutable primary key, separate from the human-readable name. This allows renaming without breaking foreign key relationships.
Database lifecycle:
┌──────────────────────────┐
│ migrations/up|down SQL │
└───────────────┬──────────┘
│ apply up/down
┌───────▼────────────┐
│ Migration Engine │
└───────┬───────┬────┘
├───────┴─────────────────────────────┬
writes appends
│ │
┌───────────────▼──────────────┐ ┌────────────────▼─────────────┐
│ CSV DB │ │ migrations.csv (history) │
│ (source of truth) │ └──────────────────────────────┘
│ tokens.csv │
│ selectors.csv │
│ layers.csv │
│ styles.csv │
│ + other at-rule tables │
└───────────────────────────────┘
│
├─────────────────────────────────┬
compile sync (optional)
│ │
┌───────▼──────┐ ┌──────────▼───────────┐
│ CSS File │ │ Postgres / SQLite │
│ (dist/*.css) │ │ (for analysis) │
└──────────────┘ └──────────────────────┘
Load your styles into Postgres for advanced querying and team collaboration.
Initialize database (one-time):
npx icbincss db init-postgres --database=icbincss_stylesThis creates:
- Postgres database
- Optimized schema (JSONB columns, GIN indexes, helpful views)
- Loads all CSV data
Sync after migrations:
npx icbincss migrate up
npx icbincss db sync-postgres --database=icbincss_stylesQuery examples:
-- View all styles with selector names
SELECT * FROM styles_full LIMIT 10;
-- Count styles by layer
SELECT layer_name, COUNT(*) FROM styles_with_layers GROUP BY layer_name;
-- Find all responsive rules
SELECT selector_id, resp_min, resp_max FROM styles WHERE resp_kind = 'media';
-- Query JSON selector definitions
SELECT name, def_json->>'kind' AS kind FROM selectors;
-- Get active tokens
SELECT * FROM active_tokens;Schema features:
- JSONB columns for selectors, font faces, keyframes (fast JSON queries)
- GIN indexes on all JSONB fields
- B-tree indexes on foreign keys
- Views:
styles_full,styles_with_selectors,active_tokens,applied_migrations
Connection options:
--database=mydb
--connection="postgresql://user:pass@host:port/db"
--host=localhost --port=5432 --user=postgres --password=secretCreate and sync a local SQLite database:
npx icbincss db sync-sqlite --file=icbincss/db/icbincss.sqliteThis creates (or updates) a SQLite database file with your styles. Open with DB Browser, TablePlus, or any SQLite client.
Sync after migrations:
npx icbincss migrate up
npx icbincss db sync-sqlite --file=icbincss/db/icbincss.sqliteQuery examples:
sqlite3 icbincss/db/icbincss.sqlite "SELECT * FROM styles WHERE resp_kind = 'media';"
sqlite3 icbincss/db/icbincss.sqlite "SELECT name, value FROM tokens;"Use cases:
- Local development and analysis
- Portable database file for sharing
- SQL queries without Postgres setup
- Integration with SQLite tools and GUIs
Generate optimized schema files for different databases:
# SQLite (TEXT columns, simple schema)
npx icbincss db export-sql --dialect=sqlite
# Postgres (JSONB columns, GIN indexes, views)
npx icbincss db export-sql --dialect=postgres
# MySQL (JSON columns, utf8mb4, InnoDB)
npx icbincss db export-sql --dialect=mysqlThis generates icbincss/db/{dialect}-init.sql files you can use to manually set up databases.
Detect conflicts and issues:
npx icbincss doctor
npx icbincss doctor --history # Include unreachable rules analysisChecks:
- Duplicate properties: Same selector/context defines a property multiple times
- Cross-context overrides: Property defined under different responsive conditions
- Specificity info: Shows specificity alongside selectors
- Shorthand/longhand conflicts: Detects
border+border-leftconflicts - Unknown tokens: Catches typos in
token()with suggestions - Unused tokens: Identifies tokens never referenced
- Unreachable rules (with
--history): Rules overridden by later migrations
Example output:
Cross-context overrides for .card (spec 0,1,0) → padding:
- (global)
- @media (min-width: 768px)
Shorthand/longhand conflict for .box:
- border (shorthand) and border-left (longhand) both defined
Unknown token 'brand/600' in selector btn
Did you mean 'brand/500'?
Check database integrity:
npx icbincss db verifyChecks:
- Referential integrity (styles → selectors, tokens, layers)
- No orphaned records
- Valid JSON in selector definitions
- No duplicate names
- Checksum drift warnings
View computed styles for a selector:
npx icbincss inspect card
npx icbincss inspect card --final # Show cascaded final valuesOutput:
Styles for .card:
@layer components:
padding: 16px
background: #fff
@media (min-width: 768px):
padding: 32px
Final cascaded:
padding: 32px
background: #fff
SQL-like introspection:
# Show properties for a selector
npx icbincss query "SELECT style_props WHERE selector = card AND width >= 768px;"
# Resolve selector to CSS
npx icbincss query "DESCRIBE SELECTOR btn;"| Feature | Status | Notes |
|---|---|---|
| Selectors | ✅ Full | Element, class, id, pseudo-classes, pseudo-elements, attributes, compound, combinators |
| Complex pseudos | ✅ Full | :is(), :where(), :not(), :has(), :nth-child() with validation |
| Nesting | ✅ Full | CHILD(), DESC(), AND() compile to flat CSS (100% browser support) |
| @media | ✅ Full | Width ranges, features (prefers-color-scheme, orientation, pointer, etc.) |
| @container | ✅ Size queries | Named containers, min/max, inline axis |
| @container style | Cannot combine with media/size in same WHERE | |
| @supports | ✅ Full | Can nest with media/container |
| @layer | ✅ Full | Declare order, set current layer |
| @scope | ✅ Full | SCOPED TO root [LIMIT limit] |
| @import | ✅ Full | CSS imports emitted at top |
| @font-face | ✅ Full | CREATE FONT_FACE family '...' (...) |
| @keyframes | ✅ Full | CREATE KEYFRAMES name (...) |
| @property | ✅ Full | Custom property registration |
| @page | ✅ Full | :first, :left, :right pseudo-pages |
| @counter-style | ✅ Full | Custom counter definitions |
| @font-feature-values | ✅ Full | OpenType feature control |
| @font-palette-values | ✅ Full | Color palette overrides |
| @starting-style | ✅ Full | Entry animations |
| CSS Variables | ✅ Full | Tokens compile to :root { --vars } |
CREATE TOKEN 'name' VALUE <css-value>;
DROP TOKEN 'name';-- Functions
C('class') -- .class
E('element') -- element
ID('id') -- #id
P('pseudo') -- :pseudo
PE('pseudo-element') -- ::pseudo-element
ATTR('name') -- [name]
ATTR('name', 'val') -- [name="val"]
ATTR('name', 'val', '^=') -- [name^="val"]
ATTR('name', 'val', '^=', 'i') -- [name^="val" i]
-- Composition
AND(a, b) -- Compound (a.b or a:b)
OR(a, b) -- Comma list (a, b)
CHILD(parent, child) -- Parent > child
DESC(ancestor, desc) -- Ancestor descendant
-- Named selectors
CREATE SELECTOR name AS <selector-expr>;
DROP SELECTOR name;-- Create
CREATE STYLE SELECTOR name (
prop = value,
prop = value
);
-- Modify
ALTER STYLE SELECTOR name ADD prop = value;
ALTER STYLE SELECTOR name SET prop = value;
-- Delete
DELETE FROM style_props WHERE selector = name AND prop = 'prop';
DROP STYLE SELECTOR name;
-- Responsive
ALTER STYLE SELECTOR name
WHERE <condition>
SET prop = value;-- Width queries
WHERE width >= 768px
WHERE width BETWEEN 768px AND 1024px
WHERE width < 1024px
-- Container queries
WHERE container main > 600px
WHERE container sidebar inline < 400px
-- Container style queries
WHERE container theme style(display: grid)
-- Media features
WHERE prefers_color_scheme = dark
WHERE orientation = landscape
WHERE pointer = coarse AND hover = none
WHERE resolution >= 2dppx
WHERE color_gamut = p3
WHERE forced_colors = active
-- Supports
WHERE supports(display: grid)
-- Combine with AND
WHERE width >= 768px AND prefers_color_scheme = darkCREATE LAYERS (layer1, layer2, layer3);
SET LAYER = layer2;-- Font Face
CREATE FONT_FACE family 'Inter' (
src = url('/fonts/Inter.woff2') format('woff2'),
font_weight = 400
);
-- Keyframes
CREATE KEYFRAMES fade_in (
'0%' ( opacity = 0 ),
'100%' ( opacity = 1 )
);
-- Property
CREATE PROPERTY '--theme-color' (
syntax = '<color>',
inherits = true,
initial_value = #000
);
-- Raw CSS (escape hatch)
RAW "@supports (display: flex) { .box { display: flex; } }";# Project setup
npx icbincss init
# Build & watch
npx icbincss build
npx icbincss watch
# Migrations
npx icbincss migrate status
npx icbincss migrate create --name=<name>.sql
npx icbincss migrate up
npx icbincss migrate down
# Analysis
npx icbincss inspect <selector>
npx icbincss inspect <selector> --final
npx icbincss doctor
npx icbincss doctor --history
npx icbincss query "<query>"
# Database
npx icbincss db rebuild
npx icbincss db verify
npx icbincss db export-sql --dialect=<postgres|mysql|sqlite>
npx icbincss db init-postgres --database=<name>
npx icbincss db sync-postgres --database=<name>
npx icbincss db sync-sqlite --file=<path>Create icbincss.config.json in your project root:
{
"outFile": "dist/icbincss.css",
"strictSemicolons": false,
"tokenVarPrefix": "",
"defaultLayers": []
}Options:
outFile- Output path for compiled CSS (relative to project root or absolute)strictSemicolons- Require;at end of every statement (default:false)tokenVarPrefix- Prefix for CSS custom properties (e.g.,"ux-"→--ux-brand-500)defaultLayers- Pre-declare layers if not all defined in SQL
For convenience, you can define tokens and selectors in dedicated files instead of migrations:
icbincss/
├── tokens.sql ← Token definitions
├── selectors.sql ← Selector definitions
└── migrations/
├── up/
└── down/
tokens.sql:
CREATE TOKEN 'brand/500' VALUE #2266ee;
CREATE TOKEN 'brand/600' VALUE #1a4fd8;
CREATE TOKEN 'space/sm' VALUE 8px;
CREATE TOKEN 'space/md' VALUE 16px;selectors.sql:
CREATE SELECTOR btn AS AND(E('button'), C('primary'));
CREATE SELECTOR card AS C('card');
CREATE SELECTOR card_header AS CHILD(card, C('header'));These files are loaded before migrations during watch and db rebuild. Prefer migrations for any change you want versioned.
Compilation is fast. Benchmark with 1,000 selectors + rules:
npm run bench
# Example output:
# rules=1000
# sql_len=170KB css_len=98KB
# parse_ms=26.4 compile_ms=11.5 total_ms=37.9Adjust size: node scripts/bench.cjs --n=2000
Q: Does this work with CommonJS projects?
A: Yes! Both require() and import work natively:
// CommonJS
const { withICBINCSS } = require("i-cant-believe-its-not-css");
// ESM
import { withICBINCSS } from "i-cant-believe-its-not-css";Q: Can I use this in production?
A: It compiles to plain CSS with zero runtime overhead. Evaluate the SQL-based workflow for your team's needs.
Q: Why SQL?
A: Migrations, version control, introspection tools, and treating styles as queryable data. If you prefer database workflows, it feels natural.
Q: How does nesting work?
A: Use CHILD(), DESC(), AND() for composition. Output is flat CSS (100% browser support) instead of native CSS nesting (91% support). Semantically identical, better compatibility.
Q: What about specificity conflicts?
A: Run npx icbincss doctor to detect conflicts. Within the same bucket (selector + layer + context), last migration wins.
Q: Can I edit the CSV files directly?
A: Not recommended. Apply migrations instead. The CSV files are the compiled output of your migrations.
Q: What if I need features not supported?
A: Use the RAW escape hatch to inject literal CSS for experiments.
- Vite Basic - Vite integration with HMR
- Next.js Basic - Next.js Pages Router integration
- DSL Samples - Comprehensive SQL syntax examples
MIT
If you liked this project, follow me for more absurd experiments:
- 🦋 Bluesky
- 🐦 X/Twitter
- 📺 YouTube @Codinhood
- 🎨 CodePen
See more absurd projects at codinhood.com
