pro/templating and a design system/beta, source not yet published

minline Front-end templating, and the stylesheet beside it Say it once in a definition, point at it from the markup

minline is two front-end tools of a few kilobytes each: an engine that puts values into a page, and a stylesheet that dresses the page without a single class for how things look. Two halves that share an attitude. The engine is 1,337 bytes and works on the document the browser already built, with no shadow copy of it to keep honest. The stylesheet is under three kilobytes and has no utility classes, because a class named after what it looks like is a style sheet written in the markup. Neither half compiles, and neither brings a dependency.

Runs in

  • Any browser
  • No build step

Built with

  • JavaScript
  • CSS
  • Apache-2.0
The minline mark: two blocky lines, a long one and a short one, with a cross at the centre, extruded
Kind
A templating engine and a design system, as a pair
minline.js
1,337 bytes Brotli, and the number is not an accident
minline.css
2,841 bytes Brotli
Dependencies
None, in either half
Build step
None. The file is the file
Surface
minline.def() to declare, data-ml to apply
Rendering
The real document, never a copy of it
Escaping
On by default; raw HTML is asked for by name
Translations
Loaded from CSV, switched at runtime
Stylesheet uses
oklch, light-dark(), cascade layers
Filters
upper, lower, trim, escape, json, default
Stylesheet
Semantic, with no utility classes
Tests
49 passing
Licence
Apache-2.0

01declared once, referenced by name

The markup says which, the definition says what

A behaviour is written once, in JavaScript, and given a name. The markup then names it in an attribute and says nothing else about it. That division is the whole design: the document stays a document, and the logic stays in the language that is good at logic.

    minline.def("navItem", "each:item in nav.items attr:href=item.url item.label");

    <a data-ml="navItem"></a>
        

The definition language is small enough to hold in one breath. each: iterates, attr: sets an attribute, class: toggles one, html: puts sanitised markup in, and a bare name puts a value in. That is most of it.

Which raises the question every templating engine has to answer: what does it do to the page when the data changes?

02the real document, not a copy

There is no second tree to keep honest

The customary approach keeps a copy of the document in memory, builds a new copy when something changes, compares the two and applies the difference. It works, and it costs a library large enough to need downloading, plus a class of bug in which the copy and the real thing disagree.

minline works on the document the browser already built. There is no shadow tree, so there is nothing to reconcile, nothing to hydrate and no moment at which the two versions could differ. The engine is 1,337 bytes largely because most of what a virtual document machine does is not being done.

Small is pleasant. Does it stay pleasant when the page gets complicated?

03definitions compose

Three small ones beat one that does everything

A definition can carry the whole behaviour of an element, and often should not. Several can be named on the same element and each does its own part, which means the pieces are reusable rather than the elements being special.

    minline
      .def("loop",  "each:item in items")
      .def("link",  "attr:href=item.url")
      .def("label", "item.label");

    <a data-ml="loop link label"></a>
        

Values pass through filters on the way out: upper, lower, trim, escape, json, and default: for the case where the value is not there. Small pieces, named, applied in a row. It is the same instinct as a shell pipeline, and it ages about as well.

There are fifteen of them, which is more than the handful that get used daily: upper, lower, trim and default for the ordinary work; first, last, length, reverse, slice, join and values for collections; and json, escape and sanitise for what goes into the page. Fifteen filters inside 1,337 bytes is the part that ought to raise an eyebrow.

Which leaves the question everyone should ask of anything that puts data into a page.

04escaping is the default

The dangerous thing has to be asked for by name

Values are escaped on the way into the document. Putting raw markup in is possible and requires the html: form, which sanitises, and which a reader of the definition can see at a glance because it is spelled differently from everything else.

The sanitiser is specific about what it will not carry, and naming the list is more use than the word sanitised on its own: <script> stripped, <iframe> and <object> removed, on* event attributes blocked, and javascript: URLs taken out. Those four are where the trouble actually comes from.

Prototype pollution is refused as well, which is the less obvious half. A key called __proto__ or constructor arriving in data is how an attacker edits objects he was never handed, and the engine declines both rather than trusting that nobody will try.

That is the right way round and it is not always how it is done. An engine whose default is raw and whose escape is an option produces exactly one kind of incident, reliably, for years. Making the safe thing the thing you get for free costs nothing at all and removes the entire category.

