<?xml version="1.0" encoding="UTF-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom" xmlns:dc="http://purl.org/dc/elements/1.1/">
  <channel>
    <title>DEV Community: kazuyuki shimizu</title>
    <description>The latest articles on DEV Community by kazuyuki shimizu (@kazuy).</description>
    <link>https://dev.to/kazuy</link>
    <image>
      <url>https://media2.dev.to/dynamic/image/width=90,height=90,fit=cover,gravity=auto,format=auto/https:%2F%2Fdev-to-uploads.s3.amazonaws.com%2Fuploads%2Fuser%2Fprofile_image%2F3960583%2F6202f5a5-165b-4986-98c7-1d238b58f528.jpg</url>
      <title>DEV Community: kazuyuki shimizu</title>
      <link>https://dev.to/kazuy</link>
    </image>
    <atom:link rel="self" type="application/rss+xml" href="https://dev.to/feed/kazuy"/>
    <language>en</language>
    <item>
      <title>I don't want to write HTML or fight global CSS, so I built a TypeScript DSL</title>
      <dc:creator>kazuyuki shimizu</dc:creator>
      <pubDate>Sun, 31 May 2026 00:40:20 +0000</pubDate>
      <link>https://dev.to/kazuy/i-dont-want-to-write-html-or-fight-global-css-so-i-built-a-typescript-dsl-406d</link>
      <guid>https://dev.to/kazuy/i-dont-want-to-write-html-or-fight-global-css-so-i-built-a-typescript-dsl-406d</guid>
      <description>&lt;h2&gt;
  
  
  TL;DR
&lt;/h2&gt;

&lt;p&gt;I got tired of writing HTML and chasing global CSS rules. I had a hunch: &lt;strong&gt;what if you could write a page the same way you write an app&lt;/strong&gt; — same declarative tree, same modifier chains, scoped style per node? I spent a year quietly testing the bet on my own side projects.&lt;/p&gt;

&lt;p&gt;It... seems okay? I've open-sourced it as &lt;strong&gt;&lt;a href="https://github.com/object-zaseeta/DraftOle" rel="noopener noreferrer"&gt;DraftOle&lt;/a&gt;&lt;/strong&gt; (&lt;a href="https://www.npmjs.com/package/draft-ole" rel="noopener noreferrer"&gt;npm&lt;/a&gt; / &lt;a href="https://draftole-demo.netlify.app/" rel="noopener noreferrer"&gt;live demo&lt;/a&gt;).&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;code&gt;page()&lt;/code&gt; writes plain static HTML + scoped CSS — zero runtime JavaScript shipped.&lt;/li&gt;
&lt;li&gt;
&lt;code&gt;app()&lt;/code&gt; adds reactive &lt;code&gt;state()&lt;/code&gt; and event handlers — TypeScript arrow functions get serialized into a minimal runtime at build time.&lt;/li&gt;
&lt;li&gt;Same DSL, same modifiers, in both cases.&lt;/li&gt;
&lt;li&gt;No bundler, no JSX, no template language, zero production dependencies.
&lt;/li&gt;
&lt;/ul&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  pnpm add draft-ole
  &lt;span class="c"&gt;# or npm install draft-ole&lt;/span&gt;
  &lt;span class="c"&gt;# or yarn add draft-ole&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;This is the 0.9.0 pre-1.0 release. The API surface is essentially settled and 1.0 is the next tag, but I'm intentionally holding back the 1.0 promise until I hear from real users. If you try it and it feels great or terrible, please tell me — both signals are useful.&lt;/p&gt;

