skip to the page

@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;