@jam-overture/ loom-primitives
The starter library — the primitives every example on this site is built from. Its own package, installed beside the framework.
Everything below is exported from that import. The names, the signatures and the sentences are read from the package itself rather than written here, so this page says what the copy of Loom in your node_modules says — and it changes in the same pull request the code does.
56 exports, in 7 modules. Generated from ./dist/primitives/index.d.ts, which is the declaration file this package publishes for @jam-overture/loom-primitives.
Install this first. @jam-overture/loom-primitives loads it the moment the import runs. Without it, the import itself fails — before any of your own code has run.
react^19.0.0optional peer dependency
pnpm add react
No import here has everything behind it. @jam-overture/loom-primitives publishes 56 of the 1,298 names this package publishes. The other 1,242 are behind one of the 16 other imports, and 15 of those 16 publish nothing this one does. The imports do not nest: a name that is not on this page is not a name that does not exist, and the search at the top of the page says which import it comes from.
Some of these names are published elsewhere too: 13 by @jam-overture/loom-primitives/compositions — the same declarations reached through two doors, so either import gives you the same thing.
The starter library
primitives
The starter primitive library, in three layers.
STARTER_PRIMITIVESvalue
Shown in use on Primitives and the registry — Start from the starter library and Starting from a band — Registering only what the bands need
const STARTER_PRIMITIVES: readonly PrimitiveEntry[]
createStarterPrimitiveRegistryfunction
A registry over the starter library, as a Result like any other — building one is the same operation for this library as for a host's own, and a helper that threw where the general function returns would be a second contract to learn.
Shown in use on Installation — TypeScript, Rendering a tree and Primitives and the registry — Start from the starter library
const createStarterPrimitiveRegistry: (additional?: readonly PrimitiveEntry[]) => Result<PrimitiveRegistry, RegistryError>
Compositions
primitives/compositions
STARTER_COMPOSITIONSvalue
Every band on offer — the phrasebook, not the page.
const STARTER_COMPOSITIONS: readonly Composition[]
CATALOGUE_TYPESvalue
Every primitive type some band in the phrasebook builds, sorted.
Shown in use on Starting from a band — Registering only what the bands need
const CATALOGUE_TYPES: readonly string[]
compositionByIdfunction
The band with this id, or undefined.
Shown in use on Starting from a band — Ask for one by name
const compositionById: (id: string) => Composition | undefined
compositionsForPartfunction
Every design of one band, in catalogue order, canonical design first.
Shown in use on Starting from a band — Parts and designs are two different words
const compositionsForPart: (part: CompositionPart) => readonly Composition[]
PAGE_SEQUENCEvalue
One design of each band, in the order a page uses them.
Shown in use on Starting from a band — A whole page to start from
const PAGE_SEQUENCE: readonly Composition[]
Composition
primitives/compositions/composition
COMPOSITION_PARTSvalue
The bands a page is made of, in the order a page uses them.
const COMPOSITION_PARTS: readonly ["banner", "nav", "hero", "proof", "features", "bento", "steps", "code", "integrations", "metrics", "specs", "pricing", "comparison", "testimonials", "credentials", "team", "articles", "changelog", "faq", "contact", "cta", "footer"]
CompositionParttype
type CompositionPart = (typeof COMPOSITION_PARTS)[number];
Compositiontype
A band of a page, dropped in whole as one operation.
type Composition = {
/**
* Lower-case words joined by hyphens, the way an anchor or a slot name is.
*
* **A part's canonical design has the part's own name as its id**, and every
* other design of that part is the part name plus what makes it different —
* `hero` and `hero-split`. That convention is not cosmetic: it is what lets
* the page sequence be *derived* rather than maintained as a second hand-kept
* list that could disagree with the first. There is a test on it.
*/
readonly id: string;
/** Which band of a page this is a design of. */
readonly part: CompositionPart;
/** What a person choosing it from a list reads. */
readonly label: string;
/**
* What lands on the page, in one clause, said before it happens.
*
* About the page and never about the verdict, for the reason the demo's
* presets give: whether the Gate applies a band or holds it is computed from
* the tree at assessment time, so a promise about the outcome would be a
* surface predicting a decision it does not make.
*/
readonly promise: string;
/** The interpreter's own words about why this subtree answers that ask. */
readonly rationale: string;
/** Every primitive type the subtree uses, for the catalogue and the audit. */
readonly uses: readonly string[];
readonly build: (ids: IdFactory) => ElementNode;
};COMPOSITION_INTERPRETERvalue
What produced the delta, for Provenance.interpreter. Not a model.
const COMPOSITION_INTERPRETER = "loom/composition"
CompositionTargettype
Where the band goes.
type CompositionTarget = {
readonly parentId?: NodeId;
/** Clamped to the parent's current child count, never trusted as an index. */
readonly index?: number;
};CompositionPlantype
type CompositionPlan = {
readonly outcome: "planned";
readonly operations: readonly TreeOperation[];
}
/**
* The named parent is not in this tree, so there is nowhere to put the band.
*
* A distinct outcome rather than a silent append at the root: a band that
* quietly landed somewhere other than where it was asked for is worse than
* one that did not land, because the second is visible.
*/
| {
readonly outcome: "no-such-parent";
readonly parentId: NodeId;
};planCompositionfunction
One insert, computed against the tree as it stands.
Shown in use on Starting from a band — It goes on the page as one insert
const planComposition: (composition: Composition, tree: LoomTree, ids: IdFactory, target?: CompositionTarget) => CompositionPlan
compositionInterpreterfunction
A composition as an ordinary ChangeInterpreter.
Shown in use on Starting from a band — Nothing gets to skip the Gate
const compositionInterpreter: (composition: Composition, idFactory: IdFactory, clock: Clock, target?: CompositionTarget) => ChangeInterpreter
Control
primitives/control
The paint and the sizing shared by the library's two controls.
CONTROL_VARIANTSvalue
const CONTROL_VARIANTS: readonly ["primary", "secondary", "quiet"]
ControlVarianttype
type ControlVariant = (typeof CONTROL_VARIANTS)[number];
CONTROL_SCALESvalue
const CONTROL_SCALES: readonly ["small", "medium", "large"]
ControlScaletype
type ControlScale = (typeof CONTROL_SCALES)[number];
controlStylefunction
Property order matters here and is not cosmetic: React writes an inline style in insertion order, so reordering these changes every rendered page's markup without changing a pixel. The order below is the one loom.action shipped with, kept so that extracting this file changed no output at all.
const controlStyle: (variant: ControlVariant, scale: ControlScale) => CSSProperties
Layout
primitives/layout
The arrangement vocabulary the compose-and-arrange primitives share.
GAP_NAMESvalue
const GAP_NAMES: readonly ["none", "tight", "snug", "normal", "loose", "roomy"]
GapNametype
type GapName = (typeof GAP_NAMES)[number];
GAPSvalue
Six names is more resolution than any other prop in the library carries, and it is deliberate here: a stack is the one primitive used at every scale on a page, from the glyph beside a label to the distance between two bands. Every other prop that could have been a length was given three options because three was all the distinctions that mattered. Here they all matter, and the grammar budget is spent on the primitive that spends it best.
const GAPS: Readonly<Record<GapName, string>>
ALIGN_NAMESvalue
const ALIGN_NAMES: readonly ["start", "center", "end", "stretch", "baseline"]
AlignNametype
type AlignName = (typeof ALIGN_NAMES)[number];
ALIGNMENTSvalue
Cross-axis placement. The names are CSS's own, less the flexbox flex- prefix.
const ALIGNMENTS: Readonly<Record<AlignName, string>>
JUSTIFY_NAMESvalue
const JUSTIFY_NAMES: readonly ["start", "center", "end", "between"]
JustifyNametype
type JustifyName = (typeof JUSTIFY_NAMES)[number];
JUSTIFICATIONSvalue
Main-axis distribution. between is the only one that is not a simple edge.
const JUSTIFICATIONS: Readonly<Record<JustifyName, string>>
COLUMN_NAMESvalue
const COLUMN_NAMES: readonly ["auto", "two", "three", "four"]
ColumnNametype
type ColumnName = (typeof COLUMN_NAMES)[number];
COLUMN_MINIMUMSvalue
A **floor for each column, never a count** — the distinction the granularity doc names as the easiest one to get wrong. Fed to auto-fit, so three means "columns no narrower than this, which is three of them at a common page width and one of them on a phone". Nothing here truncates a list, so changing it changes no node, which is what keeps it a prop.
const COLUMN_MINIMUMS: Readonly<Record<ColumnName, string>>
ASPECT_NAMESvalue
const ASPECT_NAMES: readonly ["square", "wide", "portrait"]
AspectNametype
type AspectName = (typeof ASPECT_NAMES)[number];
ASPECT_RATIOSvalue
The shapes a fixed-ratio frame may take.
const ASPECT_RATIOS: Readonly<Record<AspectName, string>>
Stylesheet
primitives/stylesheet
The one stylesheet this library emits, and the reason there is one at all.
STYLESHEET_HREFvalue
const STYLESHEET_HREF = "loom-primitives"
STYLESHEET_PRECEDENCEvalue
const STYLESHEET_PRECEDENCE = "loom"
MARQUEE_GAPvalue
The space a loom.marquee puts between its items, declared by the primitive and applied by the rules below.
const MARQUEE_GAP = "--loom-marquee-gap"
LIBRARY_CLASSvalue
Class names the library's primitives apply. Exported so a test can name them.
const LIBRARY_CLASS: {
/** Fades and lifts into place once, on entry. Stagger with `animationDelay`. */
readonly rise: "loom-rise";
/** Raises a card on hover, and marks it as a surface that responds. */
readonly lift: "loom-lift";
/** Wipes an underline in from the left on hover — a link that is a whole tile. */
readonly underline: "loom-underline";
/**
* A link inside a sentence: underlined at rest, in the paragraph's own ink.
*
* The whole of `loom.inline-link`'s appearance is here and none of it is on
* the element, which is this file's first mechanic being used rather than
* worked around — an inline `color` would be a colour no paragraph could
* lend it, and being lent the paragraph's colour is that primitive's entire
* design.
*/
readonly inlineLink: "loom-inline-link";
/** The `↗` after an outbound phrase, in its own decoration context. */
readonly inlineLinkOutward: "loom-inline-link-outward";
/** Two slow-drifting colour fields, for a hero backdrop that is not a flat wash. */
readonly aurora: "loom-aurora";
/** The disclosure marker of a `details`, rotated when its section is open. */
readonly marker: "loom-marker";
/** A logo held back to grey until it is pointed at. */
readonly mark: "loom-mark";
/** A `loom.milestone-list`: spaces its entries, ends its rail, sizes its marker column. */
readonly rail: "loom-rail";
/** The same list, set tighter — a changelog to scan rather than a history to read. */
readonly railTight: "loom-rail-tight";
/** The same list with the connecting line dropped, dots kept. */
readonly railNone: "loom-rail-none";
/**
* One `loom.milestone`, and the reason its own layout is here rather than on
* the element: **a child that lays itself out inline cannot be rearranged by
* the container it is in.** An inline style beats a rule, so the marker
* column, the rail's direction and the dot's optical offset were all
* unreachable from any parent — and a second arrangement of the same content
* model is exactly what the naming rule — a container is its child's name
* plus the arrangement — says a second container is for. Moving four
* declarations into this file is what let `loom.milestone-row` exist without
* a second child type that renders the same three fields.
*
* It stays on the *entry* rather than on the list, which is the property the
* inline version was protecting: a milestone that finds itself outside a
* list still lays itself out, because the class travels with the child.
*/
readonly milestone: "loom-milestone";
/** Its marker cell — right-aligned on a rail, above the title in a row. */
readonly railMarker: "loom-rail-marker";
/** Its dot-and-line cell, which runs down the page on a rail and across it in a row. */
readonly railTrack: "loom-rail-track";
/** The dot itself, offset onto the marker's first line where the marker is beside it. */
readonly railDot: "loom-rail-dot";
/** One entry's connector, hidden on the last entry because only CSS knows which that is. */
readonly railLine: "loom-rail-line";
/** One entry's content cell, which carries the gap to the entry below it. */
readonly railBody: "loom-rail-body";
/** A `loom.milestone-row`: the same entries laid across, as a process band. */
readonly milestoneRow: "loom-milestone-row";
/**
* A `loom.embed` frame whose shape follows its own width — portrait while it
* is narrow, 16/10 once it is not. The one shape that cannot be an inline
* `aspect-ratio`, because there is no single ratio to write down.
*/
readonly frameAdaptive: "loom-frame-adaptive";
/** A card whose title anchor is stretched over the whole surface. */
readonly cover: "loom-cover";
/** That anchor. Its `::after` is what makes the surface clickable. */
readonly coverLink: "loom-cover-link";
/** A card's cover image, cropped to a ratio the card owns rather than the file does. */
readonly coverMedia: "loom-cover-media";
/** A card's padded interior, so a lead layout can set it beside the cover. */
readonly coverBody: "loom-cover-body";
/** A `loom.article-grid` running its first piece across the top. */
readonly lead: "loom-lead";
/** One `loom.field`. Named so the form it sits in can size it in a row. */
readonly field: "loom-field";
/** Any control inside a field: the focus ring, the hover, the placeholder. */
readonly input: "loom-input";
/** The wrapper that draws a select's chevron, since the native one is not the palette's. */
readonly select: "loom-select";
/** A `loom.form` laid out as a single row — see the rule about sizing children. */
readonly formInline: "loom-form-inline";
/**
* A `loom.avatar-row` cluster: each face overlaps the one before it and rings
* itself in the page's ground so the edge reads. The ring is `bg-canvas`
* because a primitive cannot know what it is sitting on — a cluster placed
* inside a `loom.card` rings itself in the canvas colour rather than the
* card's, which is a hair off and the honest limit of a static stylesheet.
*/
readonly cluster: "loom-cluster";
/**
* The region a band too wide for the screen scrolls inside — `loom.table`'s
* and `loom.comparison-table`'s, which had one each and neither had anything
* but `overflow-x`.
*
* It is a class rather than the inline pair it replaces because three of the
* four things a scroller needs cannot be said on the element: the snap
* positions belong to the children, the focus ring is a state selector, and
* the scrollbar is the browser's own furniture. `loom.carousel` worked all of
* this out on 28 August and the only thing added here is
* `scroll-padding-inline-start`, which a carousel has no use for and a tab
…Cut short here — the whole of it is longer than a page can usefully hold.
libraryStylesheetfunction
Emitted beside a primitive's own root element rather than around it, for the reason editable.ts gives for never wrapping: a wrapper changes what >, :first-child and :nth-child select. A hoisted <style> is removed from the flow entirely, and an un-hoisted one is a metadata element siblings do not count.
const libraryStylesheet: () => ReactElement
libraryStylesheetTextfunction
The same rules as text, for a document React is not assembling.
const libraryStylesheetText: () => string
Tokens
primitives/tokens
The only way a primitive in this library names a colour, a size, or a length.
RampSteptype
type RampStep = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8;
RAMPvalue
const RAMP: readonly RampStep[]
colourfunction
const colour: (slot: PaletteSlot) => string
spacefunction
A length off the style preset's spacing scale.
const space: (step: RampStep) => string
sizefunction
A size off the font pack's type ramp.
const size: (step: RampStep) => string
radiusfunction
const radius: (name: "sm" | "md" | "lg" | "full") => string
familyfunction
const family: (role: "heading" | "body") => string
weightfunction
const weight: (role: "heading" | "body") => string
motionfunction
const motion: (speed: "fast" | "medium" | "slow") => string
MONOSPACE_STACKvalue
The fallback for the one family a font pack is allowed not to supply.
const MONOSPACE_STACK = "ui-monospace, SFMono-Regular, Menlo, Consolas, \"Liberation Mono\", monospace"
monospacefunction
const monospace: () => string
hairlinefunction
The colour a line takes when the line is the only thing being drawn.
const hairline: () => string
READABLE_MEASUREvalue
The reading measure, as a length rather than a palette slot.
const READABLE_MEASURE = "68ch"
WIDTHSvalue
const WIDTHS: {
readonly full: "100%";
readonly wide: "1120px";
readonly readable: "68ch";
}WidthNametype
type WidthName = keyof typeof WIDTHS;