Malevich

AI-Things / Skills / Skill/

Malevich component usage

Procedural guidance for working with Malevich components — installing them, composing them, applying variants, states, sizes, and modifiers, and choosing the right component for a given UI intent.

composition · basic · updated 2026-05-21

Before doing anything

Read these first. They are short, and skipping them leads to confidently wrong code:

  1. llm-wiki/architecture.md — the three-tier token model, the five component layers (primitives / elements / blocks / sections / overlays), the seven modifier categories, and the layered runtime.
  2. llm-wiki/glossary.md — naming conventions: BEM with short modifiers, the data-{category} modifier attributes, and the stylized vocabulary (suprema, compositions, deviations, atelier) that must never appear in code.
  3. llm-wiki/playbook/semantic-html.md — the element-selection cheat sheet. Always reach for a standard HTML element before composing <div> structures.

Then read the specific *.docs.md for whichever component you are about to use. That file is the source of truth — the website and the MCP server both render from it.

The mental model — a CSS framework, not Web Components

Malevich is a CSS framework with progressive JavaScript enhancement (ADR-0004). There are no custom elements, no <m-*> tags, no Shadow DOM, and no component classes you instantiate. A component is a standard HTML element plus a BEM block class. Components arrive in three independent layers:

  1. Semantic HTML (required, you write it). Pick the right element for the meaning and add the block class: <button class="button -accent">Save</button>.
  2. CSS (required, you load it once). A single import wires up every component under @layer malevich.components.
  3. JavaScript (optional). A tiny runtime auto-initializes behavior modifiers (data-behavior) and attaches interactive concerns (focus traps, positioning, keyboard nav). Without it, components still render correctly — just without enhancement.

If you find yourself writing <m-button> or customElements.define, stop: that model was retired.

Installing the dependency

pnpm add @malevich/components @malevich/core

Import the token layer and the component styles exactly once, in the root layout or entry CSS:

@import "@malevich/core/dist/tokens.css";
@import "@malevich/components/styles.css";

@malevich/core/dist/tokens.css defines every CSS custom property on :root. @malevich/components/styles.css registers the @layer malevich.components cascade layer. Application styles that override a component go in @layer user (or any layer declared after malevich.components) — never inside the component layer.

For interactive components (Dialog, Tooltip, Tabs) and for any data-behavior modifier, load the runtime once after the DOM is ready:

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

document.addEventListener("DOMContentLoaded", () => init());

init() discovers and wires every data-{behavior}="true" element and attaches interactive concerns. It is idempotent — elements it has processed are skipped on the next pass. In a single-page app or after a View Transition swaps the DOM, re-run init() (or initWithin(root)) to enhance the new subtree.

Variants vs modifiers — two mechanisms, two meanings

This split is the heart of the system (ADR-0007). Scan any template and you can tell identity from presentation at a glance:

<article class="card -feature"
         data-surface="elevated"
         data-effect="grain">…</article>

.-feature is identity; data-surface / data-effect are presentation. Never encode presentation as a .-modifier, and never encode identity as a data- attribute.

Variants — pick by intent, not by color

Variants are named after their role, not their appearance. There is one accent per visual region — it is the primary action. neutral is a secondary action, ghost is low-emphasis, danger is destructive.

<!-- One accent per region. -->
<button class="button -accent">Save changes</button>
<button class="button -ghost">Cancel</button>

<!-- WRONG: two accents compete -->
<button class="button -accent">Save</button>
<button class="button -accent">Submit</button>

When a brief asks for "a green button," resist adding a -green modifier. Ask which role the button plays. If the answer is unclear, the visual hierarchy is unclear — fix that first. If the intent is a status meaning (success/danger/warning/info), that lives in semantic status tokens, not in an invented button color.

States — driven by attributes, not classes

States like disabled, pressed, and selected are expressed through standard HTML attributes (disabled, aria-pressed, aria-selected) or pseudo-classes the component CSS already reads (:hover, :focus-visible, :active). Never use a class modifier for state — .button.-disabled is wrong.

<button class="button -accent" disabled>Save</button>
<button class="button -ghost" aria-pressed="true">Pin</button>

There is no loading variant. For a loading button, disable it and swap the label for a spinner. The runtime mirrors keyboard activation (Space/Enter) so keyboard users get the same pressed visual as mouse users — you don't manage that yourself.

Sizes — driven by the rhythm modifier

Density is a modifier category (rhythm), not a per-component size zoo. Where a component supports it, set data-rhythm="compact" / "regular" / "spacious"; rhythm inherits via the cascade, so setting it on a section flows to its descendants. A handful of components also expose -s / -l variant sizes — check the component's docs.md and its row in the applicability matrix before assuming. There is no -xs or -xl.

If you genuinely need a size the system doesn't offer, the answer is almost always a layout decision, not a component one. Adjust the parent's spacing, or choose a different component.

Modifiers — extend without forking

A modifier attaches a cross-cutting concern to a component via a plain data-{category}="value" attribute, without touching the component's source. The seven categories:

Category Concern Layer Example values
background What's behind the surface CSS solid, gradient, image
surface How the surface sits CSS flat, elevated, float
effect Decorative chrome overlays CSS grain, glow, noise, ring, blur
shader Interactive WebGL backgrounds JS (opt-in, 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

Not every component accepts every category. The single source of truth is packages/components/modifiers/applicability.json — check the component's row before applying. malevich/modifier-applicability-check flags .button[data-effect="grain"] and similar misuse at lint time. See the malevich-modifiers skill for the full procedure.

Choosing the right component

Most "I need a custom component" requests are solvable with existing components. Walk this decision tree before reaching for a new one:

  1. A primitive with no slots and no behavior? → primitive (Typography, Avatar, Icon, Divider, Spinner, Skeleton, Dots, Image).
  2. A single interactive control or display atom? → element (Button, Input, Select, Switch, Badge, Tag, Code, Alert).
  3. A composite with internal structure but not full-width? → block (Card, Accordion, Carousel, Breadcrumb, Code-block, Quote-block, State).
  4. A full-width region of the page? → section (Hero, CTA, Site-header, Site-footer) or a layout (Block-, Split-, Grid-, Stack-layout).
  5. Something that floats above the document? → overlay (Dialog, Sheet, Tooltip, Popover, Toast, Notifications).

If none fit, check whether what you need is a modifier (a cross-cutting visual or behavioral concern on an existing component) before proposing a new component. See the malevich-modifiers skill. If a genuinely new component is warranted, switch to malevich-new-component.

Composition rules

The layer hierarchy enforces composition direction (architecture.md):

The linter detects circular composition across layers. If your structure wouldn't pass, restructure before writing.

Accessibility — defaults are correct, don't fight them

Because components are standard HTML, the right element gives you keyboard support, focus management, and form integration for free. Lean on that:

Icon-only controls must carry an aria-label. Icons render via the pluggable pack contract — <i class="icon" data-icon="check"> — and decorative icons get aria-hidden="true".

When the docs.md and the website disagree

The *.docs.md file in the component's folder is the source of truth; the website and MCP server render from it. If they disagree, the docs.md wins. Flag the discrepancy in your output so a human can resolve it.

Output checklist

Before declaring a component-usage task complete: