Malevich Principles
Forty-two principles that describe how Malevich is built and why.
These are not opinions. They are decisions we've made and intend to keep. Each principle is paired with the reasoning that produced it. Where alternatives exist, we've chosen ours deliberately, and the choice is documented.
If you're an AI agent generating Malevich code: each principle constrains what you should produce. If you're a designer or developer evaluating Malevich: each principle tells you what kind of system you're adopting.
I. Naming & taxonomy
P-1.1 — Foundations is a layer of components, not tokens
Malevich uses Foundations to mean a layer of low-level components: Icon, Avatar, Typography, Spinner, Divider, Image. Tokens, by contrast, live in JSON files under tokens/ and are emitted as CSS custom properties. The two never collide because the contexts are distinct: tokens/foundations.json is data, foundations/Avatar/ is code.
This separates "what values does the system know" from "what components does the system ship." Both are foundational. Neither is the other.
P-1.2 — Components classify by functional role, not abstraction level
Layer 1 — Foundations: low-level, no slots, no logic. Layer 2 — Elements: fixed structure, three sub-roles (interactive / form / display). Layer 3 — Blocks: structural bricks for sections, typed slots. Layer 4 — Sections: page-level compositions (layout sections + ready sections). Plus Overlays: same components, rendered in portal or top-layer.
A component belongs to a layer based on what it does, not how "complex" it looks. Avatar with three sizes is a Foundation. Tabs with state management is an Element. Card with slots for arbitrary content is a Block.
P-1.3 — Variants are BEM. Modifiers are data-attributes.
Variants describe what the component is — a primary button, a danger alert, a feature card. These map to BEM short modifiers: .button.-primary, .alert.-danger, .card.-feature.
Modifiers describe how the component appears — surface, effect, rhythm, motion. These are cross-cutting concerns applied across many components, and they map to data-attributes: data-surface="elevated", data-rhythm="compact".
The syntactic distinction is intentional: it tells you which axis you're working on.
P-1.4 — Three surface tiers: flat, elevated, float
Flat: no border, no shadow, transparent or canvas. Ghost surfaces. Elevated: thin border, no shadow. Sits on canvas. Standard cards. Float: with shadow, no border. Lifts above canvas. Overlays, dropdowns.
Three is enough. Four (island vs elevated) is splitting hairs. Two (ghost vs surface) loses meaningful distinction.
P-1.5 — Status is stable, beta, soon
We don't say "experimental". Experimental sounds like "don't trust this."
We don't say "alpha". Alpha sounds like "this might be abandoned."
We say "soon". It's a promise to the user: this is coming, here's what it will look like, you can read the spec.
P-1.6 — Tokens are structured in JSON, flat in CSS
JSON nested groups give us taxonomy and Figma Tokens Studio integration: color.text.primary, space.4, radius.card.
CSS variables are flat by language constraint: --color-text-primary, --space-4, --radius-card.
Component-tier tokens stay flat in both JSON and CSS: button.background → --button-background. Variants override the same variable via cascade, not by multiplying token names.
P-1.7 — Modifiers stack on seven axes
Background — what's behind the surface (CSS, static). Surface — how the surface sits (flat, elevated, float). Effect — decorative overlays (grain, glow, ring, noise, blur). Shader — interactive WebGL backgrounds (opt-in via @malevich/shaders). Rhythm — internal breathing (compact, regular, spacious). Motion — animation character (none, subtle, standard, expressive). Behavior — runtime behavior (sticky, dismissible, collapsible).
Each component can stack modifiers from different axes. Conflicts are explicit because each axis owns its own CSS variable subset.
P-1.8 — Malevich is the brand. The architecture is generic.
The name Malevich carries Suprematist heritage, Ukrainian cultural anchor, and a particular aesthetic posture. None of these are architectural decisions — they're brand positioning.
The architecture itself — layer classification, modifier system, token tiers, presentation modes — could ship under any name. Suprematism is the wrapper, not the structure.
This separation lets the brand evolve (palette, voice, identity) without rewriting the system.
II. Elements
P-3.1 — Separation of generic and contextual elements
Some elements work everywhere with aesthetic freedom — a Button in a hero block has nothing to do with a Button in a form.
Where the context constrains the design, we ship a contextual element that owns those constraints. Form-button is not "Button with prop type=submit". It's its own component, with form-specific states (submitting, validation-error, success-confirmed) that generic Button doesn't need or know about.
This separation keeps generic Button clean and aesthetically flexible, while Form-button can integrate visually with form fields without compromising either.
P-3.2 — Native first, custom when justified
Select is <select> styled. Slider is <input type="range"> styled. Dialog is native <dialog>. Accordion is <details> styled.
Custom dropdowns, custom range controls, custom modal implementations — these are JS-heavy, ARIA-fragile, and a maintenance burden. We ship them only when the native version genuinely cannot do the job, and even then, we ship them as v1.1+ work, not v1.0 baseline.
P-3.3 — Form is its own subsystem
Forms have their own divider, their own button, their own state model. The Field-group composite isn't decoration — it's the unit of form authoring: Label + Input + HelperText + Error, together by design.
When the user is filling a form, every interaction is form-context. We honor that by giving form a coherent component family.
P-3.4 — Composite elements collapse common patterns
Some compositions appear so often that splitting them into separate components creates verbosity without benefit. Avatar-group, Tags-group, Field-group, Inline-form-group — these are not "shortcuts," they're patterns made first-class.
The test: if you find yourself writing the same wrapper markup more than three times, it's a composite waiting to be named.
P-3.5 — Display elements communicate state, not action
Badge says "this is new." Tag says "this belongs to category X." Code says "this is a literal value." Alert says "pay attention to this."
Display elements never have onClick at their core. They reflect state computed elsewhere. This semantic discipline keeps the layer pure and easy for agents to navigate.
III. Blocks
P-4.1 — One Card, many patterns
Cards differ in content, not in component. BlogCard, ProductCard, MetricCard are not separate components — they are documented patterns of how to compose the same Card with different content.
The HTML tag adapts to semantic intent: <article> for standalone content, <aside> for tangential, <div> for layout-only. The slots (header, media, body, footer) stay the same. A data-pattern attribute hints intent for AI agents and tooling without affecting style.
This avoids the god-component trap of monolithic Card while preserving the composability that makes Card useful as a primitive.
P-4.2 — Modifiers stack orthogonally
A Card can simultaneously declare its surface (flat/elevated/float), effect (grain/glow), background (solid/gradient/image), and shader (via @malevich/shaders). Each modifier owns a distinct subset of CSS variables. Conflicts are explicit because the axes don't overlap.
This is what cross-cutting really means: the modifier system isn't a collection of variants, it's an orthogonal axis system.
P-4.3 — State is its own block, not four blocks
Empty, Loading, Error, Success — same structural pattern (icon, title, description, action), four semantic intents. One block with four variants does the work of four nearly-identical blocks. The variant modifier carries the meaning the agent or designer needs.
When the structure is shared and only the intent differs, variants win over component multiplication.
P-4.4 — Charts are out of scope
Data visualization is its own domain — scales, animations, accessibility patterns, data binding — and existing libraries (Recharts, Visx, Tremor, D3) cover it well. Malevich provides design tokens so that any chart library can integrate with the system's color, motion, and typography language. We do not ship our own chart components.
A design system should be opinionated about what it ships and what it delegates.
P-4.5 — Composition over component for patterns without structure
Section headers (eyebrow + heading + paragraph + actions) and toolbars (button + button + divider + select) are patterns, not components. They compose from existing primitives without needing a wrapper class or unique structure.
The test: if the pattern can be assembled from existing components with zero new CSS, it doesn't need to be a component. Document it as a canonical pattern in /examples instead.
IV. Sections
P-5.1 — Layout sections are structure without content
Block, Split, Grid, Stack do not express what a section is "about" — they express how its content is arranged. Layout sections are slot containers. Content lives inside, layout describes the geometry.
This is what makes layout sections composable: any Hero can sit inside a Split, any Card can sit inside a Grid, any toolbar can sit inside a Stack.
P-5.2 — Ready sections are opinionated compositions
Hero, CTA, Site-header, Site-footer ship with structural decisions baked in: where the heading goes, where the actions go, what slots accept what. They are not maximally flexible — they are confident about their pattern.
The trade-off is intentional. Ready sections optimize for fast page-building over open-ended composition. When you need a Hero, you reach for Hero, not for Split + Heading + Buttons.
P-5.3 — Composition pattern beats component when structure is shared
Feature grids, testimonials, pricing tiers, FAQs — these patterns share their structure with primitives we already ship. A feature grid is Grid + feature-pattern Cards. A pricing tier is a Card with a price and an action.
We document these compositions in /examples instead of shipping them as components. When a pattern becomes ubiquitous and its variations require an opinionated default, we promote it to a ready section in a later version.
P-5.4 — Responsive ownership is layered
Layout sections own viewport breakpoints — they decide when Split becomes vertical, when Grid drops columns.
Blocks and elements own container queries — they adapt to the slot they're given, regardless of viewport.
Foundations own intrinsic scaling — clamp(), min(), max(), percentage units. They never know viewport or container; they just resize naturally.
This layered ownership means each layer's CSS is bounded by its responsibility. An agent generating a Card never writes a media query. An agent generating a Layout never writes a container query.
P-5.5 — Display text scales fluidly, body text does not
Display sizes (hero, statement, title) scale with viewport via clamp(): they grow on desktop, shrink on mobile, maintaining visual impact across breakpoints.
Body, heading, caption sizes stay fixed: 16px body reads identically on phone and desktop. Hierarchy is preserved because display always contrasts with body the same way; only display flexes.
This preserves reading comfort while letting attention-grabbing text breathe with the canvas.
P-5.6 — Site headers are configurable patterns, not multiple components
Apple's pill nav, Stripe's flat bar, Linear's gradient header — these look like different components but share the same structural skeleton (logo + nav + actions). Malevich ships one Site-header with orthogonal modifiers for shape (rectangular/rounded/pill), surface (flat/elevated/float), background (solid/blur/gradient/shader), and behavior (sticky/collapsible/fade-on-scroll).
This is the modifier system applied to its hardest case: a single component flexes into multiple aesthetic identities without duplicating structure.
V. Overlays
P-6.1 — Overlays are their own layer
Some design systems treat overlay as a presentation mode applied to existing components. Malevich keeps overlays as their own category because developers reach for "Dialog" or "Sheet" as a category, not as a modifier on something else.
The conceptual mode (inline vs overlay) is preserved in how we document and reason about components. But the practical organization — file structure, documentation hierarchy, AGENTS.md — recognizes overlays as a discoverable family.
P-6.2 — Native HTML where it earns its place
<dialog> element gives us top-layer rendering, focus trap, escape handling, and inert background — out of the box, no JavaScript required. Malevich's Dialog and Sheet both build on <dialog> rather than reimplementing what the browser already does well.
This honors a deeper principle: ship less code when the platform ships it for you.
P-6.3 — Position calculation is a solved problem
Tooltip and Popover need to compute where to render relative to a trigger — accounting for viewport boundaries, scroll containers, collision detection, and flip behavior. We use Floating UI for this because it's the de-facto standard, well-maintained, and battle-tested.
The 10KB dependency is justified. Implementing positioning logic from scratch is a many-week project with edge cases that surface for years.
P-6.4 — Focus management has sensible defaults
Modal overlays trap focus. Popovers move focus into themselves. Tooltips leave focus alone. Toasts use aria-live without stealing focus. These defaults reflect the user's mental model and accessibility best practices.
Overrides exist via data-attributes for edge cases. But the defaults work for 99% of usage — agents don't need to think about focus management to produce accessible overlays.
P-6.5 — Stand on shoulders, credit clearly
The Notifications system in Malevich is inspired by Sonner by Emil Kowalski. Same UX patterns, same intuitive API surface, same animation choices — reimplemented in Malevich's CSS-first vanilla style.
We don't pretend invention. We document inspiration. This honors the authors who solved problems we're now extending, and it tells agents where to look for canonical patterns.
VI. Modifiers system
P-7.1 — Each modifier owns a variable namespace
Surface modifier writes to --{component}-surface-*. Effect writes to --{component}-effect-*. Background to --{component}-background-*. And so on through all seven categories.
This separation lets modifiers compose without conflict. A card can have surface=float (shadow), effect=glow (extra shadow), and background=gradient simultaneously — each modifier writes to its own variables, and the component composes them into final styles.
When a new modifier is added, it claims a new namespace. It never overrides another modifier's variables.
P-7.2 — Selective inheritance based on conceptual scope
Rhythm and motion are environmental — a container's rhythm affects how its children breathe; a region's motion preference affects how its children animate. These modifiers inherit through CSS custom property cascade.
Surface, effect, background, shader, behavior are component-specific. A parent's surface doesn't dictate its children's surface. These modifiers reset at component level.
The split reflects the distinction between environment (which cascades) and identity (which doesn't).
P-7.3 — Applicability is data, not convention
Which modifiers apply to which components is declared in modifiers/applicability.json — a single source of truth. From this file we generate TypeScript types for IDE autocomplete, AGENTS.md sections for AI consumption, and validation logic for tooling.
A component author updating allowed modifiers updates one file. Everything downstream — types, docs, AI guidance — regenerates automatically.
P-7.4 — Custom modifiers are first-class citizens
Malevich ships seven modifier categories. Your team needs an eighth. That's fine — pick a data-attribute namespace, write the CSS, document it for your team. Your custom modifier composes with standard ones naturally because they all use the same data-attribute + CSS variable mechanism.
There is no plugin API to learn, no registration to perform. Extension is the same shape as the original.
P-7.5 — Shaders enhance, never required
The shader modifier activates @malevich/shaders if it's installed. Without the package, data-shader is ignored. To ensure consistent rendering, declare a fallback via another modifier (typically data-background="gradient").
This makes shaders opt-in for projects that want them, lightweight for projects that don't. The base system never depends on WebGL; WebGL extends the base.
P-7.6 — Behavior modifiers auto-init
Sticky, dismissible, collapsible — these are JavaScript behaviors that activate when @malevich/components runtime initializes. Calling init() once at app load discovers all behavior modifiers and wires them up.
No registration, no per-component initialization, no manual hookup. Modifiers declare intent in markup; runtime makes them work.
VII. Token system
P-8.1 — Three tiers, strict separation
Foundations hold raw values — colors, sizes, durations. Semantic tokens map foundations to design intent. Component tokens consume semantic for specific needs.
The rule: components read semantic. Semantic reads foundations. Foundations are private. Enforced via @malevich/lint, not just convention.
When you change a foundation value, semantic absorbs it. When you change semantic, components absorb it. The system stays predictable because each tier knows only what's directly above.
P-8.2 — Themes change semantic, never foundations
Switching themes touches one tier. Foundations remain stable across themes — brand red stays brand red whether on white or black canvas. Theme is a remapping of semantic to foundation values, not a different palette.
P-8.3 — Component tokens are flat by design
Foundations and semantic groups are nested in JSON (color.text.primary, space.4) because they have taxonomy. Component tokens are flat (button.background, card.padding) because they don't.
A card has properties, not nested categories. Cascade does the variation work; tokens name the property once.
P-8.4 — Typography names describe role, not size
Display.hero, display.statement, display.title — names that tell you when to use them. Heading.title, heading.section, heading.subsection — names that map to layout function. Body.lead, body.regular, body.support — names that describe content position.
We don't say display.xl, body.m, heading.s. Abstract sizes ask the user to memorize. Semantic roles teach.
This decouples naming from sizing: when we change a value, the name stays accurate. When the size relationship changes, the names still describe roles.
P-8.5 — Display typography scales fluidly, body does not
Display sizes (hero, statement, title) use clamp() to scale with viewport. They shrink on mobile, grow on desktop, preserving visual impact.
Heading, body, caption, code stay fixed. 16px body reads the same on phone and desktop. Hierarchy is preserved because display always contrasts with body, and only display flexes.
P-8.6 — Token naming follows predictable patterns
Foundations: category.scale.value → color.neutral.900, space.4.
Semantic: category.intent.modifier → color.text.primary, motion.duration.enter.
Component: component.property → button.background, card.padding.
An agent doesn't memorize names — it predicts them. If text colors live under semantic, color.text.* is the path. Naming carries architecture.
P-8.7 — Validation is automation, not policy
We don't say "don't reference foundations from components" as a convention. We enforce via @malevich/lint. We don't say "modifiers shouldn't conflict" as a guideline. We check at build.
Architectural rules that depend on developer discipline drift. Rules enforced by tooling survive.
Closing
Forty-two principles is a lot. They aren't meant to be memorized. They are meant to be referenceable, citable, defensible.
When someone asks "why does Malevich do it this way?", we point to a principle. When an AI agent is asked to generate Malevich code, the principles are its constraint language. When the system grows, the principles are what new components and modifiers must respect.
Principles are how a design system tells the truth about itself.