hot-crumble
hot-crumble
hot-crumble is a batteries-included Crystal toolkit for building interactive web apps with typed pages, typed models, and Turbo-powered actions. It is aimed at developers who want to move quickly from schema to UI and ship dynamic, real-time-feeling applications from a compact codebase.
Installation
-
Add the dependency to your
shard.yml:dependencies: hot-crumble: github: sbsoftware/hot-crumble -
Run
shards install
Usage
require "hot-crumble"
hot-crumble bundles the pieces you need to build dynamic apps in one place:
Crumble::Pagefor routeable pages and UI compositionOrma::Recordfor typed models and persistence- Turbo actions and model actions for interactive updates
- typed HTML, CSS, and JavaScript helpers for keeping UI code in Crystal
Routes are derived from class names by default, model-aware pages can load records directly from the URL, and model actions can update part of the page while broadcasting Turbo stream refreshes to connected clients.
Example App
The following example is designed to be pasted into a fresh folder and run as a tiny app. It shows:
- a
Projectmodel - a
Taskmodel - a
ProjectPagethat loads a model from the route - a
create_child_actionfor adding tasks - a
boolean_flip_actionfor toggling a task between open and done - scoped inline styles
shard.yml
name: demo_board
version: 0.1.0
targets:
demo_board:
main: src/demo_board.cr
dependencies:
sqlite3:
github: crystal-lang/crystal-sqlite3
hot-crumble:
github: sbsoftware/hot-crumble
File layout
.
├── shard.yml
└── src
├── demo_board.cr
├── models
│ ├── application_record.cr
│ ├── project.cr
│ └── task.cr
├── pages
│ ├── application_page.cr
│ ├── home_page.cr
│ └── project_page.cr
└── views
└── application_layout.cr
src/demo_board.cr
require "sqlite3"
require "hot-crumble"
require "./models/**"
require "./views/**"
require "./pages/**"
if Project.all.empty?
project = Project.create(name: "Launch checklist")
Task.create(project_id: project.id.value, title: "Write the landing page")
Task.create(project_id: project.id.value, title: "Add live task updates")
end
Crumble::Server.start
src/models/application_record.cr
abstract class ApplicationRecord < Orma::Record
macro inherited
id_column id : Int64
end
end
src/models/task.cr
class Task < ApplicationRecord
column project_id : Int64
column title : String
column done : Bool = false
css_class TaskRow
css_class TaskStatus
css_class TaskTitle
model_template :row_view do
li TaskRow do
strong TaskStatus do
done.value ? "DONE" : "OPEN"
end
span TaskTitle do
title.value
end
switch_done_action_template(ctx).to_html
end
end
boolean_flip_action :switch_done, :done, :row_view do
view do
template do
custom_action_trigger.to_html do
button { model.done.value ? "Mark open" : "Mark done" }
end
end
end
end
style do
rule TaskRow do
display :flex
align_items :center
gap 12.px
padding_top 10.px
padding_bottom 10.px
border_bottom 1.px, :solid, "#e2e8f0"
end
rule TaskStatus do
font_size 12.px
font_weight 700
color "#475569"
letter_spacing 0.08.em
end
rule TaskTitle do
flex_grow 1
end
end
end
src/models/project.cr
class Project < ApplicationRecord
column name : String
css_class Card
css_class TaskList
css_class Errors
def tasks
Task.where(project_id: id.value).order_by_id!
end
create_child_action :add_task, Task, project_id, tasks_view do
form do
field title : String, label: "Task title", allow_blank: false
end
view do
template do
action_form(hidden: false).to_html do
if errors = action.form.errors
div Errors do
"Please enter a title."
end
end
button { "Add task" }
end
end
end
end
model_template :tasks_view do
section Card do
h2 { "Tasks" }
ul TaskList do
tasks.each do |task|
task.row_view.renderer(ctx)
end
end
add_task_action_template(ctx).to_html
end
end
style do
rule Card do
background_color :white
border_radius 20.px
padding 24.px
box_shadow 0.px, 18.px, 45.px, rgb(15, 23, 42, alpha: 8.percent)
end
rule TaskList do
list_style :none
padding_left 0.px
end
rule Errors do
margin_bottom 12.px
color "#b91c1c"
font_weight 600
end
rule button do
background_color "#2563eb"
color :white
border :none
border_radius 999.px
padding_top 10.px
padding_bottom 10.px
padding_left 14.px
padding_right 14.px
cursor :pointer
end
rule input do
width 100.percent
max_width 320.px
padding_top 10.px
padding_bottom 10.px
padding_left 12.px
padding_right 12.px
margin_bottom 12.px
border 1.px, :solid, "#cbd5e1"
border_radius 12.px
box_sizing :border_box
end
end
end
src/views/application_layout.cr
class ApplicationLayout < ToHtml::Layout
end
src/pages/application_page.cr
abstract class ApplicationPage < Crumble::Page
layout ApplicationLayout
end
src/pages/home_page.cr
class HomePage < ApplicationPage
root_path "/"
css_class ProjectList
template do
main do
h1 { "Demo board" }
p { "Open the seeded project to try pages, models, and actions together." }
ul ProjectList do
Project.all.order_by_id!.each do |project|
li do
a href: ProjectPage.uri_path(project_id: project.id.value) do
project.name.value
end
end
end
end
end
end
style do
rule body do
margin 0.px
background_color "#f3f6fb"
color "#1f2937"
font_family "IBM Plex Sans", "Segoe UI", :sans_serif
line_height 1.5
end
rule main do
max_width 720.px
margin_top 48.px
margin_bottom 48.px
margin_left :auto
margin_right :auto
padding_left 24.px
padding_right 24.px
end
rule h1 do
margin_bottom 8.px
font_size 32.px
end
rule a do
color "#2563eb"
end
rule ProjectList do
background_color :white
border_radius 20.px
padding 24.px
box_shadow 0.px, 18.px, 45.px, rgb(15, 23, 42, alpha: 8.percent)
end
end
end
src/pages/project_page.cr
class ProjectPage < ApplicationPage
model project : Project
template do
main do
h1 { project.name }
p do
a href: HomePage.uri_path do
"Back to projects"
end
end
project.tasks_view.renderer(ctx).to_html
end
end
end
Run it
shards install
DATABASE_URL='sqlite3://./demo_board.db' ORMA_CONTINUOUS_MIGRATION=1 crystal run src/demo_board.cr
Then open http://localhost:8080.
DATABASE_URL points Orma at the SQLite database file. PostgreSQL works as well when the corresponding DB shard is required. ORMA_CONTINUOUS_MIGRATION=1 tells Orma to create or update the example tables while the app boots.
How the example fits together
HomePageresolves to/and links toProjectPageProjectPageloads aProjectfrom the route viamodel project : ProjectProject#tasks_viewrenders the current task list plus the create formProject#add_taskis a model-aware action that inserts aTaskand refreshestasks_viewTask#switch_doneflips one boolean attribute and refreshes only that task row
Included Extras
Beyond pages, models, and actions, hot-crumble also bundles:
crumble-crababelfor localized labels and page contentcrumble-jobsfor background jobs and retriescrumble-stimulusfor typed Stimulus controllers in Crystalcss.cr,js.cr, andto_html.crfor typed UI building blocks
Development
- Install dependencies:
shards install - Run tests:
crystal spec
Contributors
- Stefan Bilharz - creator and maintainer
hot-crumble
- 0
- 0
- 0
- 0
- 9
- 9 days ago
- February 15, 2026
MIT License
Sun, 12 Apr 2026 13:01:33 GMT