Malevich

AI-Things / Skills / Skill/

Malevich refactor

Convert non-Malevich UI code — vanilla HTML/CSS, Tailwind, Bootstrap, or another design system — into Malevich. A step-by-step procedure that resists the temptation to rewrite from scratch.

refactor · intermediate · updated 2026-05-21

The refactor mindset

Refactor is not rewrite. The original code had a structure and an intent. Both must survive. Your job is to express the same UI using Malevich components, with as few structural changes as the conventions allow.

When in doubt, choose the change closer to the original. A literal translation a human can read against the original is more valuable than an "improved" version that has drifted.

Step 0 — Inventory first, refactor second

Before writing a single line of new code, produce two lists:

  1. Visible UI elements — buttons, inputs, cards, modals, tabs, whatever is rendered. Each gets a one-word identifier.
  2. Original construct used for each<button>, <div class="btn">, shadcn <Button>, a Tailwind utility cluster, etc.

This list is the contract. Refactoring means producing a third column: the Malevich component that replaces each. If any element has no Malevich equivalent, stop and consult the human — don't invent one.

Step 1 — Run the audit (briefly)

Use the malevich-audit skill to identify which pieces are already conformant (rare) and which are not. The audit output becomes your refactor roadmap. Even when the code is "obviously" non-conformant, the structured list is useful for prioritization.

Step 2 — Map elements one-by-one

For each row in the inventory:

From <button> or <div class="btn">

→ Malevich Button: a standard <button> with the .button class.

<!-- before (Tailwind) -->
<button class="bg-blue-600 hover:bg-blue-700 text-white px-4 py-2 rounded">
  Save
</button>

<!-- after (Malevich) -->
<button class="button -accent">Save</button>

Decide the variant from purpose, not color:

If a color was overridden inline (e.g. a green button among blue ones), preserve the intent — that was likely a success meaning. Map it to the status semantics, and flag it for the human if no matching button treatment exists.

From <div class="card"> or any card-like container

→ Malevich Card block. Read the original and assign slots:

<!-- before -->
<div class="card">
  <h3>Title</h3>
  <p>Description.</p>
  <button class="btn">Action</button>
</div>

<!-- after -->
<article class="card">
  <header class="card__header">
    <h3 class="card__title">Title</h3>
  </header>
  <div class="card__body">
    <p>Description.</p>
  </div>
  <footer class="card__footer">
    <button class="button -accent">Action</button>
  </footer>
</article>

The header/body/footer triad is the canonical card structure. Omit slots that don't apply — don't render empty sections. Use <article> or <section> for the card root, not a bare <div>. If the original carried a visual treatment (a shadow, a textured background), express it as a modifier — data-surface="elevated", data-effect="grain" — checked against Card's row in the applicability matrix, never as hand-rolled CSS on the card.

From a <form>

→ Field-group elements composed in a <form>.

Forms are where unstructured code is most common. Watch for:

From a modal / dialog

→ Malevich Dialog overlay, built on the native <dialog> element.

Modals are the highest-risk refactor because they hand-roll focus management. Native <dialog> + showModal() gives you the focus trap, backdrop scrim, top-layer rendering, and Escape-to-close for free. Remove the hand-rolled equivalents; don't preserve them "just in case."

<dialog class="dialog" data-behavior="dismissible">
  <header class="dialog__header">
    <h2>Delete this canvas?</h2>
    <button class="dialog__close" aria-label="Close" formmethod="dialog">
      <i class="icon" data-icon="close" aria-hidden="true"></i>
    </button>
  </header>
  <div class="dialog__body">
    <p>This cannot be undone.</p>
  </div>
  <footer class="dialog__footer">
    <button class="button -neutral" formmethod="dialog">Cancel</button>
    <button class="button -danger">Delete</button>
  </footer>
</dialog>

Open it with document.querySelector("dialog").showModal(). Load the runtime (init()) so the dismissible behavior is wired (click-outside and focus restoration). Buttons with formmethod="dialog" close the dialog natively.

From a tooltip-like construct (often custom JS)

→ Malevich Tooltip overlay. Strip the hand-rolled positioning logic; the runtime positions the tooltip viewport-aware. Keep the trigger and the message; drop the bespoke position: absolute math. Tooltip accepts only surface="float", rhythm="compact", and motion — don't reach for other modifiers.

From an inline notice / banner

→ Malevich Alert — a display element (per ADR-0006, Alert lives in elements/display/, not Blocks). Alert is CSS-only by default and opts into the dismissible behavior:

<aside class="alert -warning" data-behavior="dismissible">
  <i class="alert__icon icon" data-icon="warning" aria-hidden="true"></i>
  <div>
    <p class="alert__title">Heads up</p>
    <p class="alert__body">Your trial ends in 3 days.</p>
  </div>
</aside>

Status is a variant (-info / -success / -warning / -danger). Dismissibility is the data-behavior="dismissible" modifier — the runtime wires the close affordance; without the runtime the alert simply isn't dismissible. Use role="alert" only for urgent messages that must be announced immediately; otherwise <aside>. The status meaning must be in the text, never carried by color alone; icons are decorative (aria-hidden).

Icons

→ the pluggable icon-pack contract: <i class="icon" data-icon="name"> (ADR-0011). The icon renders via CSS mask-image with currentColor, served from the registered pack (@malevich/icons-default by default). Replace <i class="fa-…">, icon fonts, and bespoke icon components with the .icon element carrying a canonical data-icon name (and optional data-icon-weight). Decorative icons get aria-hidden="true"; meaningful icons get an accessible label. Reference canonical names by intent (delete, close, chevron-down), not by glyph.

Tailwind utility clusters with no semantic anchor

<div class="flex gap-4 items-center bg-white p-6 rounded-lg shadow"> has no semantic identity. Reason about its role on the page:

Not every <div> becomes a component. Layout containers stay layout containers — they just consume var(--space-*) instead of raw pixels.

Step 3 — Token substitution sweep

After structural mapping, sweep the CSS once more for raw values:

Never reach for a tier-1 foundation directly (var(--palette-*), var(--scale-*)) — that fails malevich/no-foundation-direct-reference. If a value has no Malevich equivalent, do not invent a new token mid-refactor. Use the closest existing semantic token and flag the gap for the human.

Step 4 — Re-run audit

Before declaring done, run malevich-audit against your output. Aim for zero blockers, zero majors. Minors are acceptable but should be listed.

Step 5 — Diff against original

Produce a concise change summary in three lists:

A good refactor has few entries in each list. A long list means you've drifted into rewrite territory.

Common pitfalls

"I'll just use Malevich for the new parts."

Mixing systems in one file produces hybrids that age badly. If the file is being touched, refactor the whole file or none of it.

Renaming identifier classes mid-refactor.

Tempting, but it widens the diff and complicates review. Keep identifiers (.user-card, .checkout-form); only change the visual classes (.btn.button).

Porting Tailwind one-to-one.

Tailwind utility soup looks close to Malevich because both use utility-ish syntax, but Malevich is BEM with semantic tokens, not utility classes. Don't rename bg-accent text-on-accent rounded-md px-4 py-2 into a class list — that's Tailwind with new names, not Malevich.

Reaching for a custom element.

If you catch yourself writing <m-button> or customElements.define, stop. Malevich has no custom elements (ADR-0004). The answer is a standard element plus a class.

Inventing new variants.

If your refactor would create .button.-custom-1, stop. The need for a custom variant almost always means either (a) an existing variant covers this under a different name, or (b) the original expressed a cross-cutting concern that belongs in a data-{category} modifier, or (c) it expressed something the system doesn't yet support — a decision for the human.

Output checklist