The .milda file
One text file carries a whole design system - foundations, components, the library's organisation, the demo scenes, the asset library and the documentation - and reads back into the editor it came from, or into any reader that implements the grammar. This page is what the file promises: what it carries, what it deliberately leaves out, and how its references resolve.
Everything here is the reader's contract, not a description of our exporter. If a field of a design system is not in the “carried” list and not in the “left out” list with a reason, that is a bug in the format.
#What it carries
A file is a sequence of declarations, in any order. The header declares the surface version; everything else is the system:
milda 0.4 # the surface version - see "Versioning" below prelude 0.0.2 # the design-system vocabulary (archetypes, atoms, facets) meta { name "Acme DS"; version "2.1.0" } context-group ColorScheme { Light Dark } foundations { layer Base { token palette/blue/500 : color = hex(3b82f6) "Our accent, at rest." token space/2 : spacing = 8px token fast : duration = 150ms } } type Tone = enum(info, danger) group Forms "Things you type into." component Field "A labelled text input." { archetype TextInput group Forms props { label : string; value : string; disabled : boolean = false } events { change } part root { kind container; layout { stack column; gap space.2 } } part root.label { kind text; content bind label; ink palette.blue.500 } part root.input { kind input bind value value emits { change on change payload bind value } a11y { labelled-by root.label } } } example "Field in a form" for Field { demo { open : boolean = false } part root { kind container } } assets { svg "logo" { mime "image/svg+xml"; view-box "0 0 24 24"; url "https://…" } } docs { page "Fields" in Guides { heading "Fields" anchor fields component Field component-props Field } }
- foundations - context groups, layers, token groups, every token with its type, value and description.
- shared types- your type declarations (the language's built-ins are implicit).
- components - archetype, description, contract (props with defaults, bindings and annotations, events), authored behaviour composition, and the whole anatomy.
- every part - its path in the tree, kind, provenance, presence, slot descriptor, text, accessible naming, event emissions, state and value bindings, value type, repetition, cell role, control and group realization, presentation intent, media provenance and alt text, link destination, facets, layout, conditional state rules, transitions, nested instances, free behaviours, foreign content, demo actions, and the per-archetype configuration surfaces.
- the library- component groups (nestable, with their prose) and each component's membership.
- examples - demo scenes: what they demonstrate, their own state variables, their node tree.
- assets- the library: folders, each asset's identity and metadata, and where its bytes are.
- docs - site copy, footer, pages with their section and nesting, prose, and every transclusion.
- output settings- the emission idiom and the date/time/colour representations. These are the generated components' public API, so a file without them describes a different contract than the one that was published.
#What it leaves out, and why
Five things, each for the same kind of reason: they belong to a workspace rather than to the system.
- the project / document id - the file is portable because it is not tied to one workspace.
- activity history - a record about the system, not the system.
- release history and contract digests- the project's record of what it shipped.
- publishing config, connections, credentials - where bytes go is not what the system is.
- a part's platform element -
div/span/svgwould make the file a DOM description. The neutralkindis carried, and each target derives its element from it.
#Everything is addressed by name
No reference in a .milda file points at an internal id. Every one names something the file itself declares, which is what lets a reader resolve it without our database:
- a part(an ancestor predicate, accessible naming, a behaviour's target) - its dotted part path,
root.surface.label - a component (an instance, a docs block) - its declaration identifier
- an event (an emission) - its declared name
- a token - its path,
palette.blue.500 - a component group or asset folder - its slash path,
Forms/Text - a docs page (as a parent) - its title
- an example or an asset - its name
root.surface.label is the child label of the child surface of root. The prefix IS the parent - the path is the only place nesting is recorded, which is why a part is declared with a path rather than a bare name.One consequence worth knowing when you read a file: a declaration name is an identifier, because references resolve against it. Where the authored name is free text - a component called “Color picker” - the identifier is derived and the spelling you typed travels in meta { name "Color picker" }.
#What a part can say
A part block is not only paint. Each statement is a real keyword, so the file reads as a description of the component rather than as serialised state:
part root.input { kind input # what it IS - neutral; the element is the target's decision origin author # provenance: archetype-born, or you added it locked present when not disabled # renders only when the predicate holds content bind label # or: content "Submit", or a block with when … -> … legs a11y { labelled-by root.label; described-by root.hint } emits { change on change payload bind value } bind value value # this part's value is this prop bind state disabled disabled # a UI state driven by a prop value string repeat items as item # or: repeat static ["a", "b"] as item cell header col control checkbox # checkbox | switch presentation accessible-name media img source bind src sample url "https://…" alt "A picture" destination bind href fill palette.blue.500 # paint, as before layout { stack row; gap space.2 } state hovered { fill palette.blue.600 } when disabled { opacity dim } motion { transition fill ink duration fast easing standard } instance Chip { label = "New"; tone = bind value } behavior focus-trap { return-focus-on-close true; initial-focus-node-id root.input } calendar { selection-mode range; week-starts-on 1 } severity danger foreign web-react "Keyboard hint" "<kbd>Cmd K</kbd>" }
text is already a paint facet - text mdsets the font size. A keyword whose meaning depended on whether its argument was a string or a word would read fine and parse wrong, so a part's text is content.#Foreign content is quarantined
foreign carries code written for one specific target. It is in the file because silently dropping code you wrote is worse than carrying it - but it is genuinely not portable, and the target tag is what makes that honest:
- A reader that does not implement
<target>must skip the block. It must not interpret, translate or execute it, and it should surface that the component carries non-portable content. - Milda's own reader warns when it meets a target it cannot read, so an unsupported snippet is reported rather than silently ignored.
#Where the asset bytes are
An asset declaration carries its identity and metadata plus where the bytes are, and that is the one choice the format offers. Both are the same grammar, so a reader implements one path:
assets { folder Brand/Icons # Bytes beside the file - the text stays diffable, which is what a pull request needs. svg "logo" in Brand/Icons { mime "image/svg+xml"; url "https://…/logo.svg" } # Bytes IN the file - it then needs nothing else to be complete. svg "mark" { mime "image/svg+xml"; data "data:image/svg+xml;base64,…" } }
The Studio offers both: Download bundle writes the .milda plus an assets/ directory (with a manifest that also names anything it could not include), and Download self-contained inlines every asset as a data: URI. The bundle is the one to review in a pull request; the self-contained file is the one to hand to someone.
#Versioning
The header declares two versions, and they answer different questions. prelude is the design-system vocabulary - archetypes, atoms, facets. milda is the surface version, and it is what tells you whether a file omits something because the writer predated it or because the system does not have it: a milda 0.1 file has no motion statement because motion had no syntax yet, while a milda 0.4 file without one has no motion. The surface version is bumped whenever the grammar gains capability, which is why no third version number is needed.
0.4 is the first version that breaks rather than adds. A name followed by its type now has one spelling everywhere - the colon - so a props block is written size : Size and a demo block open : boolean, exactly as token brand : color and field : string already were. The colon-less form those two blocks used to accept is an error.
If you are implementing a reader: accept a file whose milda version you do not know, parse what you recognise, and report the rest rather than failing. The grammar is additive and every statement is self-delimiting.
#Reading further
- The grammar itself -
packages/milda/spec/GRAMMAR.ebnf- and the contract in prose,packages/milda/spec/FORMAT.md. - Values & literals for what goes on the right-hand side of a token, and Facets & tokens for the paint vocabulary.
- The authoring walkthrough for the same model seen from the Studio.