Write Solana programs in HTML.
HTMLSOL is a compiler. You describe accounts with <table>, instructions with <form> and their arguments with <input>. It writes the C, every safety check you'd forget, an Anchor-format IDL and a web3.js client.
The compiler is one dependency-free file, /lib/htmlsol.js. It runs in the browser and in Node 18+. The C it produces is built into a native SBF binary with Solana platform-tools, the same toolchain the official C examples use. There is no interpreter on chain and no Rust anywhere.
Quick start
The compiler is an ES module served from this site. Import it straight from the URL in a browser, or download it once for Node.
The import URL is this site's own copy of the compiler. There is no npm package to install and nothing to configure.
Try it here
This box runs the real module from /lib/htmlsol.js. Break a tag and press Compile.
Your first program
A counter that only its owner can increment. Two accounts, one argument, one rule.
Read it top to bottom. <table> says what a Counter account stores. Each <form> is an instruction. Inputs come first: accounts in the order the client passes them, then arguments in the order they are packed. Then statements run top to bottom.
The increment form above turns into 17 lines of C with seven checks: account count, signature, writable, owner, size, discriminator and data length, plus the require and checked math. Open it in the Playground to see every line.
How it works
- Parse. A forgiving tokenizer reads tags, attributes and text, with line numbers. Comments and
<!doctype>are dropped. Unlike a browser, unclosed containers are errors. - Check. Names, types, table fields and account flags are resolved. Writing to an account without
mut, comparing a key with a number or using an undeclared name stops the build with a message and a line. - Generate. Each form becomes a C function; an
entrypointreads the 8-byte instruction discriminator and dispatches. Account checks come first, then argument reads, then your statements. - Describe. The same tree produces the IDL and the client, so all three always agree.
- Build. You run clang from Solana platform-tools on the C. That step happens on your machine, not in the browser.
Discriminators follow Anchor: instructions use the first 8 bytes of sha256("global:<name>"), accounts use sha256("account:<Name>"). Anchor clients and explorers decode HTMLSOL programs without knowing what they were written in.
Programs and tables
Every file has exactly one <program id="name">. Everything else lives inside it. <html>, <body>, <center> and <div> around it are allowed and ignored, so a program can also be a valid web page.
A <table id="Name"> declares an account layout. Each <td name type> is a field. Closing </td> is optional, and <tr> rows are flattened if you like your tables proper.
| type | size | C type | notes |
|---|---|---|---|
u8 | 1 | uint8_t | |
u16 | 2 | uint16_t | |
u32 | 4 | uint32_t | |
u64 | 8 | uint64_t | Lamports, counters, amounts. |
i64 | 8 | int64_t | |
bool | 1 | uint8_t | Literals true and false. |
pubkey | 32 | SolPubkey | Compared with == / != only. |
The account's data is an 8-byte discriminator followed by the fields, packed, in declaration order. A Counter with a pubkey and a u64 takes 48 bytes.
Forms and inputs
<form action="name"> is one instruction. Inputs must come before the first statement. Account inputs are numbered in the order they appear; that's the order a client must pass them.
| input | attributes | checks the compiler writes |
|---|---|---|
type="signer" | mut | Signed the transaction; writable if mut. |
type="account" | of, mut, init, signer | With of: owned by this program, big enough, right discriminator. init instead checks the discriminator is zero and writes it. |
type="program" | The account is executable. Needed for transfers. | |
type="u8 … pubkey" | An argument. Length-checked, read little-endian, in order. |
init does not create the account. The client creates it with SystemProgram.createAccount, owned by your program and sized 8 + table size, in the same transaction. See Calling it from a client.
Statements
Statements run in order. Values can be number literals, true/false, an argument name, account.field, account.key or account.lamports.
| tag | does |
|---|---|
<set to value> | Assigns. Keys can be set from keys only. |
<add to value> | Checked addition. Overflow fails with 4100. |
<sub to value> | Checked subtraction. Underflow fails with 4101. |
<require if error> | Fails with the named error unless the condition holds. Operators: == != < <= > >=. |
<center>…</center> | Runs what's inside. Adds a comment to the C. That's all. |
Writing to .lamports moves SOL directly. The runtime only allows it to take lamports from accounts your program owns, and the total has to balance by the end of the instruction.
Transfers with <a>
A hyperlink that pays. <a href="system:transfer"> makes a cross-program call to the System Program and moves lamports from a signer to any writable account.
Requirements the compiler enforces: the form has a type="program" input, from is a signer, both ends are mut. Only system:transfer is supported in 0.1. Calls to other programs are planned as <iframe>.
<marquee> and <blink>
<marquee times="N"> repeats its contents N times, from 1 to 64. A marquee with no times doesn't compile: it would scroll until the runtime ran out of compute.
<blink>text</blink> writes a line to the transaction logs with sol_log. It costs about 100 compute units plus the length of the text, so it's the priciest tag in the language.
Errors
Declare errors with <error name="Name">message</error>. Codes start at 6000 in declaration order, or set one with code. A <require> naming an undeclared error gets the next free code and its name as the message.
Every failed require logs the code, the name, the message and the source line, like Error 6000 Unauthorized: Only the counter's authority can change it (counter.html:20).
Tags reference
| tag | where | compiles to | est. CU |
|---|---|---|---|
<program id> | top level | The program and its entrypoint. | — |
<table id> <td name type> | program | A packed struct and a discriminator. | — |
<error name code> | program | A custom error code. | — |
<form action> | program | An instruction function. | 60 |
<input type="signer"> | form | Signature check. | 5 |
<input type="account" of> | form | Owner, size and discriminator checks. | 26 |
… mut | input | Writable check. | +3 |
<input type="program"> | form | Executable check. | 5 |
<input type="u64"> | form | Checked argument read. | 6 |
<require> | form | Conditional error. | 4 |
<set> <add> <sub> | form | Assignment, checked math. | 5 |
<a href="system:transfer"> | form | System Program call. | ~1,250 |
<marquee times> | form | for loop. | 3 / turn |
<blink> | form | sol_log. | 100 + length |
<center> | anywhere | A comment. | 0 |
<!-- --> | anywhere | Nothing. | 0 |
Any other tag is ignored with a warning, so <p> and <div> are free documentation.
JavaScript API
| export | returns |
|---|---|
compile(src, { file, address }) | { ok, errors, warnings, program, c, lines, idl, client, instructions, estimate } |
parse(src) | { root, errors }: the tag tree with line numbers. |
discriminator(kind, name) | 8 numbers. kind is "global" or "account". |
sha256(string) | Uint8Array(32). |
EXAMPLES | counter, vault, guestbook, hype as strings. |
TYPES, VERSION, COMPILER_ERRORS | Type sizes, "0.1.0", built-in error codes. |
lines is the C as [{ text, src }], where src is the HTML line that produced it. The homepage uses it to draw the wires between the two panels. estimate is { bytes, cu: { [instruction]: number } } from the compiler's cost table.
IDL format
Anchor's IDL spec: address, metadata, instructions with discriminators, accounts and args, accounts, types and errors. Pass { address } to compile to fill in the program id.
Generated C
The output includes solana_sdk.h and nothing else. Each form becomes ix_<name>; the entrypoint deserializes accounts with sol_deserialize, reads the discriminator and dispatches. Structs are packed, math uses __builtin_add_overflow, keys are compared with SolPubkey_same.
Don't edit the C. Change the HTML and compile again; the header says so too.
Error codes
| code | when |
|---|---|
ERROR_NOT_ENOUGH_ACCOUNT_KEYS | Fewer accounts than the form's account inputs. |
ERROR_MISSING_REQUIRED_SIGNATURES | A signer input didn't sign. |
ERROR_INVALID_ARGUMENT | A mut account isn't writable, or accounts failed to deserialize. |
ERROR_INCORRECT_PROGRAM_ID | A table account isn't owned by this program, or a program input isn't executable. |
ERROR_ACCOUNT_DATA_TOO_SMALL | Account data is shorter than 8 + table size. |
ERROR_INVALID_ACCOUNT_DATA | Wrong discriminator: it's not that table. |
ERROR_ACCOUNT_ALREADY_INITIALIZED | init on an account that already has a discriminator. |
ERROR_INVALID_INSTRUCTION_DATA | Unknown instruction or arguments too short. |
4100 / 4101 | Overflow in <add>, underflow in <sub>. |
6000+ | Your <error> tags, in order. |
Build and deploy
You need the Solana CLI, which ships platform-tools with clang for SBF. Put the generated C in src/counter/counter.c and add a two-line Makefile that includes the SDK's C build rules.
On older Solana CLI versions the include path is …/bin/sdk/sbf/c/sbf.mk. Run ls ~/.local/share/solana/install/active_release/bin to see which one you have.
Deploying again to the same program id upgrades it. Put the id into compile(src, { address }) so the IDL and client carry it.
Calling it from a client
The Client tab in the Playground and out.client give you one instruction builder per form for @solana/web3.js. Creating and initializing a counter in one transaction looks like this:
Limits and threat model
What the compiler guarantees: every account a form names is checked for count, signature, writability, ownership, size and type before your statements run; arguments are length-checked; math is checked; errors carry their source line.
What it doesn't do in 0.1:
- No program-derived addresses yet. Accounts are created by the client and passed in.
- No closing or resizing accounts.
- Calls to other programs: System Program transfers only.
- Integers, booleans and keys only. No strings or arrays in tables.
- Compute and size numbers are estimates from a cost table, not measurements.
- The compiler hasn't been audited. Read the generated C before you put real money behind it, the same as any code.
$HTML
The token of the only compiler that accepts <marquee>. The compiler itself is free and stays free: it's a file at /lib/htmlsol.js.
Only trust the contract address published on the homepage; this page reads it from there.
FAQ
Is HTML a programming language?
It compiles to a native Solana binary with signature checks and overflow-safe math. You decide.
Does it really compile, or is this a joke?
Both. The compiler is real: open the Playground, change a tag, and the C changes. The .so is built with Solana's own clang.
Do I need Rust?
No. You need the Solana CLI to build and deploy, which brings its own clang. No cargo, no lifetimes.
Can I use CSS?
Not yet. <style> for the compute budget is on the list.
Why is a transfer an <a> tag?
A link takes you somewhere. This one takes your lamports somewhere. It felt right.
Do I need $HTML to use the compiler?
No. The compiler is a free file with no keys, accounts or limits.
Changelog
0.1.0 · 2026-10-28
- First release:
<program>,<table>,<form>, all input types,<require>,<set>,<add>,<sub>. <a href="system:transfer">System Program transfers.<marquee times>loops and<blink>logs.<center>accepted.- Anchor-format IDL and a web3.js client generator.
- Playground with sharable links.
Next
<iframe src="program:…">for calls to any program, from its IDL.- Program-derived addresses with
<input type="account" seeds>. - W3C validation and 1.0.