Navigation

v0.0.4
Building

Contracts

Read component markup and public properties, understand what the checker verifies, and describe a component of your own.

On this page

Every stylesheet in the package has a contract beside it: button.css and button.json. The contract says what the stylesheet promises, in a form a program can read: its classes, where each one goes, the states it draws, the custom properties it publishes and the smallest correct markup.

The checker, generated component tables, portable component pages and registry read these contracts. Keeping the class rules in one source reduces duplication; package tests check that the contract and stylesheet agree. A contract describes the supported markup, but does not replace a browser check of interaction, accessibility or visual behavior.

Start with npx master-ui contract button to find the markup and public properties of a button. You can also import it as @master/ui/components/button.json, or take all of them from @master/ui/contracts.json or https://ui.master.co/contracts.json. The schema is https://ui.master.co/contract.schema.json.

#What a contract holds

KeyHolds
name, title, tier, category, descriptionThe slug, which is the stylesheet's file name; the name a reader sees; component or layout; its group; one sentence on what it is
useWhen to reach for it
insteadNeighbors that are the right choice in another situation, each as a when and the name of another contract in use
composesOther contracts whose classes its markup uses, such as input and listbox for a combobox. Their stylesheets must be imported too
scriptnone, optional or required
behaviorThe reference script that drives the pattern
keyboardWhat the keys do, natively or once the behavior is attached
platformEach platform feature used as an enhancement, with what a browser without it shows
classesEvery class the stylesheet defines, and no other
statesEach state the stylesheet draws, and the attribute or pseudo-class that marks it
propertiesThe public custom properties
exampleThe smallest correct markup

name, title, tier, category, description, use, script, classes and example are required. The schema allows no key beyond the ones in this table and $schema.

#Classes

Each class has a name, a kind and a description. The kind says where the class may stand.

Roots, modifiers and parts

A modifier shares an element; a part sits inside it. The root dialog and modifier dialog-drawer belong on the same dialog element. Its child heading carries the part dialog-title. Adding the modifier to a separate element would not satisfy that relationship.

KindWhere it goesExample
rootBy itself. A stylesheet may have severalbtn, btn-group, dialog
partInside its root, which it names in ofdialog-title of dialog
modifierOn the same element as its base, which it names in ofbtn-primary of btn

A modifier of a part names the part: menu-item-danger is of menu-item. of is a list when a class goes with any one of several. These are three entries from the button's classes:

button.json
{ "name": "btn", "kind": "root", "description": "The raised base button", "elements": ["button", "a", "summary", "label", "input"] },{ "name": "btn-sm", "kind": "modifier", "of": "btn", "exclusive": "size", "description": "32px tall; the default is 38px" },{ "name": "btn-square", "kind": "modifier", "of": "btn", "label": true, "description": "A square button for one icon; it needs an accessible name" }

#What the checker enforces

The kind already gives the checker two rules: a modifier needs its base, and a part needs its root. Four optional keys on a class turn a rule of the documentation into something master-ui check reports. A contract adds one only when the stylesheet or the platform requires it, because a wrong constraint is a false error in someone's project.

KeyMeaningExample
elementsThe class only works on these elements. Left out when any element will dodialog on ["dialog"]
attributesAttributes the element must carry: true for any value, a string for one value, an array for one of severalswitch: { "type": "checkbox", "role": "switch" }
labelThe element needs an accessible namebtn-square, dialog-close
exclusiveA group key. Two modifiers with the same key do not share an elementbtn-sm and btn-lg are both "size"

A fifth key loosens a rule. "anywhere": true marks a part that may stand outside its root, such as field-hint, a part of field that may also stand outside one.

#States and properties

A state is drawn by the stylesheet and marked in the markup with a native or ARIA attribute. There is never a class for a state. Each entry names the class that carries the mark in on, a short name, and the mark as it is written in markup or CSS.

button.json
{ "on": "btn", "name": "pressed", "mark": "aria-pressed=\"true\"", "description": "A quiet toggle that is on stays sunk into a well" }

A mark that begins with a colon, such as :user-invalid or :popover-open, is a pseudo-class the browser sets; there is nothing to write for it.

properties lists the custom properties that are public API: the ones a project may set. Each has a name and a description, the class that declares or reads it in on when that is not the first root, and a default when the stylesheet declares one.

button.json
{ "name": "--btn-height", "default": "2.375rem", "description": "Minimum block size" }

Component-specific custom properties outside this list are implementation details. Theme tokens are a separate public surface shared across components; see Default theme for their roles. Use Customization to choose between a theme token and a component property, and Stability for the current version policy.

#Script and behavior

script says how much of the component depends on JavaScript.

ValueMeaning
noneEvery state works without script
optionalA script adds a convenience, such as arrow keys in a menu, and the page works without it
requiredThe pattern has no native element, so a script moves its attributes

behavior names the reference script, published as @master/ui/behaviors/<behavior>.js. The combobox's contract says "script": "required" and "behavior": "combobox":

