Pattern: The Provider
Solving Parameter Drilling with Scope
One of the biggest challenges in building complex Typst templates is Parameter Drilling.
If you have a document hierarchy like Book > Chapter > Section > Component, and the Component needs to know the "Primary Color" or the "Current Theme," you traditionally have to pass that variable manually through every single function call.
Loom solves this with the Provider Pattern.
The Problem: Brittle Templates
In standard Typst, your code often looks like this. You are forced to be a "data courier," carrying variables down to places that need them.
// ❌ The old way: Passing state manually everywhere
#let my-chapter(number, theme-color, body) = {
// You have to accept 'theme-color' just to pass it down...
block(body(number, theme-color))
}
#let my-section(chapter-num, theme-color, body) = {
// ...and pass it down again...
text(fill: theme-color)[#chapter-num.1]
}
This is brittle. If you want to add a "font-size" setting later, you have to update every single function signature in the chain.
The Solution: Context Injection
Loom components (Motifs) have a built-in mechanism called Scope. It works remarkably like CSS variables.
Any component can act as a Provider, injecting data into a shared context (ctx) that automatically "cascades" down to all descendants. The descendants act as Consumers, reading from that context without knowing who provided it.
Implementing a "Smart" Component
The most robust way to use Scope is to handle three cases at once:
- Override: The user specifically passed a value (
color: red). - Inherit: The user passed
auto, so we look up the tree. - Default: No one defined it, so we fallback to a safe value (
black).
Loom makes this easy with loom.mutator.
#import "loom-wrapper.typ": *
#import loom: mutator
// THE COMPONENT
#let my-button(label, color: auto) = motif(
// 1. THE SCOPE PHASE (Logic)
// We determine the final value BEFORE we draw.
// The syntax is: (key, override_value, default: fallback_default)
scope: (ctx) => mutator.batch(ctx, {
mutator.derive("my-btn-color", color, default: black)
}),
// 2. THE DRAW PHASE (Render)
// We can now safely assume 'ctx.my-btn-color' exists.
draw: (ctx, public, view, body) => {
box(fill: ctx.my-btn-color, inset: 10pt)[#label]
},
none
)
Using the Component
Because we implemented the Provider pattern, this single component is now incredibly flexible:
// Case 1: Explicit Override
#my-button("Danger", color: red)
// Case 2: Context Inheritance (The Provider)
// We set the color ONCE at the top...
#apply(my-btn-color: color)[
#stack(dir: ltr)[
#my-button("Submit") // ...and these automatically become Blue
#my-button("Cancel") // ...without passing arguments!
]
]
// Case 3: Robust Default
#my-button("Boring") // Defaults to Black, doesn't crash.
Critical Concepts
To use this pattern effectively, you must understand two specific rules about Loom's data flow.
1. Scope is "Public" (Self + Children)
When you use the scope function, the changes you make to ctx are visible to:
- The Component Itself: You can access the new values immediately in your
drawormeasurefunctions. - All Descendants: Every child, grandchild, and great-grandchild will see these values.
This is why we say it behaves like CSS. If you set a property, it propagates down until something else overrides it.
2. Measure is "Private" (Local Only)
In contrast, if you calculate something inside the measure function and return it as part of the view tuple, that data is private.
- It is visible to your
drawfunction. - It is NOT passed down to children.
Rule of Thumb:
- Use
scopefor Shared State (Themes, Config, Chapter Numbers). - Use
measurefor Local Logic (Geometry, layout calculations).
Best Practices
Always Sanitize with Scope
A common mistake is assuming a key exists because "usually a parent sets it."
If a user pastes your component into an empty file, ctx.my-key might be missing, causing a crash.
By using the scope: (ctx) => mutator.batch(ctx, mutator.ensure(key, default)) pattern shown above, you guarantee that the key always exists with at least a default value, making your components crash-proof.