Usage rules & constraints
A design system says what it offers: tokens, components, their props and slots. Milda also lets it say how those pieces are meant to be put together - one primary action per form, no badge inside a button, colours only from tokens - in a form a checker can evaluate, an agent can follow and a person can read.
One principle splits this into two layers: a rule never restates the contract, it says only what the contract cannot. What can be decided from a single instance of a component belongs to that component's interface. What needs to know where the instance sits, or how many there are, is a usage rule.
#Interface constraints
Written in a component's block. They are part of the contract: the generated types enforce them where a type can, and adding one is a breaking change in the contract diff.
component IconButton { archetype Button props { loading : boolean = false loadingLabel : string required when loading "Read out while the spinner replaces the label." label : string labelledBy : string href : string target : string one-of label, labelledBy # exactly one exclusive href, target # at most one } }
x : T required- always supplied.x : T required when P- supplied whenever the conditionPholds. The condition reads the component's other props only: a UI state or a theme has no value where the component is used.one-of a, b- exactly one is supplied;any-of a, b- at least one;exclusive a, b- at most one.
“Supplied” means written at the use site. A declared default does not count: a constraint over a prop says the default is only a placeholder.
#What the React target emits
A component with a constraint types its props as a base interface intersected with one union per constraint, so the compiler rejects the misuse:
<IconButton label="Save" /> // ok
<IconButton /> // error: label or labelledBy
<IconButton label="Save" labelledBy="t" /> // error: not both
<IconButton label="Save" loading /> // error: loadingLabel is required
<IconButton label="Save" loading={busy} /> // error: busy may be trueA condition no type can say - an ordering like count > 0, or two props at once - leaves the prop optional, is listed in the target's report of what it could not translate, and is enforced by milda check instead.
milda check evaluates every rule kind and the interface constraints over a consumer’s TSX and JSX, following its own components across files. Stylesheets and CSS-in-JS are not read yet.proposal 0053
#Usage rules
A top-level declaration, decided over the usage tree: the instances of the library's components as a consumer composes them. Every rule has an id (what a finding reports), a severity, and a mandatory reason, which the agent reads verbatim.
rule one-primary-action error { max 1 Button where variant = Primary within archetype Form because "A form has one way forward. A second primary action makes the reader choose." }
#The subject
- A component, by the name it is declared with:
Button. - An archetype:
archetype Form- anything that realizes it, whether a library component or what the checker knows is native on a platform (a<form>on the web). This is how a rule stays platform-neutral without being useless. - Either, narrowed by its props:
Button where variant = Primary. An unwritten prop takes its default.
#The statements
Every statement opens with one keyword of its own.
max 1 Button where variant = Primary within archetype Form # at most min 1 Tab within Tabs # at least - a required child min 2 max 7 Tab within Tabs # both bounds never Badge within Button # forbidden nesting, any depth only Tab within Tabs, archetype ContextMenu # a required parent tokens color, spacing # values only from tokens prefer Link for archetype Link # the component an intent goes through
- A count is decided per scope: each subject instance counts for its nearest enclosing scope, so a nested form does not lend its button to the outer one.
tokensnames token types the way the foundations block spells them, never style properties.preferis broken by anything that realizes the archetype and is not that component: a native equivalent, another library component, or one written in the consuming app.
variant={kind}) does not match, and what it cannot prove is not reported: a Tab returned from the app's own wrapper, used elsewhere, is not an only violation.#Where they go
- AGENTS.md states the interface constraints and the usage rules, errors first, with each rule's reason, by the identifiers the package exports.
- DESIGN.md lists the constraints with each component's props, the rules that name a component under it, and the whole list at the end.
- design-system.json carries them as data: a top-level
rulesarray (always present), andrequiredWhen/constraintson the components. - milda check evaluates both layers over the code that uses the system: each rule reports under its own id (
rule:tab-placement) with its severity and its reason, and a broken interface constraint is an error. It follows the app's own components across files, so the wrapper above is judged where it is rendered. - The release lists added, removed and re-graded rules in its changes. They never move the version: a rule cannot break a build.
In the Studio, constraints are edited in the Interface panel - the prop editor's Required is Optional, Always or When…, and Between props holds the group constraints - and usage rules in Foundations → Manage → Usage rules. See also The .milda file for the surface version that introduced them.
These words, in the reference
- A usage rule: how the system is meant to be composed, checked over the instances a consumer writes.
- The prop must be given.
- A constraint over a group of props.
Open a word for its entry, or read them all in the reference.
See it built in the tour11. Usage rules5. The contract