Transforming HTML to JSX

  • June 5, 2025
  • go, jsx, react and tool

restache extends HTML with curly braces, letting you write React components in HTML that compile to JSX.

Overview

The project aims to offer a simpler alternative to JSX with built-in support for React hooks planned in the roadmap.

Status

🚨 This is early-alpha. It is intended for Go or React devs who are into DSLs and code generation, people building UI builders, or anyone who wants to cut down on JSX boilerplate and help shape the project through feedback.

Example: Dashboard app

The tetsuo/dashboard repository includes an example setup with ESBuild and a small set of components built with Restache that demonstrate the current state of the art.

👉 See it online – the design's actually pretty sleek.


Try it yourself

Here's an example

Output:

Restache v0 draft

Restache extends HTML5 with Mustache-like syntax to support variables, conditionals, and loops.

Variables

Variables provide access to data in the current scope using dot notation.

Accessing component props

Output:

They can only appear within text nodes or as full attribute values inside a tag.

✅ can insert variable {here}, or <img href={here}>.

When

Renders block when expression is truthy

Output:

Unless

Renders block when expression is falsy

Output:

Range

Iterates over a list value

Output:

Range blocks create a new lexical scope. Inside the block, {name} refers to the local object in context; outer scope variables are not accessible.


⚠️ Control structures must wrap well-formed elements (or other well-formed control constructs), and cannot appear inside tags.


Comments

There are two types of comments

Output:

Standard HTML comments are removed from the generated output.

Restache comments compile into JSX comments.


Generating JSX

Restache transpiler generates a React JSX component from each input and handles JSX-specific quirks where necessary.

Fragment wrapping

Multiple root elements are wrapped in a Fragment

Output:
  • This also applies within control blocks.
  • If you only have one root element, then a Fragment is omitted.

Case conversion

React requires component names to start with a capital letter and prop names to use camelCase. In contrast, HTML tags and attribute names are not case-sensitive.

To ensure compatibility, Restache applies the following transformations:

  • Elements written in kebab-case (e.g. <my-button>) are automatically converted to PascalCase (MyButton) in the output.
  • Similarly, kebab-case attributes (like disable-padding) are converted to camelCase (disablePadding).
kebab-case 🔜 React case

Output:

ℹ️ Attributes starting with data- or aria- are preserved as-is, in line with React's conventions.

Attribute name normalization

Certain attributes are automatically renamed for React compatibility

Output:

Attribute renaming only occurs when the attribute is valid for the tag. For instance, formaction isn't renamed on <img> since it isn't valid there.

However, some attributes are renamed globally, regardless of which element they're used on. These include:

  • All standard event handler attributes (onclick, onchange, etc.), which are converted to their camelCased React equivalents (e.g. onClick, onChange)
  • Common HTML aliases and reserved keywords like class and for, which are renamed to className and htmlFor
  • Certain accessibility- and editing-related attributes, such as spellcheck and tabindex

See table.go for the full list.

Implicit key insertion in loops

When rendering lists, Restache inserts a key prop automatically, assigning it to the top-level element or to a wrapping Fragment if there are multiple root elements.

Key is passed to the root element inside a loop

Output:
If there are multiple roots, it goes on the Fragment

Output:
Manually set key when there's single root

Output:

Importing other components

Restache supports an implicit module system where custom elements (i.e., tags that are not part of the HTML spec) are automatically resolved to file-based components without requiring explicit imports.

Component imports are inferred from the tag names. The following examples show how different components are resolved:

HTML JSX Import path
<my-button> <MyButton> ./MyButton
<ui:card-header> <UiCardHeader> ./ui/CardHeader
<main> <main> Not resolved, standard tag
<ui:div> <UiDiv> ./ui/div

Component resolution

Any tag that isn't a known HTML element is treated as a component.

When the parser encounters such a tag, it follows these steps:

1. Check for namespace

Restache first determines whether the tag uses a namespace. Namespaced tags contain a prefix and a component name (e.g., <ui:button>).

2. Standard custom tags

If the tag does not contain a namespace (e.g., <my-button>):

  • Restache first looks for an exact match in the build configuration's tagMappings.
  • If no mapping is found, it falls back to searching in the current directory. For example, <my-button> could resolve to either ./my-button or ./MyButton.

3. Namespaced tags

If the tag does contain a namespace (e.g., <ui:button>):

  • Restache first checks the tagPrefixes configuration. If a prefix (e.g., ui) is defined, it uses the mapped path. For example, if mui is mapped to @mui/material, then <mui:app-bar> resolves to @mui/material/AppBar.

  • If no mapping is found, it attempts to resolve the component from a subdirectory: e.g., <ui:button> → ui/Button.js, ui/button.jsx, ui/button.tsx, etc.

ℹ️ Note: Standard HTML tags are not resolved as components, even if identically named files exist in the current directory. However, namespacing can override this behavior. For example, <ui:div> will resolve to ./ui/div (or ./ui/Div), even though <div> is a native HTML element.


ESBuild integration

Restache includes an ESBuild plugin that makes integration simple and easy in Go:

  • Register .html loader and pass it to the plugin
  • Plugin uses Restache compiler to convert to .jsx
  • No runtime library needed; everything is transpiled ahead of time

The dashboard project includes a working build script (build.go).

There's currently no support for Node.JS environment, but planned.


Roadmap

Integration with React hooks

Currently, most logic must live in a .jsx file next to the corresponding .html file.

The long-term plan is to introduce a minimal set of expressions into the language itself so that common hooks like useState, useContext, and useSelector from Redux can be inferred from markup and compiled automatically.


Relational and logical expressions

Support for predicates inside range blocks is planned for v1.

{#products: price < 100 && inStock}
  <product-card />
{/products}

This could be compiled as a filter, and potentially mapped to things like backend queries (e.g. MongoDB, CouchDB, Algolia, ...) as well.


Smarter code generation

Before adding expressions, there's still a lot that can be optimized with the syntax that's already in place.

Consider pattern matching. Since Restache doesn't support expressions beyond dot notation, patterns have to be represented structurally. For example, using an object with a mutually exclusive key set:

{
  home: {...},
  settings: undefined,
  products: undefined
}

Then in the template:

{?home}    <home />    {/home}
{?settings}<settings />{/settings}
{?products}<products />{/products}

Compiles to:

if (props.home) <Home />
if (props.settings) <Settings />

This results in an O(n) operation instead of an O(1) equality check like switch(route), but the difference is negligible unless you're dealing with many conditions.

Future versions of Restache will generate if/else or switch statements when keyed unions are used, along with other optimizations such as merging adjacent {?x} and {^x} blocks into a single conditional.


More codegen targets

Plans include:

  • Emitting a real JavaScript AST instead of raw JSX strings
  • Supporting things other than React
  • Option to emit TSX

When TypeScript is used and type information is available at build time, more advanced optimizations may be possible. But even without that, structural inference allows detecting optionals and iterables, which can be used to emit a generic type for the component.