☕ CoffeeHaml

Haml structure. CoffeeScript semantics. React runtime.
Zero overhead. 50% fewer tokens than JSX.

GitHub npm Quick Start

0
Runtime KB
~50%
Token Reduction
52 kB
Compiler Size
25+
Features

Write This

-# Dashboard.chaml
import { observer } from 'mobx-react-lite'
import { useMemo } from 'react'

@observer
%div.page
  %header
    %h1= "Welcome, #{user.name}"
    %nav{ role: 'primary' }
      - for link in navLinks
        %a{ href: link.url }= link.label

  %main
    .dashboard-grid
      = widgets.map (widget) ->
        %Widget{ widget, onDelete: -> handleDelete(widget.id) }

    %aside
      - if alerts.length > 0
        .alert-banner{ role: 'alert' }
          - for alert in alerts
            %p= alert.message
      - else
        %p.empty-state No alerts — all clear.

Get This

import { jsx, jsxs, Fragment } from "react/jsx-runtime";
import { observer } from "mobx-react-lite";
import { useMemo } from "react";

export default observer(function Dashboard(props) {
  return jsxs("div", { className: "page" },
    jsxs("header", null,
      jsx("h1", null, "Welcome, " + user.name),
      jsx("nav", { role: "primary" },
        ...navLinks.map(link => jsx("a", { href: link.url }, link.label))
      )
    ),
    jsxs("main", null,
      jsx("div", { className: "dashboard-grid" },
        ...widgets.map(widget => jsx(Widget, { widget, onDelete: () => handleDelete(widget.id) }))
      ),
      jsx("aside", null,
        ...(alerts.length > 0 ? [
          jsxs("div", { className: "alert-banner", role: "alert" },
            ...alerts.map(alert => jsx("p", null, alert.message))
          )
        ] : [
          jsx("p", { className: "empty-state" }, "No alerts — all clear.")
        ])
      )
    )
  );
});

Quick Start

Scaffold a project v0.6.0

$ npx coffeehaml init

  ☕ CoffeeHaml init — project scaffold
  Working directory: /my-react-app
  Detected: package.json, Vite, TypeScript

  Add coffeehaml as devDependency? (Y/n):
  Configure Vite plugin? (Y/n):
  Create sample .chaml component? (Y/n):
  Add build/watch scripts to package.json? (Y/n):

Or install manually

$ npm install coffeehaml

Vite

// vite.config.ts
import coffeehaml from 'coffeehaml/vite';

export default {
  plugins: [coffeehaml()],
};

CLI

# One-shot compile
$ npx coffeehaml compile app.chaml -o app.js

# Watch mode (v0.5.0)
$ npx coffeehaml watch src/ --wrap component

Supports .chaml, .coffeehaml, and .cohaml extensions.

All Features

FeatureStatus
Elements, components, implicit divs
.class / #id shorthand
{attr: val} + spread {props...}
Inline = expression output
= expression continuation v0.5.0
= expr -> arrow continuation v0.6.0
- statement continuation v0.4.2
- if / - unless / - else / - else if
- for item in items.map()
- while loops
Optional chaining props.gyro?.roll
Haml / HTML comments
:filter plugins (markdown, asciidoc)
Prologue passthrough (import, @decorator)
Component / HOC wrapping
Vite plugin (HMR + Fast Refresh v0.6.0)
CLI --wrap flag v0.5.0
CLI watch mode v0.5.0
CLI init scaffold v0.6.0
Source-located error messages v0.5.0
Multi-error parser recovery v0.6.0
Source maps
CoffeeScript expression compilation✓ (peer dep)

Node API

import { compile, compileFile } from 'coffeehaml';

const result = compile('%div Hello', {
  filename: 'app.chaml',
  wrap: 'component',   // 'none' | 'component' | 'observer' | string[]
  sourceMap: true,
});

console.log(result.code);
// → import { jsx } from "react/jsx-runtime";
//   export default function App(props) {
//     return jsx("div", null, "Hello");
//   }

Design Principles

Documentation

Grammar Reference · Syntax Examples · Architecture · AST Specification · GitHub README