Learn Ruby in your browser — an interactive, notebook-style course in German, English, Japanese, French and Italian. No setup: Ruby itself runs client-side via ruby.wasm.
- Lessons from
puts "Hallo, Welt!"through strings,mapandselect, nil, truthiness and symbols, patterns drawn with loops, sorting and binary search, time and dates, methods, secret codes (Caesar and ROT13), reading error messages, recursion (up to the Towers of Hanoi), classic puzzles (primes, anagrams, Collatz, Goldbach), Struct and Data, classes, inheritance, modules, IRB and reading the Ruby documentation, to gems, HTML parsing with Nokogiri, exact arithmetic with BigDecimal, web routing with Sinatra and Roda, Ruby on Rails itself booted in the page (routes, controllers and views, Active Record on SQLite, a page from the database withresources, forms, all seven REST actions,has_manyandbelongs_to), 3D graphics, tables, frames and colours for the terminal with the TTY toolkit, sketches that move and follow the mouse with Processing, test data that looks real with Faker, ERB templates checked by Herb (its C parser as WebAssembly), PowerPoint decks with ruby_pptx, PDFs with Prawn and HexaPDF, JPEG photos with pure_jpeg, pandas, SymPy, NumPy, charts with matplotlib and machine learning with scikit-learn from Ruby through PyCall, machine learning in Ruby itself with Rumale (a postcode you write on a letter, read by a nearest-neighbours classifier), tables, statistics and networks with SciRuby's gems (data frames with Daru, the bell curve with distribution, integration and minimization, charts drawn by Rubyvis, the fastest train across Switzerland with networkx.rb), a SQLite database with Sequel, shipping a program as an .exe with OCRAN, a look at the Ruby community (RubyKaigi, weird code, how IRB reads code), music computed in plain Ruby (samples as an Array, a WAV file built withpack, chords and a drum beat - played below the cell), Snake built in five steps and played with the arrow keys below the cell, games with ruby2d whose code opens a real window on your computer too, the other Rubies (JRuby, TruffleRuby, mruby, PicoRuby, IronRuby on .NET, RubyMotion, DragonRuby and more), a lesson whose cells and IRB run on PicoRuby.wasm instead of CRuby, Android apps in Ruby with Ruboto on a simulated phone (JRuby in the browser, Ruboto's own Ruby, a photo frame), Ruby on a simulated Raspberry Pi Pico 2 W (LEDs, buttons, sensors, a robot, an e-paper display, Wi-Fi and Bluetooth, then a real board), Ruby compiled ahead of time by Matz's Spinel - Notebook UI: lessons interleave text with runnable CodeMirror
cells (Shift+Enter). All cells of a lesson share one binding, and every
cell shows its last expression as
=> …—putsis never required. Run a cell first and the cells above it that have not run yet run before it, quietly ("Ran cells 1–3 above first"). - Errors explained kindly: when a cell fails, a short explanation in
the lesson's language (German, English, Japanese) says what went wrong
and what to write instead, with the line and a caret under the culprit
(
6 x 7→ "Ruby multiplies with*"; a missingendnames thedefthat lost it;fox["name"]on a hash with symbol keys suggestsfox[:name]). Ruby's own message stays one click away. 44 rules, checked against 70 typical beginner mistakes drawn from the exercises. A misspelt name also gets Ruby's own suggestion ("💡 Did you mean name?"), in the learner's language. - Hints in steps, never stuck: every exercise has a 💡 button beside ▶. The first click gives a nudge (where to start, which method), the next the detailed hint, and then a finished solution to compare with - one click puts it into the cell. A failed check reveals the next hint too, Chunky says it; an error points at the explanation and at 💡. Every solution shown is one the test harness verifies.
- Bonus exercises: most Basics lessons end with a second, harder task in a "Bonus" box - checked and cheered like the first, with its own hints, but the lesson's tick comes from the main task alone.
- Ruby cheat sheet: one click in the header opens the course's Ruby on one page - 17 topics, each with the lessons that teach it and links into the official documentation, searchable. On a wide screen it stands beside the lesson, so you read it while typing; on a phone it slides over the page.
- The documentation, everywhere: every lesson ends with a "📖 Look it
up" box linking the official docs of what it used (docs.ruby-lang.org;
Rurima, the Japanese reference, for Japanese), and lesson
dokuteaches how to read them. - Lesson sidebar: the lessons in four groups (basics, side trips, Ruby on a Pico, the timelog track) with done counts, ticks and a bacon progress strip, searchable and foldable; it can be put away, and on a phone it is a drawer.
- Keyboard and screen readers: a skip link past the index, editors
that Escape, then Tab leaves, Run buttons named with their cell that keep
the focus, a status line that reads out what a run printed and what
the fox says, the focus on the new lesson's heading after every lesson
change, a modal drawer on a phone, named widgets (IRB, mini browser, 3D,
a game you can leave with Tab, the step-by-step slider)
and contrast at WCAG AA (audit:
experiments/10-accessibility). - Live runs: a cell runs by itself a second after you stop typing, as
long as the code parses - a rehearsal that keeps no file it writes,
installs only gems already in the cache, fetches nothing from the web and
is stopped after a second; ▶ runs the code for real. The foxes only speak
up when an exercise passes. On in the lessons, off in the workshop (the
⚡ Live switch beside ▶); a lesson whose cells compute too much for it
(
"live": false, the music lesson) turns it off and says why. - In-browser gem installer: pure-Ruby gems install at runtime
(
install_gem "chunky_png"), fetched from a local cache or from rubygems.org through a same-origin nginx proxy, and unpacked onto the wasm filesystem (writable memory) with their declared load paths on$LOAD_PATH- sorequire,require_relative,autoload,__dir__and data files next tolib/behave as on disk. - Nokogiri in the browser: the cache holds
nokogiri-pure - Nokogiri
1.19.4 with its C extension, libxml2, libxslt and gumbo ported to Ruby -
built under the name
nokogiri, so gems that depend on nokogiri (loofah, sanitize, rails-html-sanitizer, premailer, feedjira, rubyXL, roo, caxlsx, reverse_markdown …) install and run too. Other native gems fail with a friendly explanation of the wasm limitation that names the gem with the C code (often a dependency), unless it is built into the wasm image (json, date, openssl …), has a pure-Ruby stand-in (Numo, the arrays under Rumale:html/numo_narray.rb; sqlite3, on sql.js - SQLite in WebAssembly, loaded only when needed - so Sequel's own SQLite adapter runs, andSequel.sqlite("x.db")is a real SQLite file you can download:html/sqlite3_sqljs.rb; Processing, which draws through OpenGL:html/processing.rb; ruby2d, whose own Ruby runs here with its C extension on SDL3 written in Ruby:html/ruby2d.rb; in the cache,SUBSTITUTES: a dependency onbigdecimalinstalls bigdecimal-pure, which unblocks activesupport, liquid, prawn, dry-types …) or the gem that wants it can do without it (OPTIONAL_NATIVE_DEPS: ruby_pptx falls back to REXML). - Downloads: any file a cell writes -
deck.save("chunky.pptx"),File.write("notes.txt", …)- appears below the cell as a download link;download_file(data, "name")offers data that never went through a file,show_pdfputs a PDF in the browser's own viewer below the cell, andshow_audioa sound (a WAV, a file or an Array of samples) in the browser's player, with a picture of its wave. - Workshop: beside the lessons, a small IDE for your own multi-file
programs -
require_relativebetween files,File.read/File.writeon the project, input forgets, pictures, PDFs and sounds it writes previewed and kept, SQLite databases kept with the project, files renamable - with every widget below available. - Your progress stays yours: nothing is stored on a server. Progress, code and workshop files live in the browser and can be saved as a progress file (download, load again, merged key by key) or - in Chrome and Edge over https - into a connected folder, where workshop files are real files.
- Offline, if you like: one click keeps the whole course on your device (about 104 MB, or 52 MB with Python left out - a checkbox), and it opens and runs without a connection - lessons, cells, the bundled gems, Python. Online it always loads the current version, and the copy updates itself.
- Python next to Ruby: the PyCall lessons run real pandas, SymPy, NumPy,
matplotlib (its charts drawn as SVG below the cell) and scikit-learn -
Pyodide, CPython in WebAssembly, loaded only for
those lessons, each with just the packages it imports - through a small
bridge with the pycall gem's API (
html/pycall.rb), so their code runs unchanged with the real gem. - Data frames as tables: a cell that ends with a Daru
DataFrameorVectorshows it as a table, the way a pandas DataFrame shows (lesson 30) - the first 30 rows, numbers to six places. - Programs from a cell (optional, the Spinel lesson): a 📦 button builds
the cell's Ruby as a standalone Windows
.exeor Linux program with Spinel, compiled in the browser tab by clang and lld running as WebAssembly. The toolchain (45 MB) is built on a laptop and copied to the host:tools/compile/build.sh,publish.sh, see docs/HANDOVER.md §6d. - Interactive widgets:
show_irb(a real IRB terminal with_, multi-line input, and authentic prompts),show_browser(a fake browser window that speaks Rack directly to your Sinatra, Roda or Rails app - links, forms, cookies and redirects as in a browser, with the app's log under the page),show_image(inline pictures: PNGs from chunky_png, JPEGs from pure_jpeg, SVG, charts from Rubyvis;alt:says what they show),show_three(a WebGL stage for scenes built with three-rb, optionally animated per frame and orbitable with the mouse), andshow_letter(an envelope to write a postcode on with mouse or finger; a Ruby block reads it). - Objects as boxes and arrows:
show_objects(a: a, b: b)orshow_objects(binding)draws the names and the objects behind them, an arrow for every reference, like Python Tutor - sob = avsa.dup, a shallow copy of a hash, or two cats with the same name are seen, not just told (lessonsarrays,hashes,klassen). Pure Ruby to SVG; a screen reader hears the arrows in words. - Step by step through a cell: in the basics lessons with plain Ruby (3-12), ⏯ beside ▶ runs a cell and records it line by line - then a slider walks through the run: the line about to run marked in the editor, the variables of the cell, a method or a loop's pass (what just changed in yellow), the output so far, and Chunky saying what happens ("Line 2 is next - for the 3rd time"). The arrow keys walk it, a screen reader hears each step; an edit clears it.
- Turtle graphics with Chunky as the turtle:
turtle { 4.times { forward 100; right 90 } }draws below the cell, as an SVG that draws itself while Chunky runs along the path (lessonturtle: loops, methods with parameters, recursion up to a Koch snowflake). The exercise is graded on what was drawn - edges, corners, closed or not - not on pixels. - A terminal below each cell: colours from ANSI escape codes (pastel,
test runners) show as colours, and box-drawing characters (
┌─┐, from tty-table, tty-box) are as wide as the code font's letters, so tables line up. - Processing sketches:
setupanddrawfrom the processing gem run below the cell, about 60 frames a second, and hear the mouse and the keys - the gem's API in pure Ruby, painted on a canvas. - Games in a cell:
show_game(width: 20, height: 15) { |g| ... }draws a grid of cells below the cell;g.every(0.15) { ... }andg.on_key(:left) { ... }make it move and steer (Chunky's Snake, lesson 41). The page runs the loop and calls Ruby only when a timer is due or a key came in; the game runs only while it has the focus (a click, or Tab and Space; Esc pauses, Tab leaves), a screen reader hears the score and the end of a round, and an endless loop in a round is stopped after a second. Checks play the game headless. - ruby2d games, unchanged:
require "ruby2d", shapes,update do,on :key_held,show- lessonruby2druns real ruby2d 1.0 programs: the gem's own Ruby (vendored as it is,tools/vendor_ruby2d.rb) with the part it writes in C on SDL3 stood in for byhtml/ruby2d.rb, the window a canvas below the cell that the page drives like a game. The same file opens a real window withruby game.rbon a computer. Shapes, colours, text, keys, mouse; not images, sprites, sound or gamepads. - A compiler in the browser: in lesson
spinela cell's plain Ruby is a program for Spinel, Matz's ahead-of-time compiler, which runs in the page as WebAssembly together with clang (YoWASP's, itself WebAssembly), driven by Ruby on PicoRuby.wasm in workers: Ruby becomes C, the C becomes a WebAssembly module, and the module runs below the cell - with the time of each step, the C to read, the module to download (wasmtime run -W exceptions=y main.wasm), and CRuby's output of the same code beside it, as the oraclespin testtakes too. What an AOT compiler cannot do (evalof a String,method_missing) is refused with the line.show_spinel_irbis IRB on it: every line compiled with the ones before into a program of its own and run from the start, only the new line's output shown (lessonspinel, about 27 MB, loaded when it opens). Not in git: the deploy builds it (tools/build_spinel.rb, pinned intools/spinel.json). - A Pico on the page: the hardware lessons run on PicoRuby.wasm with
PicoRuby's own GPIO, PWM, ADC, I2C, SPI and
Net::HTTPcode over a simulated board, drawn below the cell as a Pico 2 W on a breadboard with the parts wired to it - press its button, move the light or the robot's box, read the e-paper, type to it over Bluetooth, or ■ reset it. Drivers sit in read-only tabs beside the code, as files in/libdo on a real Pico, and exercises are checked against what the board did. - _why's own cartoon foxes, from the poignant guide, cheer you on (CC BY-SA 2.5, traced to SVG and animated).
gem/chunky_bacon gives a Ruby program on your own
computer the course's helpers (show_image, show_objects, turtle, show_audio, show_browser,
download_file, ...), so code from the lessons and the workshop runs
unchanged. chunkybacon run starts a program with them loaded.
gem/chunkybacon and gem/chunky-bacon
are aliases: all three names install the same gem.
One runnable cell on another page - a blog post, a slide - with the course's Ruby, its output and its widgets:
<pre data-chunky data-gems="chunky_png" data-lang="en">
words = %w[chunky bacon is chunky]
words.tally
</pre>
<script src="https://letsruby.com/embed.js" defer></script>embed.js turns every pre[data-chunky] into an iframe of the course's
embed.html, as tall as the cell. Options on the <pre>: data-gems,
data-lang (de, en, ja; else the page's language), data-load
(visible, the default: Ruby loads when the cell scrolls into view;
click: on the first ▶; eager), data-run="1" and aria-label (the
iframe's title). The script's own data-base names the course's address;
without it, it is where embed.js came from.
The code goes into the address's fragment (#code=…, compressed), so it
never reaches the server. For an <iframe> by hand, or a "▶ Run" link in a
README (GitHub drops iframes; the link opens the cell as a page of its own):
ruby tools/make_embed_url.rb example.rb --gems chunky_png --lang en # --load click, --run, --base URLOnly pages on https://idogawa.com may put a cell in an iframe
(frame-ancestors, nginx/default.conf); a plain link works from
anywhere. The cell runs sandboxed, in an origin of its own: code from a
link sees none of the course's progress, files or offline copy, although
it comes from the course's host. Gems come from the course's cache only
(the rubygems bridge serves the course's own pages). How it works:
docs/HANDOVER.md §6k.
For operations, internals and traps see docs/HANDOVER.md.
The page runs on two Rubies: PicoRuby.wasm (0.9 MB) draws everything you read within a fraction of a second, while CRuby's ruby.wasm (10 MB) loads behind it and runs the code - see docs/PICORUBY_SHELL.md. The PicoRuby lesson (41) runs its cells on a second PicoRuby.wasm, in a Web Worker - the same runtime, nothing more to download (docs/HANDOVER.md §6m).
The runtimes and libraries the page loads from its own server
(ruby.wasm, PicoRuby.wasm, Pyodide, sql.js, Herb, the gem cache) are
kept in the repository, because a push is the deploy; each is written
by a tool under tools/ that downloads a pinned version, checks it
against the registry's checksum and can say whether the files on disk
are still those (--check) - §9a of the handover.
Built on the same foundation as
BrowserRubyKoans
(koans.idogawa.com): browser.script.iife.js + ruby-app.wasm with app
logic written in Ruby (html/main.rb) via the JS bridge. Lesson content
lives in html/lessons.js; the gem loader in html/browser_gems.rb
unpacks .gem files in Ruby onto the wasm filesystem, and hooks require
for shims of stdlib that cannot load in WASI (socket, net/http over the
browser's fetch, resolv).
three.js is vendored under html/assets/three/ and imported lazily
(window.ensureThree in index.html) only by lessons whose cells call
show_three - three-rb builds the scene graph in Ruby and looks the
library up as globalThis.THREE. All stages on the page share one
Three::Backends::ThreeJS: that backend caches the three.js objects it
built and afterwards only pushes what changed, so a second one would
rebuild an already-clean scene as bare defaults.
docker compose up -d # serves on port 8011Lesson spinel (Spinel) needs its build in html/assets/spinel/, which is not
in git: ruby tools/build_spinel.rb (Ruby 3.3+, no C compiler and no
Node: clang is WebAssembly, run through the wasmtime gem; a few minutes
the first time, afterwards a no-op until tools/spinel.json changes).
Or any static file server over html/ (the rubygems proxy then needs
nginx, see nginx/default.conf). Lessons then live at /index.html#methoden.
nginx, the dev server and server/ serve the landing page (welcome.html)
at / and the course at /learn; an old /#methoden link goes on to the
course.
Without Docker, ruby tools/dev_server.rb (port 8011) serves html/ with
nginx's bridges, headers and rules.
server/ is a small Roda app
that serves the same html/ and gives every lesson a permalink -
/de/methoden, /en/werkstatt - with its own title and link preview, plus
room for backend code under /api:
cd server && bundle install && bundle exec puma -b tcp://127.0.0.1:8012 config.ruThe page notices it is served with permalinks and uses them; without the
server everything works as before. With Docker:
docker compose --profile server up -d (see docs/HANDOVER.md §7a for nginx).
cd test
node make_lessons_json.js # test/lessons.json for the Ruby harnesses
ruby check_harness.rb # every lesson: starter fails, solutions pass
ruby lint_lessons.rb # lessons.js content: de/en/ja/fr/it parity, references, Japanese rules, Prism, gems, counts
ruby lint_lessons_test.rb # the linter's fault-injection tests
ruby gems_harness.rb # gem installer, sinatra + roda offline
ruby shell/run.rb # the page shell (PicoRuby code) under Minitest
ruby autorun_test.rb # live runs: what may run, the time limit
ruby earlier_cells_test.rb # which cells above a cell run first
ruby ansi_test.rb # terminal colours in a cell's output
ruby object_graph_test.rb # show_objects: the walk, the SVG, the alt text
ruby turtle_test.rb # turtle graphics: the recorded path, the check helpers, the SVG
ruby game_test.rb # show_game headless: the lesson's Snake by timer and keys, the copies checks play
ruby step_recorder_test.rb # ⏯'s recorder: steps, frames, hidden locals, caps, every cell of the stepper lessons
ruby friendly_errors_harness.rb --summary # 70 beginner mistakes, each explained by the expected rule
ruby friendly_errors_robustness.rb # the explanations never raise, never leave a %{...}
node browser_test.mjs # Playwright end-to-end against port 8011
node progress_test.mjs # progress file, workshop, connected folder
node boot_failure_test.mjs # what the page says when a runtime fails
node language_test.mjs # ?lang=, last choice, browser languages, English
node live_test.mjs # live runs in a lesson and in the workshop
node offline_test.mjs # offline mode: the copy, offline, a deploy, turning it off
node spinel_test.mjs # lesson `spinel` in a browser: Spinel compiling, IRB on it (after tools/build_spinel.rb)
ruby spinel_rb_test.rb # spinel(code) without the page: CRuby's run, the shell's ui strings
ruby spinel_build_test.rb # the compiler worker's plain Ruby: --print-build, clang's command line, IRB's verdict
ruby server_test.rb # the optional server (server/)
ruby dev_server_test.rb # the bridges serve only the course's pages, the embed's headers; nginx says the same
node embed_test.mjs # an embedded cell on another site (dev server with FRAME_ANCESTORS="http://blog.test:*")
node picoruby_test.mjs # the PicoRuby lesson: cells and IRB on PicoRuby.wasm in a worker
node rails_test.mjs # the Rails lessons: Rails boots from the cache, views, the log pane, Active Record on SQLite, a form sent (en, de, ja)
ruby ../tools/vendor_picoruby.rb --check # the PicoRuby runtime is the one PICORUBY_VERSION.txt records
ruby ../tools/build_embed_ui.rb --check # html/embed-ui.js has lessons.js's ui strings
BASE=http://127.0.0.1:8012/ node permalink_test.mjs # permalinks, against the server
cd ../gem/chunky_bacon && rake test # the companion gemThe legal notice and privacy policy name whoever runs a deployment, so they
are not in git: html/impressum.html and html/datenschutz.html are
ignored and live only on the server. Copy the templates and fill in every
yellow .todo:
cp html/impressum.example.html html/impressum.html
cp html/datenschutz.example.html html/datenschutz.htmlThe deploy hooks leave untracked files alone. Without the files the footer
links to them disappear (html/legal_links.js); the privacy template lists
what the course sends where, so check it again when a lesson starts talking
to a new host.
Both licenses allow commercial use.
- Code: MIT, © Andi Idogawa - the app, tools and tests.
- Course content: CC BY-SA 4.0,
© Andi Idogawa - the lesson and interface texts in
html/lessons.jsand the Chunky Bacon fox (html/assets/chunky.svg, and in ASCII in the gem,gem/chunky_bacon/lib/chunky_bacon/fox.txt). Credit the course and keep adapted versions under CC BY-SA. The code examples inside the lessons are also available under the MIT license, so you can use them in your own programs freely. - Third-party components (the ruby.wasm build, CodeMirror, three.js, the cached gems, the web fonts) keep their own licenses - see THIRD_PARTY_NOTICES.md.
The phrase "Chunky Bacon" is
an homage to why's (poignant) guide to Ruby by why the lucky stiff — fondly
remembered. The foxes on the page are his, from chapter 3 of the guide
(CC BY-SA 2.5), traced to SVG in html/assets/why-foxes/, and on the social
cards; the original Chunky drawing (html/assets/chunky.svg) stays with the
companion gem. The look borrows the design language of Citroën's 2 CV Concept
(redrawn, no Citroën marks): see docs/HANDOVER.md §10b.
