to_html
Version, currently 1.2.119 versions
- 1.7.1latestMar 4, 2026
- 1.7.0not indexedMar 4, 2026
- 1.6.0not indexedMar 4, 2026
- 1.5.2not indexedMar 4, 2026
- 1.5.1not indexedMar 4, 2026
- 1.5.0not indexedMar 4, 2026
- 1.4.0not indexedMar 4, 2026
- 1.3.0not indexedMar 4, 2026
- 1.2.1not indexedMar 4, 2026
- 1.2.0not indexedMar 4, 2026
- 1.1.0not indexedMar 4, 2026
- 1.0.7not indexedMar 4, 2026
- 1.0.6not indexedMar 4, 2026
- 1.0.5not indexedMar 4, 2026
- 1.0.4not indexedMar 4, 2026
- 1.0.3not indexedMar 4, 2026
- 1.0.2not indexedMar 4, 2026
- 1.0.1not indexedMar 4, 2026
- 1.0.0not indexedMar 4, 2026
github.com/sbsoftware/to_html.cr
The fastest HTML builder for Crystal you can get
20 stars
0 dependents
License: MIT
Nothing has been indexed for 1.2.1 yet. The tag is recorded, its shard.yml has not been read, so the manifest and dependency list below are empty because they are unknown rather than because they are absent.
Installation
# Add this to your shard.yml
dependencies:
to_html:
github: sbsoftware/to_html.cr
version: ~> 1.2.1Then run:
shards installshard.yml
No shard.yml has been indexed for 1.2.1. You can read it on the repository.
Dependencies
Unknown: the shard.yml for this version has not been read yet.
README
This README is the one indexed from the repository at its latest ref, not from the tag for this version.
# `to_html`
`to_html` is the fastest HTML builder library for Crystal you can get (see [Benchmark](#benchmark)).


The main idea behind this project is to be able to output HTML without leaving the Crystal syntax, as most templates need to contain logic and thus most other templating engines implement their own way of expressing such logic. Why not just use the tools we already have?
Another important core concept is that what can be known at compile-time should not be calculated at runtime. That's why all of this functionality is provided via macros that do their best to prepare everything while compiling to be ready to *just render* when your program runs. Just like [ECR](https://crystal-lang.org/api/latest/ECR.html) (and almost as fast as it), only with a better interface ;-)
## Contents
- [Usage](#usage)
- [Examples](#examples)
- [Benchmark](#benchmark)
- [Contributing](#contributing)
## Usage
*shard.yml*
```crystal
dependencies:
to_html:
github: sbsoftware/to_html.cr
```
*somewhere in your code*
```crystal
require "to_html"
```
*in any class/module*
```crystal
class MyView
# defines #to_html(io : IO) and #to_html
ToHtml.instance_template do
# use any HTML5 tag name as a method call with a block
html lang: "en" do
head do
title { "My View" }
end
body do
main do
p { "Tada!" }
# special case: `select` is a reserved keyword in crystal; need to use `select_tag` instead
select_tag do
option(value: "one") { "One" }
option(value: "two", selected: true) { "Two" }
end
end
end
end
end
end
```
### Element Concatenation
Just write your tag name calls and as many string literals as you want below each other - they will all be concatenated. You can also use control logic like `if`s and `#each`.
```crystal
require "to_html"
class MyLongView
getter show_author : Bool
getter random_lines : Array(String)
def initialize(@show_author, @random_lines); end
ToHtml.instance_template do
div do
p do
"In the realm of code, so vast and wide,"
br
"An HTML div began its thrilling ride."
br
"With attributes unique, it came alive,"
br
"To journey through the web, it would strive."
end
if show_author
div class: "author" do
i { "ChatGPT" }
end
end
p do
random_lines.each do |random_line|
strong { random_line }
br
end
end
end
end
end
puts MyLongView.new(true, ["Test", "Blah", "Foo"]).to_html
```
### Global Macros
If you're sure you won't get any name clashes (or you don't care), you can `require "to_html/globals"` to get rid of the module name before calling the template macros.
```crystal
require "to_html/globals"
class MyView
instance_template do
div do
strong { "More concise!" }
end
end
end
```
## Examples
### Static Instance Template
```crystal
require "to_html"
class StaticInstanceView
ToHtml.instance_template do
h1 { "Hello World!" }
p do
"This is a template."
br
"Treat it well."
end
end
end
puts StaticInstanceView.new.to_html
```
### Dynamic Instance Template
```crystal
require "to_html"
class DynamicInstanceView
getter heading : String
def initialize(@heading); end
ToHtml.instance_template do
h1 { heading }
p do
"This is "
i { "another" }
" template."
end
end
end
puts DynamicInstanceView.new("Template!").to_html
```
### Class template
```crystal
require "to_html"
class ClassView
ToHtml.class_template do
div do
p { "This needs no instance" }
end
end
end
puts ClassView.to_html
```
### Inline Templates
```crystal
require "to_html"
class InlineView
getter names : Array(String)
def initialize(@names); end
ToHtml.inline_template :greeting do |who|
p { "Hello #{who}" }
end
ToHtml.instance_template do
div do
names.each do |name|
greeting(name)
end
end
end
end
puts InlineView.new(["Ada", "Grace"]).to_html
```
### Class Inline Templates
```crystal
require "to_html"
class InlineClassView
ToHtml.class_inline_template :title_fragment do |text|
h2 { text }
end
ToHtml.class_template do
section do
title_fragment("Hello")
end
end
end
puts InlineClassView.to_html
```
### Compile-time Control Flow (MacroIf / MacroFor)
In addition to runtime `if`/`#each`, you can also use Crystal's macro control flow (`{% if %}` / `{% for %}`) inside templates. This runs at compile-time and can be used to generate markup based on compile-time information such as compiler flags or `@type`.
#### `MacroIf` (`{% if %}`)
```crystal
require "to_html"
class BuildVariantView
ToHtml.class_template do
head do
{% if flag?(:release) %}
script src: "/assets/app.min.js"
{% else %}
script src: "/assets/app.js"
{% end %}
end
end
end
puts BuildVariantView.to_html
```
#### `MacroFor` (`{% for %}`)
```crystal
require "to_html"
class AutoFormView
getter first_name : String
getter last_name : String
def initialize(@first_name, @last_name); end
ToHtml.instance_template do
form do
{% for ivar in @type.instance_vars %}
label for: {{ivar.name.stringify}} do
{{ivar.name.stringify}}
end
input type: :text, name: {{ivar.name.stringify}}, value: {{ivar.name.id}}
{% end %}
end
end
end
puts AutoFormView.new(first_name: "Ada", last_name: "Lovelace").to_html
```
### Attributes
#### Named Arguments
You can add attributes to your tags via named arguments to the calls.
```crystal
require "to_html"
class NamedArgumentsView
ToHtml.instance_template do
div class: "my-div" do
form action: "https://www.example.com", method: "POST" do
input type: "submit", name: "submit", value: "Submit!"
a href: "/index" do
"Cancel"
end
end
end
end
end
puts NamedArgumentsView.new.to_html
```
#### `data` / `aria` Helpers
You can define `data-*` and `aria-*` attributes via `data:` / `aria:` hashes or named tuples.
When multiple sources assign the same `data-*` / `aria-*` key, all values are joined with spaces.
```crystal
require "to_html"
class DataAriaView
ToHtml.instance_template do
div(
{"data-foo", "from-tuple"},
data_foo: "from-explicit",
data: {foo: "from-hash", user_id: 42, active: true, ignored: nil},
aria: {label: "Profile", hidden: false}
)
end
end
# <div data-foo="from-tuple from-explicit from-hash" data-user-id="42" data-active="true" aria-label="Profile" aria-hidden="false"></div>
puts DataAriaView.new.to_html
```
#### Object Interface
Another way to add attributes is via objects that implement `#to_html_attrs`. The easiest way to do so is via the `ToHtml.instance_tag_attrs`/`ToHtml.class_tag_attrs` macros.
This is still a bit experimental but the following example shows a few possible use cases, as well as the full potential of these macros.
```crystal
require "to_html"
class ObjectInterfaceView
ToHtml.instance_template do
div MyObject do
form MyResource do
input type: "submit", name: "submit", value: "Submit"
end
a MyResource do
MyResource.name
end
end
end
end
class MyObject
ToHtml.class_tag_attrs do
data_controller = "my-object"
end
end
class MyResource
def self.path
"/my_resource"
end
ToHtml.class_tag_attrs do
form do
action = path
end
a do
href = path
end
end
end
# <div data-controller="my-object">
# <form action="/my_resource">
# <input type="submit" name="submit" value="Submit">
# </form>
# <a href="/my_resource">MyResource</a>
# </div>
puts ObjectInterfaceView.new.to_html
```
### More
For more examples, the [specs](https://github.com/sbsoftware/to_html.cr/tree/main/spec) are quite expressive.
## Benchmark
Have a look into the `benchmark/` folder to find out how these numbers were determined. As this has only been done on one local machine, the absolute numbers are not meaningful. The ratios are the interesting part and the reason for this listing.
Execute `crystal run --release benchmark/benchmark.cr` to reproduce.
```
ecr 999.84k ( 1.00µs) (± 6.97%) 4.27kB/op fastest
to_html 576.96k ( 1.73µs) (± 7.51%) 5.52kB/op 1.73× slower
blueprint 377.61k ( 2.65µs) (± 8.35%) 4.9kB/op 2.65× slower
markout 120.59k ( 8.29µs) (± 8.54%) 8.08kB/op 8.29× slower
html_builder 49.94k ( 20.02µs) (± 7.38%) 10.4kB/op 20.02× slower
water 46.83k ( 21.35µs) (± 8.31%) 11.2kB/op 21.35× slower
```
Compared shards taken from [awesome-crystal](https://github.com/veelenga/awesome-crystal#html-builders)
## Contributing
1. Fork this repository
2. Read the [Contribution Guidelines](CONTRIBUTING.md)
3. Create a branch with your desired changes
4. Create a pull request
Documentation
Built from the current release. The first visit to a release nobody has asked for starts its build.
Links
This release
- Version
1.2.1- Tagged
- Mar 4, 2026
- Commit
7c22a0928ac3- Indexed
- not yet
Dependents
No indexed shard depends on this one yet.
Repository
github.com/sbsoftware/to_html.cr
Metadata
- Created
- Aug 12, 2026
- Updated
- Aug 12, 2026
- Synced
- Aug 12, 2026
- Versions
- 19