Malevich

Architecture decision record

ADR-0004 · Component delivery model

0004 — Component delivery model: CSS-first with progressive JavaScript enhancement

Status

Accepted, 2026-05-16

Context

Malevich is built for AI-agentic workflows. Designers and developers working with Claude Code, Cursor, Antigravity, and similar tools need a component model that AI agents can generate, modify, and reason about with minimal indirection.

The initial architectural assumption was Web Components — custom elements with the m- prefix (e.g. <m-button>), Shadow DOM encapsulation, and property-based APIs. This assumption persisted through Phase 1 documentation in llm-wiki/architecture.md and several references in CLAUDE.md.

Reviewing the assumption during Phase 2 architecture session revealed that Web Components add cognitive load without delivering value for this specific project:

  1. AI agents generate plain HTML and CSS more reliably than custom element invocations with property setters. A token-economy gain comes from removing the indirection.

  2. Standard HTML elements (<button>, <input>, <dialog>) carry built-in accessibility, semantics, and form integration that custom elements need to reimplement.

  3. Style isolation through Shadow DOM creates friction with our tweak system (which depends on CSS class composition) and with user override patterns (which depend on standard CSS specificity).

  4. Most modern component libraries that started Web-Component-first (Shoelace, FAST, Adobe Spectrum) have either added Light DOM modes or moved entirely to CSS-class-based architectures.

The reframing: Malevich is a CSS framework with progressive JavaScript enhancement, not a Web Components library.

Decision

Components are delivered as three layers, each functional independently:

Layer 1 — Semantic HTML (required, written by author)

Authors use standard HTML elements. The base block class is added, plus any modifiers and tweaks.

<button class="button -accent -m">Save</button>
<input class="input" type="email" />
<dialog class="dialog" id="confirm">...</dialog>

Standard HTML elements are used wherever they exist with appropriate semantics:

For higher-order compositions (FAQ sections, product cards, pricing blocks, hero sections), authors compose from semantic primitives:

<section class="block-faq">
  <h2 class="block-faq__title">Frequently asked questions</h2>
  <dl class="block-faq__list">
    <details class="faq-item">
      <summary class="faq-item__question">How does it work?</summary>
      <div class="faq-item__answer">...</div>
    </details>
    <!-- more items -->
  </dl>
</section>

The base CSS layer styles these semantic patterns. Authors don't need to know the implementation — they pick the right element for the meaning, apply the block class, and the design system handles the rest.

Layer 2 — CSS styles (required, loaded by author)

A single CSS import provides all component styles. Components are written using only semantic tokens; raw values are forbidden (enforced by @malevich/lint).

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

Style isolation uses CSS Cascade Layers, established at the package level:

@layer malevich.reset, malevich.tokens, malevich.base,
       malevich.components, malevich.tweaks, user;

@layer malevich.components {
  .button { /* component styles */ }
}

User CSS in @layer user has higher precedence by layer order, allowing overrides without !important. The reset layer applies a minimal scoped reset to component selectors only — it does not affect the host page's default styles.

Layer 3 — JavaScript enhancement (optional, opt-in)

For components that benefit from JavaScript behavior — tabs, dialogs, tooltips, dropdowns, accordions — a runtime bootstrap library attaches behavior to elements matching component selectors.

import '@malevich/components';                  // auto-init everything
import { initButtons } from '@malevich/components/button';  // selective

The runtime performs three jobs:

  1. Inject tweak slots. Components that opt into tweak layers get their DOM augmented with reserved slot elements (.button__bg, .button__glow, .button__fx, etc.) automatically. Authors write <button class="button">Save</button>, runtime expands it to the full tweak-ready structure.

  2. Attach interactive behavior. Tabs become focusable and keyboard-navigable. Dialogs become trappable. Tooltips become positioned. All progressive — without the JS layer, components still look correct, just without interactive enhancement.

  3. Wire up tweak system. When a tweak class is applied, the runtime activates the corresponding tweak module against the target's tweak slots.

JavaScript is never required for visual correctness. A page loading only the CSS gets fully-styled, semantically-correct components. The JS adds interactivity and effects.

Implications

Cascading consequences

Tweak system implementation

Tweak slots are injected by the runtime when it encounters a component with active tweaks:

<!-- Author writes -->
<button class="button -accent t-glow-hover">Save</button>

<!-- Runtime expands to -->
<button class="button -accent t-glow-hover" data-malevich-ready>
  <span class="button__bg" data-tweak-layer="background" hidden></span>
  <span class="button__glow" data-tweak-layer="glow" hidden></span>
  <span class="button__content">Save</span>
  <span class="button__fx" data-tweak-layer="effects" hidden></span>
</button>

Without runtime, the button renders correctly visually — just without the glow effect. Progressive enhancement.

Higher-order components

For composite blocks like FAQs, product cards, pricing blocks, etc., Malevich provides:

  1. Semantic patterns — recommended HTML structures using native elements
  2. Base styling — CSS rules that style these patterns by default
  3. Block-level classes — to opt into Malevich's styling

AI agents are explicitly instructed (in CLAUDE.md and component docs) to use semantic HTML patterns when composing higher-order blocks. The agent's output is then both Malevich-styled AND semantically correct for screen readers, SEO, and progressive enhancement.

This is encoded in the playbook/semantic-html.md document, which becomes part of every agent's context.

Consequences

Good

Bad

Migration impact

This decision changes what was documented as Web Components architecture in:

These will be updated in a follow-up commit that aligns documentation with this ADR.

Alternatives considered

Alternative 1: Web Components with Light DOM

Use customElements.define('m-button', ...) but render content in Light DOM (no Shadow DOM). Rejected because:

Alternative 2: Tailwind-style utility classes

Reject component classes entirely in favor of atomic utilities. Rejected because:

Alternative 3: CSS-in-JS components (e.g. Lit + emotion)

Components ship as JavaScript modules with CSS in template literals. Rejected because:

References