&lt;blockquote&gt;
&lt;p&gt;(Yes, AI can generate HTML/CSS now. I'm not making a claim about how DraftOle compares — that's a separate experiment I haven't run. This article is just about what I built and why.)&lt;/p&gt;
&lt;/blockquote&gt;

&lt;p&gt;## Honestly? I just don't want to write HTML or global CSS anymore&lt;/p&gt;

&lt;p&gt;Let me be candid about the motivation. It's not a refined "type safety extends to the leaves" pitch. It's two embarrassingly small frustrations I kept hitting on every side project.&lt;/p&gt;

&lt;h3&gt;
  
  
  1. I don't want to write HTML
&lt;/h3&gt;

&lt;p&gt;I'm building logic in TypeScript — typed values, typed functions, typed data flow — and then at the last mile I have to drop into stringly-typed HTML. Attribute names are strings. Class names are strings. Five levels of nesting and I can't tell which element carries which style anymore. The logical layer is type-safe, and then the presentation layer reverts to "paste these strings together carefully." That mismatch grates every time.&lt;/p&gt;

&lt;h3&gt;
  
  
  2. I don't understand global CSS
&lt;/h3&gt;

&lt;p&gt;CSS-in-JS, CSS Modules, Tailwind — pick your weapon, eventually you write a rule against &lt;code&gt;body&lt;/code&gt; or &lt;code&gt;*&lt;/code&gt;, and now your scoping assumptions are gone. For a one-page LP I'd spend more time chasing "where is this style coming from" in DevTools than writing the page itself. After enough projects you just sigh and accept it.&lt;/p&gt;

&lt;h3&gt;
  
  
  3. Apps are declarative — why aren't pages?
&lt;/h3&gt;

&lt;p&gt;When I build apps in React or SwiftUI, none of this bothers me. You drop a &lt;code&gt;Text&lt;/code&gt; inside a &lt;code&gt;VStack&lt;/code&gt;, you call &lt;code&gt;.padding()&lt;/code&gt;, the style attaches to that node. Structure and style coexist in a tree and stay local to it.&lt;/p&gt;

&lt;p&gt;&lt;strong&gt;What if I could write a page the same way? Would the HTML problem and the CSS problem both just dissolve?&lt;/strong&gt;&lt;/p&gt;

&lt;p&gt;That was the bet. I wrote a private View DSL for a year, dogfooded it on my own landing pages and small apps, and eventually thought "you know, this might actually be okay to release." So I cleaned it up and pushed &lt;code&gt;draft-ole@0.9.0&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;A pleasant side effect of basing the whole thing on a typed view tree: &lt;strong&gt;HTML attribute typos become compile errors&lt;/strong&gt;. The TypeScript checker reaches all the way to your &lt;code&gt;&amp;lt;div&amp;gt;&lt;/code&gt;. That wasn't the primary goal — it's just what falls out when you build a strongly-typed view model.&lt;/p&gt;

&lt;h2&gt;
  
  
  What DraftOle looks like in practice
&lt;/h2&gt;

&lt;h3&gt;
  
  
  A static page
&lt;/h3&gt;



&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;page&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;Heading&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-ole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;hero&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nc"&gt;Section&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;
    &lt;span class="nc"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
      &lt;span class="nc"&gt;Heading&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello, DraftOle!&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2.75rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f172a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
      &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;A TypeScript-native View DSL.&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;1.125rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
        &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#475569&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
    &lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;frame&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;maxWidth&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;720&lt;/span&gt; &lt;span class="p"&gt;}),&lt;/span&gt;
  &lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#f8f9ff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;page&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;hero&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="k"&gt;export&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// writes index.html + style.css&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;Run with &lt;code&gt;node --experimental-strip-types build.ts&lt;/code&gt; (or &lt;code&gt;tsx&lt;/code&gt;, or &lt;code&gt;ts-node&lt;/code&gt;). The output is plain HTML and scoped CSS. Zero runtime JavaScript ships to the browser.&lt;/p&gt;

&lt;p&gt;The modifier-chain shape is borrowed from SwiftUI's ViewModifier:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight swift"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// SwiftUI&lt;/span&gt;
  &lt;span class="kt"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nv"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt;
      &lt;span class="kt"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="s"&gt;"Hello"&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;title&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
          &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="n"&gt;primary&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
  &lt;span class="p"&gt;}&lt;/span&gt;
  &lt;span class="o"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;





