Haml structure. CoffeeScript semantics. React runtime.
Zero overhead. 50% fewer tokens than JSX.
-# 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.
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.") ]) ) ) ); });
$ 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):
$ npm install coffeehaml
// vite.config.ts import coffeehaml from 'coffeehaml/vite'; export default { plugins: [coffeehaml()], };
# 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.
| Feature | Status |
|---|---|
| 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) |
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"); // }
.chaml, not generated JSGrammar Reference · Syntax Examples · Architecture · AST Specification · GitHub README