import { combobox } from '@master/ui/behaviors/combobox.js'combobox(document.querySelector('.combobox'))

A behavior moves the attributes the stylesheet reads. Patterns such as a splitter also pass event-derived values through a public custom property; the stylesheet decides how to draw them. keyboard lists what the keys do once it is attached, or what the native element already does. See behaviors.

#The example

example is the smallest markup that is correct, with real words in it. The package tests run the checker over every example and expect no diagnostics. Use it as the markup baseline, then provide the labels, unique ids, application actions and any required behavior for your page.

<button type="button" class="btn btn-primary">Save changes</button>

#Write a contract for your own component

A project adds a component of its own when it has something the library lacks, with a material, a state, a platform behavior or a geometry its pieces share. Write its stylesheet in @layer components, with class selectors and tokens, and write its contract beside it in ui/. The contract follows the same schema as the library's.

ui/price-tag.css
@layer components {  .price-tag {    --price-tag-size: 1.125rem;    display: inline-flex;    align-items: baseline;    gap: .375rem;    color: var(--color-text-strong);    font-size: var(--price-tag-size);    font-variant-numeric: tabular-nums;    &.price-tag-sale {      color: var(--color-danger);    }  }  .price-tag-was {    color: var(--color-text-muted);    font-size: .75em;  }}
ui/price-tag.json
{  "$schema": "https://ui.master.co/contract.schema.json",  "name": "price-tag",  "title": "Price tag",  "tier": "component",  "category": "Commerce",  "description": "The price of one product, with the price it replaces.",  "use": "A price on a product card or in a cart row. Add price-tag-sale and the old price when the product is reduced.",  "script": "none",  "classes": [    { "name": "price-tag", "kind": "root", "description": "The current price, in strong ink and tabular figures" },    { "name": "price-tag-sale", "kind": "modifier", "of": "price-tag", "description": "A reduced price, in the danger hue" },    { "name": "price-tag-was", "kind": "part", "of": "price-tag", "elements": ["s"], "description": "The price before the reduction, struck through in muted ink" }  ],  "properties": [    { "name": "--price-tag-size", "default": "1.125rem", "description": "Font size of the current price" }  ],  "example": "<p class=\"price-tag price-tag-sale\">$24 <s class=\"price-tag-was\">$32</s></p>"}

Import ui/price-tag.css after Master UI in the application stylesheet so the example is drawn. The CLI discovers the sibling contract independently of that CSS import:

app.css
@import '@master/css';@import '@master/ui';@import './ui/price-tag.css';

The project's component is then available to the same tools:

  • npx master-ui list lists it, and npx master-ui contract price-tag prints it. An agent that reads contracts before writing markup finds it the same way.
  • npx master-ui check holds markup to it. Here npx master-ui check cart.html reports three mistakes:
cart.html
<p class="price-tag-sale">$24</p><p class="price-tag">$24 <span class="price-tag-was">$32</span></p><p class="price-tag price-tag-large">$24</p>
cart.html:1:1 error missing-base "price-tag-sale" modifies "price-tag": write class="price-tag price-tag-sale".cart.html:2:26 error element "price-tag-was" goes on <s>, not <span>.cart.html:3:1 warning unknown-class "price-tag-large" is not a class of price-tag. It has: price-tag, price-tag-sale, price-tag-was.1 files checked: 2 errors, 1 warnings.

Three details:

  • Where contracts are read from. Every .json file in the project's ui/ directory that has a name and a classes array. master-ui.json may name another directory in directory and more patterns in a contracts array.
  • The schema line. "$schema": "https://ui.master.co/contract.schema.json" lets an editor validate the file as it is written.
  • A name the library already has. A project contract with the name of a library contract replaces it. That is how an owned component is described by its copy.

#Diagnostics

master-ui check prints one line per finding: the file, line and column, the severity, a code and a message. Every code is an error, except unknown-class, which is a warning unless --strict is passed.

CodeWhat it meansHow to fix it
unknown-classA class starts with a root's name and a dash, and is in no contractUse one of the classes the message lists. For a class of the project's own, add it to a contract in ui/ or give it a name outside the component's
missing-baseA modifier is on an element without the class it modifiesAdd the base beside it: class="btn btn-primary"
outside-rootA part has no ancestor with its root classMove it inside the root, or add the root class to the container
exclusiveTwo modifiers of the same group are on one element, such as two sizesKeep one
elementA class is on an element its contract does not allowUse the element the message names: dialog goes on a dialog element
attributeThe element lacks an attribute the class requires, or has another valueAdd the attribute the message names, such as role="switch"
labelA control has no accessible nameGive it text of its own, aria-label, aria-labelledby, or a label that wraps it or points at it

The checker reads source rather than rendering a page. It can inspect literal classes inside expressions, but cannot resolve computed class names, prove where a framework component renders its children or inspect a root in another file. It leaves uncertain cases alone. A clean result does not verify focus order, contrast, browser support or whether the chosen component fits the task.