&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="c1"&gt;// DraftOle&lt;/span&gt;
  &lt;span class="nc"&gt;VStack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;16&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt;
    &lt;span class="nc"&gt;Text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Hello&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;2rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;800&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;})&lt;/span&gt;
      &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#0f172a&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;),&lt;/span&gt;
  &lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;h3&gt;
  
  
  An interactive app
&lt;/h3&gt;

&lt;p&gt;When you need state and event handlers, &lt;code&gt;app()&lt;/code&gt; is the entry instead of &lt;code&gt;page()&lt;/code&gt;. Same primitives, same modifiers:&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight typescript"&gt;&lt;code&gt;  &lt;span class="k"&gt;import&lt;/span&gt; &lt;span class="p"&gt;{&lt;/span&gt; &lt;span class="nx"&gt;app&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;hstack&lt;/span&gt; &lt;span class="p"&gt;}&lt;/span&gt; &lt;span class="k"&gt;from&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;draft-ole&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;;&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;app&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;title&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;Counter&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;lang&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;en&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;
  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;state&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;0&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;span&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;text&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;map&lt;/span&gt;&lt;span class="p"&gt;((&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nc"&gt;String&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;n&lt;/span&gt;&lt;span class="p"&gt;)))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;font&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;size&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;4rem&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;weight&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;700&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="na"&gt;family&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;monospace&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;});&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;incBtn&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nx"&gt;el&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;button&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;type&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;button&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;+1&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;on&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;click&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;=&amp;gt;&lt;/span&gt; &lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;set&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="nx"&gt;count&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;get&lt;/span&gt;&lt;span class="p"&gt;()&lt;/span&gt; &lt;span class="o"&gt;+&lt;/span&gt; &lt;span class="mi"&gt;1&lt;/span&gt;&lt;span class="p"&gt;))&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;10px 24px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;background&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#6d28d9&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;foregroundStyle&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;#ffffff&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;)&lt;/span&gt;
    &lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;cornerRadius&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;8px&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="kd"&gt;const&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt; &lt;span class="o"&gt;=&lt;/span&gt; &lt;span class="nf"&gt;vstack&lt;/span&gt;&lt;span class="p"&gt;({&lt;/span&gt; &lt;span class="na"&gt;spacing&lt;/span&gt;&lt;span class="p"&gt;:&lt;/span&gt; &lt;span class="mi"&gt;20&lt;/span&gt; &lt;span class="p"&gt;},&lt;/span&gt; &lt;span class="nx"&gt;display&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;incBtn&lt;/span&gt;&lt;span class="p"&gt;).&lt;/span&gt;&lt;span class="nf"&gt;padding&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="mi"&gt;48&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;

  &lt;span class="nx"&gt;doc&lt;/span&gt;&lt;span class="p"&gt;.&lt;/span&gt;&lt;span class="nf"&gt;exportTo&lt;/span&gt;&lt;span class="p"&gt;(&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="s1"&gt;./dist&lt;/span&gt;&lt;span class="dl"&gt;'&lt;/span&gt;&lt;span class="p"&gt;,&lt;/span&gt; &lt;span class="nx"&gt;view&lt;/span&gt;&lt;span class="p"&gt;);&lt;/span&gt;   &lt;span class="c1"&gt;// writes index.html + style.css + script.js&lt;/span&gt;
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;&lt;code&gt;doc.state&amp;lt;number&amp;gt;(0)&lt;/code&gt; returns a reactive value. &lt;code&gt;.on('click', () =&amp;gt; count.set(count.get() + 1))&lt;/code&gt; accepts a &lt;strong&gt;TypeScript arrow function&lt;/strong&gt;. At build time, the function body is extracted from the AST, serialized into runtime JavaScript, and embedded into &lt;code&gt;script.js&lt;/code&gt;. The state subscription engine is a few hundred lines and ships with the output — that's the only runtime DraftOle adds when you use &lt;code&gt;app()&lt;/code&gt;.&lt;/p&gt;

&lt;p&gt;The arrow function can only capture variables that go through the state API (&lt;code&gt;.get / .set / .map / .update / .field / .each&lt;/code&gt;). Capturing anything else (a closed-over variable, an outer-scope helper) is a &lt;strong&gt;compile-time error&lt;/strong&gt; — the serializer needs a closed surface to work with.&lt;/p&gt;

&lt;p&gt;&lt;code&gt;examples/interactive/&lt;/code&gt; ships with working demos of Counter, Todo, Form, Shopping Cart, Card Gallery, and Priority Tasks. The Todo app is around 100 lines.&lt;/p&gt;

&lt;p&gt;## What DraftOle does not do&lt;/p&gt;

&lt;p&gt;I'll leave you to draw comparisons against whatever you're currently using. What I can describe is the scope I have not implemented:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;No router / multi-page convention.&lt;/strong&gt; &lt;code&gt;page()&lt;/code&gt; and &lt;code&gt;app()&lt;/code&gt; return single pages. Multi-page wiring is your code.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No SSR / hydration.&lt;/strong&gt; &lt;code&gt;app()&lt;/code&gt; mounts on load. There is no SSR + hydration two-step.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No pre-built component ecosystem.&lt;/strong&gt; You get view primitives and modifiers. Higher-level components are yours to build.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No file-based pages.&lt;/strong&gt; A page is a TypeScript file you run with &lt;code&gt;node&lt;/code&gt;.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;No state persistence (localStorage / URL sync).&lt;/strong&gt; Handle it in your application code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If any of these are required for your use case, DraftOle is not the right tool today, and that is fine.&lt;/p&gt;

&lt;p&gt;## How the internals work (a little)&lt;/p&gt;

&lt;p&gt;For the curious:&lt;/p&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;View → HTML mapping&lt;/strong&gt;: each primitive returns a &lt;code&gt;PairType&lt;/code&gt; / &lt;code&gt;SelfClosingType&lt;/code&gt; / &lt;code&gt;TextType&lt;/code&gt;. A tree walker emits HTML strings deterministically.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Scoped CSS&lt;/strong&gt;: every styled view gets a hash-suffixed class name. CSS is inlined into &lt;code&gt;&amp;lt;head&amp;gt;&amp;lt;style&amp;gt;...&amp;lt;/style&amp;gt;&amp;lt;/head&amp;gt;&lt;/code&gt;. No global selector wars.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;TypeScript transformer&lt;/strong&gt;: a custom TS transformer plugin extracts arrow-function bodies from &lt;code&gt;.on(event, fn)&lt;/code&gt; calls at the AST level and serializes them into runtime JavaScript. Anything that captures non-state-API closures is a compile-time error.&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Zero deps&lt;/strong&gt;: &lt;code&gt;package.json#dependencies&lt;/code&gt; is empty. Everything in &lt;code&gt;node_modules/draft-ole/&lt;/code&gt; is DraftOle's own code.&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If reading source code as a documentation form appeals to you, &lt;code&gt;src/transformer/&lt;/code&gt; has &lt;code&gt;handler-serializer.ts&lt;/code&gt;, &lt;code&gt;each-state-rewriter.ts&lt;/code&gt;, and &lt;code&gt;inline-recovery.ts&lt;/code&gt; — they're some of the more interesting parts.&lt;/p&gt;

&lt;p&gt;## Try it in 60 seconds&lt;br&gt;
&lt;/p&gt;

&lt;div class="highlight js-code-highlight"&gt;
&lt;pre class="highlight shell"&gt;&lt;code&gt;  &lt;span class="nb"&gt;mkdir &lt;/span&gt;try-draftole &lt;span class="o"&gt;&amp;amp;&amp;amp;&lt;/span&gt; &lt;span class="nb"&gt;cd &lt;/span&gt;try-draftole
  pnpm init &lt;span class="nt"&gt;-y&lt;/span&gt;
  pnpm add draft-ole

  &lt;span class="nb"&gt;cat&lt;/span&gt; &lt;span class="o"&gt;&amp;gt;&lt;/span&gt; hello.ts &lt;span class="o"&gt;&amp;lt;&amp;lt;&lt;/span&gt;&lt;span class="sh"&gt;'&lt;/span&gt;&lt;span class="no"&gt;EOF&lt;/span&gt;&lt;span class="sh"&gt;'
  import { page, Section, Text } from 'draft-ole';
  const doc = page(
    Section(Text('Hello from DraftOle.').padding(48)).background('#f0f4ff'),
    { lang: 'en', title: 'Hi' },
  );
  doc.export('./out');
&lt;/span&gt;&lt;span class="no"&gt;  EOF

&lt;/span&gt;  node &lt;span class="nt"&gt;--experimental-strip-types&lt;/span&gt; hello.ts
  open out/index.html
&lt;/code&gt;&lt;/pre&gt;

&lt;/div&gt;



&lt;p&gt;That's it. One TypeScript file, one node command, plain static HTML on disk.&lt;/p&gt;

&lt;h2&gt;
  
  
  What's next
&lt;/h2&gt;

&lt;p&gt;0.9.0 is a feedback window. The API surface is essentially settled and 1.0 is the next tag, but I'm intentionally holding back the 1.0 promise until I hear from real users. Things I most want feedback on:&lt;/p&gt;

&lt;ol&gt;
&lt;li&gt;&lt;strong&gt;Does the SwiftUI-style modifier API feel natural in TypeScript, or weird?&lt;/strong&gt;&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Are there view primitives you reached for and couldn't find?&lt;/strong&gt; (Common candidates: &lt;code&gt;Card&lt;/code&gt;, &lt;code&gt;Grid&lt;/code&gt;, &lt;code&gt;Image&lt;/code&gt; variants, form controls.)&lt;/li&gt;
&lt;li&gt;&lt;strong&gt;Does anything in the README sound like marketing rather than honest description?&lt;/strong&gt;&lt;/li&gt;
&lt;/ol&gt;

&lt;p&gt;&lt;a href="https://github.com/object-zaseeta/DraftOle/issues" rel="noopener noreferrer"&gt;GitHub Issues&lt;/a&gt; and &lt;a href="https://github.com/object-zaseeta/DraftOle/discussions" rel="noopener noreferrer"&gt;Discussions&lt;/a&gt; are both open. "I tried it and it doesn't fit my use case" is a valuable signal too — I want to know where the seams are.&lt;/p&gt;

&lt;h2&gt;
  
  
  Links
&lt;/h2&gt;

&lt;ul&gt;
&lt;li&gt;
&lt;strong&gt;GitHub&lt;/strong&gt;: &lt;a href="https://github.com/object-zaseeta/DraftOle" rel="noopener noreferrer"&gt;https://github.com/object-zaseeta/DraftOle&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;npm&lt;/strong&gt;: &lt;a href="https://www.npmjs.com/package/draft-ole" rel="noopener noreferrer"&gt;https://www.npmjs.com/package/draft-ole&lt;/a&gt;
&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Live demo&lt;/strong&gt;: &lt;a href="https://draftole-demo.netlify.app/" rel="noopener noreferrer"&gt;https://draftole-demo.netlify.app/&lt;/a&gt; (this LP is built with DraftOle itself — &lt;a href="https://github.com/object-zaseeta/DraftOle/blob/main/examples/page-landing.ts" rel="noopener noreferrer"&gt;source&lt;/a&gt;)&lt;/li&gt;
&lt;li&gt;
&lt;strong&gt;Positioning doc&lt;/strong&gt;: &lt;a href="https://github.com/object-zaseeta/DraftOle/blob/main/docs/positioning.md" rel="noopener noreferrer"&gt;docs/positioning.md&lt;/a&gt;
&lt;/li&gt;
&lt;/ul&gt;

&lt;p&gt;If you give it a try, even a one-liner reaction in the comments or on social means a lot at this stage.&lt;/p&gt;

</description>
      <category>html</category>
      <category>css</category>
      <category>typescript</category>
    </item>
  </channel>
</rss>
