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:
- Visible UI elements — buttons, inputs, cards, modals, tabs, whatever is rendered. Each gets a one-word identifier.
- 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:
- The page's main CTA? →
-accent - A secondary action? →
-neutral(or omit for default) - Low emphasis? →
-ghost - Destructive? →
-danger
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:
- Label/input pairings — each field is a
field-grouppairing a<label>with its control. - Inline error messages — use the field's Helper-text / Error elements
with an
aria-describedbylink to the input. - Required markers — handled by the field, never hand-placed.
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:
- Hero region? → Hero section
- Top chrome with logo + nav? → Site-header section
- A loose box with no further meaning? → Card block, default
- Just a layout box with no UI identity? → a layout section
(Stack-layout / Grid-layout) or keep it a
<div>and use Malevich spacing tokens in your layout CSS.
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:
- Replace every hex/rgb/hsl with a semantic color token
(
--color-ink-strong,--color-surface-raised,--color-accent,--color-danger, …). - Replace pixel spacing with
var(--space-inset-*)/var(--space-gap-*). - Replace font sizing with the typography tokens. Each font token
expands into five custom properties —
--font-heading-title-family,--font-heading-title-size,--font-heading-title-weight,--font-heading-title-line-height,--font-heading-title-letter-spacing— so set all five. - Replace
border-radiusliterals withvar(--radius-*). - Replace
box-shadowliterals withvar(--shadow-*).
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:
- Visual changes — anything that looks different (rounding, spacing, color). Flag each so the human can confirm or reject.
- Structural changes — any DOM restructure (added wrapper, changed tag, replaced custom JS with a built-in component).
- Behavioral changes — anything that behaves differently (focus, keyboard, transitions).
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
- Inventory list documented
- Every UI element mapped to a Malevich component
- Token substitution complete; no raw values, no tier-1 references
- Hand-rolled overlay behavior replaced with native
<dialog>/ the runtime - Visual treatments expressed as matrix-checked
data-modifiers, not hand-rolled CSS - No custom elements; standard HTML + classes throughout
- Accessibility at least as good as the original (no regressions)
-
malevich-auditreturns zero blockers, zero majors - Diff summary produced (visual / structural / behavioral)
- Open questions for human decision listed at the end