Context
Model runtime conditions and update derived tokens with Genome mutation.
Runtime context contains values that can change after a Genome is created.
genome.mutate({
mode: "dark",
scale: 1.25,
});Token functions receive the current context as their second argument:
const tokens = {
surface: (_dna, context) => (context.mode === "dark" ? "#121620" : "#ffffff"),
};Context type
The public context type is an open record:
interface RuntimeContext {
[key: string]: unknown;
}GenomeJS does not reserve specific context properties.
These are all application-defined:
genome.mutate({
mode: "dark",
scale: 1.25,
density: "compact",
viewport: "desktop",
containerWidth: 720,
});Initial context
A new Genome begins with an empty context:
{
}Apply initial runtime values after construction:
const genome = new Genome({
primitives: {
spacing: 16,
},
tokens: {
gap: (dna, context) =>
`${Number(dna.spacing) * Number(context.scale ?? 1)}px`,
},
});
genome.mutate({
scale: 1,
});Token functions should include sensible fallbacks because they are initially resolved before the first explicit mutation.
Number(context.scale ?? 1);Mutation is a shallow merge
Given this context:
{
mode: "light",
scale: 1,
}Calling:
genome.mutate({
mode: "dark",
});Produces:
{
mode: "dark",
scale: 1,
}Properties not included in the patch remain unchanged.
Nested objects are not deeply merged.
genome.mutate({
viewport: {
width: 1280,
height: 720,
},
});A later mutation replaces the entire viewport value:
genome.mutate({
viewport: {
width: 768,
},
});Prefer flat context properties when possible:
genome.mutate({
viewportWidth: 768,
viewportHeight: 720,
});Narrow unknown values
Because context values are unknown, token functions should check their expected type.
const tokens = {
gap: (dna, context) => {
const scale = typeof context.scale === "number" ? context.scale : 1;
return `${Number(dna.spacing) * scale}px`;
},
};For string unions:
type Mode = "light" | "dark";
function readMode(context: RuntimeContext): Mode {
return context.mode === "dark" ? "dark" : "light";
}Then use the helper:
const tokens = {
surface: (_dna, context) =>
readMode(context) === "dark" ? "#121620" : "#ffffff",
};Common context patterns
Color mode
genome.mutate({
mode: "dark",
});const tokens = {
surface: (_dna, context) => (context.mode === "dark" ? "#121620" : "#ffffff"),
};User scale
genome.mutate({
scale: 1.25,
});const tokens = {
spacing: (dna, context) => {
const scale = typeof context.scale === "number" ? context.scale : 1;
return `${Number(dna.baseSpacing) * scale}px`;
},
};Density
genome.mutate({
density: "compact",
});const tokens = {
controlHeight: (_dna, context) =>
context.density === "compact" ? "36px" : "44px",
};Container width
genome.mutate({
containerWidth: 480,
});const tokens = {
columns: (_dna, context) => {
const width =
typeof context.containerWidth === "number" ? context.containerWidth : 0;
return width >= 720 ? 3 : 1;
},
};One mutation, multiple tokens
Several tokens can respond to the same context property:
const tokens = {
gap: (dna, context) => {
const compact = context.density === "compact";
return compact
? `${Number(dna.baseSpacing) * 0.5}px`
: `${dna.baseSpacing}px`;
},
controlHeight: (_dna, context) =>
context.density === "compact" ? "36px" : "44px",
radius: (_dna, context) => (context.density === "compact" ? "8px" : "12px"),
};genome.mutate({
density: "compact",
});All derived tokens resolve again. CSS properties whose final string value did not change are not rewritten.
Context and subscriptions
Subscribers run after a mutation has been resolved and expressed:
const unsubscribe = genome.subscribe(() => {
console.log(genome.getTrait("controlHeight"));
});
genome.mutate({
density: "compact",
});Context and scoped instances
A scoped child receives the parent context plus overrides:
genome.mutate({
mode: "light",
density: "comfortable",
});
const child = genome.scope(element, {
density: "compact",
});Conceptually:
parent context:
mode = light
density = comfortable
child context:
mode = light
density = compactLater parent mutations do not automatically mutate the already-created child instance.
What can go wrong?
No fallback for initial resolution
This may produce NaN before an initial mutation:
const tokens = {
spacing: (dna, context) => Number(dna.spacing) * Number(context.scale),
};Use:
Number(context.scale ?? 1);Assuming context is strongly typed
This is unsafe:
context.scale.toFixed(1);Narrow the value first.
Expecting a deep merge
mutate() performs a shallow object merge.
Using context for stable configuration
Values that never change are usually clearer as primitives.