Malevich

AI-Things / Skills / Skill/

Malevich modifiers

Apply the modifier system correctly — the seven categories, the data-attribute attachment, the applicability matrix that declares what each component accepts, and the split between CSS-only and runtime modifiers.

modifiers · intermediate · updated 2026-05-21

What a modifier is

A modifier is a self-contained, attribute-driven extension that attaches to a component without modifying the component's source. Modifiers let you add a glow, an elevated surface, a gradient background, a sticky behavior, or a denser rhythm to a Card or Button while the component's own CSS stays untouched.

The modifier system replaces the v0.1.0 "tweak" model (ADR-0007). If you see t-glow-hover classes, data-tweak, @malevich/tweaks, or *.tweak.yml manifests, that is the retired vocabulary. The current system is seven enumerated categories attached via plain data attributes. Author against the categories, never the old t- prefix.

Variants vs modifiers — the syntactic split

Two attachment mechanisms, two meanings (ADR-0007):

The split is intentional: a reader (human or agent) scanning a template tells identity from presentation instantly.

The seven categories

Every modifier belongs to exactly one category. The category alone tells you whether it is CSS-only or runtime, whether it inherits, and whether it can conflict.

Category Concern Layer Values
background What's behind the surface CSS-only solid, gradient, image
surface How the surface sits CSS-only flat, elevated, float
effect Decorative chrome overlays CSS-only grain, glow, noise, ring, blur
shader Interactive WebGL backgrounds JS, opt-in @malevich/shaders (v2.0) webgl-*
rhythm Internal spacing density CSS, inherits compact, regular, spacious
motion Animation character CSS, inherits none, subtle, standard, expressive
behavior Runtime behaviors JS, auto-init sticky, dismissible, collapsible, copyable

effect is the renamed treatment category. behavior is the only category that requires the JS runtime; the other six are CSS-only and degrade cleanly when JS is absent (the category simply has no observable effect).

Attachment — the data-{category} attribute

A modifier attaches as a data-{category}="value" attribute on the component's host element, alongside its block class and any variant:

<article class="card -feature"
         data-background="solid"
         data-surface="elevated"
         data-effect="grain">
  <header class="card__header"><h3 class="card__title">Title</h3></header>
  <div class="card__body">…</div>
</article>

<dialog class="dialog" data-surface="float" data-behavior="dismissible">…</dialog>

CSS-only categories take effect at parse time — no JS, no build step, no class composition. A behavior modifier is wired by the runtime (see below).

The applicability matrix — check before applying

Not every component accepts every category × value combination. The single source of truth is packages/components/modifiers/applicability.json — a per-component map of which categories are available and which values within each are valid. Read the component's row before applying a modifier.

Examples from the matrix:

Applying data-effect="grain" to a Button, or data-background to a Tooltip, is a violation that malevich/modifier-applicability-check flags at CSS lint time. The same matrix feeds the docs portal's modifier table, the generated TypeScript types, the MCP server, and each component's agents.md.

Stacking is conflict-free by construction

Each category owns a disjoint CSS custom-property namespace (--{component}-surface-*, --{component}-effect-*, --{component}-background-*, …). Components compose the final value from multiple namespaces:

.card {
  box-shadow:
    var(--card-surface-shadow, none),
    var(--card-effect-shadow, none);
}

Because modifiers write into disjoint namespaces, a combination like surface=elevated + effect=glow + background=gradient needs no resolver — the cascade handles it. You can stack one value per category freely, as long as each is in the component's matrix row. You cannot stack two values of the same category (a component has one surface).

Selective inheritance

This is enforced by where each modifier writes its custom properties (section/:root scope vs. component scope). You don't manage it — just know that setting data-rhythm="compact" on a layout densifies its children, while data-effect="glow" affects only the element it's on.

Behavior modifiers and the runtime

Behavior modifiers (sticky, dismissible, collapsible, copyable) require JavaScript. The @malevich/components runtime exposes init(), which discovers and wires every data-{behavior}="true" element automatically:

import { init } from "@malevich/components";
init();

There is no per-behavior init call — the runtime registry handles discovery. Some components default a behavior on because it is core to identity: inline Code and Code-block default to data-copyable="true"; Kbd defaults to data-copyable="false" (opt-in). All other behaviors are opt-in via an explicit data-{behavior}="true".

Without the runtime, behavior modifiers simply do nothing — the component still renders correctly, it just isn't interactive.

Custom modifiers — the extension pattern

There is no plugin system. To express a one-off the built-in categories don't cover ("a 2px corporate border on this one card"), write a custom data-attribute plus CSS into the component's namespace:

<article class="card" data-corporate="enterprise">…</article>
[data-corporate="enterprise"] {
  --card-border: var(--border-width-emphasis) solid var(--color-accent);
  --card-radius: var(--radius-section);
}

Custom modifiers don't register anywhere — they just write into the component's custom-property namespace. Use real tokens, never raw values. The full contract is in /playbook/extending-modifiers.

Applying a modifier correctly

  1. Check the matrix. Is the category available on this component, and is the value in the allowed list? If not, it's a violation — pick a different approach.
  2. Use the right mechanism. Identity → variant (.-x). Presentation → modifier (data-x). Don't cross them.
  3. Mind the runtime for behavior modifiers — init() must run, or nothing happens.
  4. Respect inheritance. Reach for rhythm / motion at the section level when you want a whole region to share density or animation character; reach for the non-inheriting categories per element.
  5. Respect motion and contrast. A motion="expressive" modifier must still degrade under prefers-reduced-motion; effects must keep text legible.

Modifiers are seasoning, not structure. If the UI doesn't read correctly without the modifier, the problem is in the component or the layout, not the modifier.

Output checklist