Translation belongs here too, because it is the other thing that goes wrong when text is put into a page. Strings load from CSV, which a translator can open without being taught anything, and the language changes at runtime rather than at build time. The chain that picks one is short and stated: whatever minline.lang() was told, then the document's own lang, then British English.

So much for the half in JavaScript. There is another.

05a stylesheet without utility classes

A class named after its appearance is CSS in the markup

The other half is a stylesheet of under three kilobytes, and its position is that a class called mt-4 or text-sm has not organised anything. It has moved the style sheet into the document one word at a time, where it cannot be changed centrally and cannot be read as a whole.

So minline.css styles elements for what they are. The markup stays a description of the content, the appearance stays in one file, and changing how a heading looks is one edit rather than a search across the templates.

What it is built from is worth naming, because it is the modern platform rather than a polyfilled approximation of it: colours in oklch so a palette can be reasoned about, light-dark() for theming instead of a second stylesheet, native nesting rather than a preprocessor, and cascade layers to settle precedence once rather than by accident. That list is, as it happens, the same list this page is built on.

    @layer globals, minline, theme;   your overrides always win
        

That one line is the whole precedence policy. The system's own rules sit in the middle layer, so anything you write in theme beats them without a single !important and without counting selectors. Under three kilobytes, which is what a design system weighs when it trusts the browser to do the parts it is already good at.

That is the argument. The next screen is it running, so none of it has to be taken on trust.

Livethe stylesheet, running

Every element it styles, once, on this page

Below is minline itself, both files unmodified, styling a document of its own. Not one element in it carries a class for how it looks: they are buttons, fields, a meter and a set of details, and the stylesheet knows what those are.

Three things in it are worth catching. It is dark because the demo sets color-scheme: dark and every colour in the stylesheet comes from light-dark(), so a theme is one declaration rather than a second file. The captions, the placeholders and the field values are all rendered by the engine from an inline set of two languages, which is what the switch at the top is doing. And the layout around the specimens sits in the theme layer, so it beats the system without one !important: the line above being used rather than described. Open it on its own if you would rather have it whole.

Both halves, then. What does the pair actually cost a reader?

06what the pair weighs

One thousand three hundred and thirty-seven bytes

measured1,337 Bminline.js, Brotli
measured2,841 Bminline.css, Brotli
measured0Dependencies
measured49Tests passing

The engine entire, not a core that fetches the rest. The number is chosen and the project says so, which is the sort of small joke a person leaves in and a committee takes out, and it is also literally true: the minified engine compresses to 1,337 bytes on the nose. Both figures are Brotli at quality 11: the engine minified, the stylesheet as written, which is the file the demo above loads. Over gzip, which is what this site's own server happens to send today, the two are 1,504 and 3,357.

Both halves arrive as files. There is nothing to install, nothing to configure and no step between what was written and what runs, which also means there is no version of this page where the build is broken and nobody can ship. Starting is three lines: include the script, write the markup, define the behaviour.

Two files, then, and two ideas. Why one project?

07two halves, one conviction

Both of them are refusals of the same habit

The engine refuses to rebuild the document, and the stylesheet refuses to move itself into the markup. Different refusals, one belief underneath: the browser already does most of this well, and a great deal of front-end work is arranging to do it again, differently, at a cost.

They are useful apart. The engine works with any stylesheet and the stylesheet works with no JavaScript at all. They are one project because they were written by somebody who was annoyed about the same thing twice.

They also sit beside a third thing. HTM/A brings server-held state and a transport; minline brings the small language for putting values into a page and the translations that go with it. The two are compatible by design rather than by dependency, so either can be adopted on its own and the second one added later, or never. Both of the old project sites were built with minline, its own and HTM/A's, which is a fair measure of how far the small one goes unaided.

Edgeswhere it stops

Where it stops, and what that leaves you

  • It is beta, and the source is not published. The 49 tests pass and the sizes are measured; both become checkable by anybody the day it ships.
  • No component model, no lifecycle, no store. Definitions and filters are the whole surface. An application that wants components with their own state and a router around them wants a framework, and this is not one.
  • The stylesheet has an opinion about your markup. Styling elements for what they are only works if the markup says what things are. On a document full of divs it has nothing to hold on to, and that is a fair reason to choose otherwise.
  • 1,337 bytes buys a small language. Iteration, attributes, classes, sanitised markup, values and six filters. Anything past that goes in your own JavaScript, which is where it was always going to be honest about living.