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
- 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-mlto 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
Startbefore the argument
Before the argument, the facts
Beta, and the source is not yet published, so the figures rest on trust until it is. What is measured: 1,337 bytes for the engine and 2,841 for the stylesheet, both compressed with Brotli, no dependencies in either, and 49 tests passing. The two halves ship together and are useful apart; this page treats them as one thing because they were written from one conviction.
The argument, in seven steps
- OneDeclared once, referenced by nameThe behaviour is in a definition; the markup just points at it.
- TwoThe real document, not a copyNo shadow tree to build, compare and reconcile.
- ThreeDefinitions composeThree small ones on one element beat one that does everything.
- FourEscaping is the defaultRaw HTML has to be asked for by name, which is the right way round.
- FiveA stylesheet without utility classesA class named after its appearance is CSS written in the markup.
- SixWhat the pair weighs1,337 bytes and 2,841 bytes, for the whole of both.
- SevenTwo halves, one convictionWhy they are one project and not two.
Then, for reference
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
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.