Your First Page

Build your first Thorm page and learn the basic workflow: structure, state, and rendering mode.

The Request

We have received a request from the CTO of ACME HQ, Goof Duckerson, to design and build a dedicated contact page for their organization. Mr. Duckerson supplied detailed specifications, including preferred messaging, layout expectations, and a clear visual direction that reflects ACME HQ’s brand identity. Along with the creative brief, he provided an explicit color scheme and asked that we stay consistent with their established look and feel. He also requested that TailwindCSS be used as the primary styling approach so the implementation remains modern, modular, and easy to maintain. Our goal is to translate these requirements into a clean, accessible contact experience that matches their branding while keeping the codebase efficient and scalable for future updates.

P.S. In case you're wondering, ACME HQ and Mr. Goof Duckerson are 100% imaginary—no actual ducks were promoted to CTO in the making of this tutorial.

Acme contact page request

Prerequisites

  • PHP: >= 8.1
  • A local web server (built-in PHP server is fine)
  • Composer (optional)

Instalation

If you have composer installed:

Console
composer create-project thorm/app contact
cd contact

For this tutorial we are going to assume the following folder structure:

Console
/hello-thorm
    /src
        /assets
        /php
        /runtime
    /vendor
    index.html (this file will be overwritten at build time)
    index.ir.json (will be created at build time)
    composer.json

At this point, it might be a good idea to register the namespaces.

Console
composer dump-autoload

Template and CSS

Building full pages from scratch with Thorm will require a html template. Templates are familiar to PHP developers, almost all frameworks use them in one way or another. While Thorm can easily write the entire DOM, there isn't much interactivity outside of <div id="app"></div>

Here's how the JavaScript flow in template:

  • It loads the runtime as an ES module: import { mount } from '{$runtime}'; where {$runtime} resolves to something like .../thorm/runtime.index.js.
  • It defines an async IIFE (Immediately-invoked function expression) assigned to RT. That function:
    • fetches the IR JSON from {$iruri_dir}{$iruri} with cache: 'no-store';
    • parses the JSON;
    • calls mount(ir, document.getElementById('app')) to render into #app;
    • then tries to surface atoms, subs, and a notify function from the runtime's store (binding notify to preserve this).
  • RT is a Promise of { atoms, subs, notify }, so consumers can await RT to get those handles.

Everything else is template wiring:

  • {$headScripts} is where your fonts/Tailwind/custom CSS get injected.
  • {$title} sets the page title.
  • {$iruri_dir}{$iruri} becomes the URL to the generated IR JSON.
HTML
<!doctype html> <html> <head> <meta charset="utf-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <link rel="icon" type="image/png" href="/images/thorm-builder_32x32.webp"> <title>{$title}</title> {$headScripts} </head> <body> <div id="app"></div> <script type="module"> import { mount } from '{$runtime}'; const RT = (async function () { const res = await fetch('{$iruri_dir}{$iruri}', { cache: 'no-store' }); const ir = await res.json(); const x = mount(ir, document.getElementById('app')); const atoms = x?.core?.store?.atoms; const subs = x?.core?.store?.subs; const notify = typeof x?.core?.store?.notify === 'function' ? x.core.store.notify.bind(x.core.store) : (typeof x?.core?.notify === 'function' ? x.core.notify.bind(x.core) : null) return { atoms: atoms ?? null, subs: subs ?? null, // if notify exists, bind it to its host to preserve `this` notify: notify }; })(); </script> </body> </html>

Alongside TailwindCSS, we'll add a small custom CSS layer to capture the client’s exact color palette, typography tweaks, and any brand‑specific styling that Tailwind’s defaults don't cover. This keeps the utility workflow while still matching ACME HQ's visual identity and polish.

Copy and paste this inside /public/style.css

CSS
:root{ --acme-orange: #f27a1a; --acme-red: #d83b2c; --acme-blue: #2f7dbf; --acme-yellow: #f3c84b; --acme-cream: #f7efe4; --acme-ink: #2a1d16; } body{ font-family: "Nunito", sans-serif; color: var(--acme-ink); background: radial-gradient(circle at 50% 18%, rgba(242,122,26,.6) 0 18%, transparent 19%), radial-gradient(circle at 50% 18%, rgba(216,59,44,.45) 0 34%, transparent 35%), radial-gradient(circle at 50% 18%, rgba(47,125,191,.35) 0 52%, transparent 53%), radial-gradient(circle at 50% 18%, rgba(243,200,75,.35) 0 70%, transparent 71%), radial-gradient(circle at 15% 30%, rgba(47,125,191,.35) 0 20%, transparent 21%), radial-gradient(circle at 85% 28%, rgba(242,122,26,.35) 0 20%, transparent 21%), radial-gradient(circle at 12% 70%, rgba(243,200,75,.35) 0 22%, transparent 23%), radial-gradient(circle at 88% 72%, rgba(216,59,44,.3) 0 22%, transparent 23%), repeating-conic-gradient(from 10deg, rgba(255,255,255,.18) 0 8deg, rgba(255,255,255,0) 8deg 16deg), #f4e4cf; min-height: 100vh; } .title-font{ font-family: "Bangers", cursive; letter-spacing: 1px; } .card{ background: rgba(255,255,255,.92); border: 3px solid var(--acme-ink); border-radius: 24px; box-shadow: 0 14px 0 rgba(42,29,22,.2); } .burst{ background: radial-gradient(circle at 50% 50%, #fef7e4 0 44%, transparent 45%), repeating-conic-gradient( from 0deg, var(--acme-yellow) 0 12deg, var(--acme-orange) 12deg 24deg, var(--acme-blue) 24deg 36deg, var(--acme-red) 36deg 48deg ); border: 3px solid var(--acme-ink); border-radius: 999px; box-shadow: 0 10px 0 rgba(42,29,22,.2); } .text-acme-muted{ color: #5e4a3b; } .bg-acme-orange{ background: var(--acme-orange); } .bg-acme-red{ background: var(--acme-red); } .bg-acme-blue{ background: var(--acme-blue); } .bg-acme-yellow{ background: var(--acme-yellow); } .bg-acme-cream{ background: var(--acme-cream); } .border-acme-ink{ border-color: var(--acme-ink); } .input-acme{ border: 2px solid var(--acme-ink); background: #fffdf8; } .input-acme:focus{ outline: none; border-color: var(--acme-orange); box-shadow: 0 0 0 3px rgba(242,122,26,.25); } .shadow-acme{ box-shadow: 0 8px 0 rgba(42,29,22,.2); }
Status: Developer Preview
Things may change, things might break. If something feels awkward, it's probably a design edge we're still smoothing out.