Skip to content

Code that midcode can edit

How to write markup, classes, components and content, by hand or with an agent, so every part of it stays editable on midcode's canvas.

View as Markdown

midcode edits a site by changing the characters where a thing is written: the words between two tags, one class in a className, one value in an array. So what’s written as plain text in the code can be changed on the canvas, and what’s computed when the page renders can’t.

This page is for whoever writes that code, a person or an AI agent. Each rule gives the form midcode can edit, the form it can’t, and why. None of it is needed for a site to open in midcode: it decides how much of the site you can change by clicking. The examples are React; other languages are further down.

Where an element is written

While your dev server runs under midcode, every element knows where its opening tag is written: src/components/Hero.tsx:14:7 is the file, the line and the column. Every edit is made there, and it’s the reference midcode gives an agent.

  • Write JSX in .tsx or .jsx files. In Next.js and Vite projects those are the files midcode marks.

  • Elements from a package, or from HTML put in as a string (dangerouslySetInnerHTML, rendered Markdown), aren’t written in your code: they show on the canvas and can’t be edited there.

  • Never write data-mc, data-mci or data-mcu yourself. midcode adds them while the dev server runs, and one written by hand points at the wrong place.

Text

TSX
// Do: the words are written, in the markup or in data
<h1>Built to outlast</h1>
<h3>{project.title}</h3>   {/* title: 'Oak House', in content/projects.ts */}

// Don't: the words are put together when the page renders
<h1>{`Built to ${verb}`}</h1>
<p>{count} projects</p>
<h3>{project.title.toUpperCase()}</h3>

Double-clicking a text edits it in place when everything inside the element is literal text. Otherwise midcode looks through the project for the exact text as one whole string and edits it there: an array in a data file, a prop where the component is used, a JSON catalog. A text assembled from pieces never exists as one string, so it’s never found.

  • If the same string is written in several places, midcode takes the one in the element’s file or in a file it imports. If it still can’t tell, it lists the places and changes nothing.

  • For capitals use a class (uppercase), not .toUpperCase().

  • Keep a text in one element. In <p>Made <em>to order</em></p> only the <em> can be edited in place.

More in Text, images and video.

Classes

TSX
// Do: class names written out, in the attribute
<section className="px-6 py-16">
<div className={cn('rounded-2xl p-6', featured && 'ring-1', className)}>

// Don't: classes kept somewhere else, or built
const box = 'rounded-2xl p-6'
<div className={box}>
<div className={`p-${size}`}>
<div className={button({ size })}>   {/* cva */}

The style panel swaps one class for another inside the text of className, so each diff is one word. It reads plain strings, template literals, arrays, +, and the arguments of cn(), clsx(), classnames(), twMerge() and cx(). A class held in a constant, glued to an expression (p-${size}) or returned by a function isn’t written there: midcode says it comes from a prop or a condition, and leaves it.

  • When className is only a variable (className={styles.card}), there’s nowhere to add a class. Wrap it, className={cn(styles.card)}, and midcode adds its classes as a first argument.

  • A class inside a condition (featured && '…', a ternary, a clsx({ … }) key) can be taken out, but midcode never writes into a branch: the new class goes in the unconditional text. Put what a person will tune there.

More in Tailwind CSS.

Breakpoints

TSX
// Do: mobile first, with min-width screens
<h1 className="text-4xl md:text-6xl lg:text-8xl">

// Don't: max-width screens, or screens with a name of your own
<h1 className="text-8xl max-lg:text-6xl max-md:text-4xl">

Each breakpoint on the canvas writes at one min-width screen, and the smallest writes with no prefix. midcode reads sm:, md:, lg:, xl:, 2xl:, min-[810px]: and min-[50rem]:. A class with any other prefix (max-md:, dark:, [&>svg]:, your own screen name, two states at once) is kept exactly as written and never read. The panel doesn’t show or change it, so it can silently win over what the panel writes. A project’s breakpoints, and the screen each one writes at, are in .midcode/breakpoints.json. Without that file they are the defaults: Phone with no prefix, Tablet at md:, Desktop at lg:. More in The canvas and breakpoints.

Projects without Tailwind 4

TSX
// Do: midcode's utilities, with mid: first
<h1 className="title mid:text-[40px] mid:md:text-[80px] mid:hover:text-[#ff5b2e]">

// Don't: bare utilities, the prefix in the wrong place, a built name
<h1 className="title text-[40px] md:mid:text-[80px]">
<div className={`mid:p-${n}`}>

A project is a Tailwind 4 project when one of its stylesheets has @import "tailwindcss" or an @theme block: write Tailwind’s classes there. In any other project (plain CSS, CSS modules, Bootstrap, Tailwind 3) the utilities are Tailwind 4’s with mid: in front, and midcode reads only those: the site’s own classes are left alone, even one called flex.

Their CSS is midcode.css, rebuilt from the mid: classes that appear whole in the code: by the app while the project is open in it, and anywhere else by the midcode package (npx midcode, once it’s installed). Never edit midcode.css by hand, and put tokens in .midcode/theme.css (Theme and design tokens). See Without Tailwind and The midcode package.

Images and attributes

TSX
// Do: the value is one whole string, here or in data, or an imported file
<img src="/images/oak.jpg" alt="Oak House" />
<img src={studio.photo} alt={studio.name} />

// Don't: the value is computed
<img src={`/images/${slug}.jpg`} alt={alt(slug)} />

Replacing an image, or changing an alt or a link’s href, rewrites the attribute where its value is written as one string: on the element, on the component’s use, or in the data. In a Next.js project, an image you import is replaced in place, by a file of the same type. In other projects, reference the image by its path in the public folder.

Components

TSX
// Do: one root element, and the rest of the props passed on to it
export function Button({ className, children, ...rest }: ButtonProps) {
  return <button className={cn('rounded-full px-5 py-2', className)} {...rest}>{children}</button>
}

// Don't: nothing passed on, or no element of its own
export function Button({ children }: ButtonProps) {
  return <><button className="rounded-full px-5 py-2">{children}</button></>
}

With {...rest} on the root, each button on the page carries where its <Button> is written, so the label and the classes given at that use (<Button className="mt-8">Buy</Button>) are edited there. Without it, a text passed as children can’t be found. Variants need a function component that returns an element of its own, not a fragment.

Props

TSX
// Do: a type, literal defaults, hints where the type isn't enough
type TickerProps = {
  /** @label Speed @min 10 @max 120 @step 5 @unit s */
  speed?: number
  align?: 'left' | 'center'
  /** @control color */
  tint?: string
  heroImage?: string
}
export function Ticker({ speed = 40, align = 'left', tint = '#111111', heroImage }: TickerProps) {

// Don't: nothing to read
export function Ticker(props: any) {

Props show in the right panel as controls: a switch for boolean, a number field for number (a slider with @min and @max), a choice for a union of strings, a text field for string. A string becomes an image, color or link control by the last word of its name (heroImage, textColor, ctaHref) or with @control image|color|link|textarea. Defaults are read from the destructuring when they’re literals. A ReactNode or a function shows as code. Without types, midcode reads the destructuring, its defaults and a JSDoc @type {'a' | 'b'}.

Where the component is used, write values midcode can write back: title="Oak", speed={60}, featured. An expression (speed={fast ? 20 : 60}, {...config}) shows as code. A prop made on the canvas is a variable.

Variants and states

TSX
// Do: a variant prop, data-variant and a named group on the root
type CardProps = { title: string; variant?: 'primary' | 'ghost' }

export function Card({ title, variant = 'primary', ...rest }: CardProps) {
  return (
    <article data-variant={variant} className="group/card rounded-2xl bg-white p-6 hover:shadow-lg data-[variant=ghost]:bg-transparent" {...rest}>
      <h3 className="text-lg group-hover/card:underline group-data-[variant=ghost]/card:text-neutral-500">{title}</h3>
    </article>
  )
}

// Don't: variants decided in code
<article className={variant === 'ghost' ? 'bg-transparent' : 'bg-white'}>
<article className={card({ variant })}>   {/* cva */}

midcode writes variants this way itself, and needs them this way to show a component as a grid of variants and states:

  • The prop is called variant, typed as a union of kebab-case names, with a default.

  • The root has data-variant={variant} and group/<name>, the component’s name in kebab-case (ProductCard is group/product-card).

  • On the root, a variant’s classes start with data-[variant=x]: and a state’s with hover:, active: or focus-visible:. Inside it, group-data-[variant=x]/<name>: and group-hover/<name>:, group-active/<name>:, group-focus-visible/<name>:. The default variant has no prefix.

Variants written with cva, or with conditions on the prop, are recognized and left alone: the canvas says they’re written in code. Use focus-visible:, not focus:, which midcode doesn’t read as a state. In a project without Tailwind 4 all of it carries the prefix: mid:group/card, mid:data-[variant=ghost]:bg-transparent. More in Components.

Lists of content

TypeScript
// Do: an array of plain objects, at the top of a module
export const projects = [
  { title: 'Oak House', year: 2024, cover: '/images/oak.jpg', featured: true },
  { title: 'Pine Studio', year: 2023, cover: '/images/pine.jpg', featured: false },
]

// Don't: a list that's assembled
export const projects = [...houses, ...studios].map(toProject)
export const site = { projects: [/* … */] }

The CMS shows as a collection any array whose items are all object literals, declared at the top level of a .ts, .tsx, .js, .jsx or .mjs file (exported or not, or the default export), a JSON file that is an array of objects, and a folder of .md or .mdx files with front matter. A value is editable when it’s a string, a number, a boolean, null or a list of strings; anything else (an import, JSX, a call) shows as code. An object of plain values such as { en: 'Free', es: 'Gratis' } is one field per key.

Languages

TypeScript
// Do: one catalog per language, or a text per language under language codes
// messages/en.json, messages/es.json
const price = { en: 'Free', es: 'Gratis' }
export const locales = ['en', 'es']
export type Lang = 'en' | 'es'

// Don't: languages decided in code
const price = lang === 'es' ? 'Gratis' : 'Free'
const price = { english: 'Free', spanish: 'Gratis' }

Languages finds JSON catalogs named by language (messages/es.json, locales/es/common.json, in a folder called messages, locales, lang, i18n, translations or dictionaries), objects whose keys are all language codes, the Lang type and the locales list. A single { en, es } object in a project with no catalogs isn’t taken as a setup.

What midcode leaves in the project

.midcode/comments.json is the list of comments left on the canvas, readable without midcode. anchor.ref.mc is where the element is written, anchor.ref.mci the component use that drew it, if any. resolvedAt is null while a comment is open, and attachments are paths inside .midcode/attachments/.

.midcode/comments.json
[
  {
    "id": "3f9a1c2e",
    "createdAt": 1759676400000,
    "resolvedAt": null,
    "page": "/",
    "breakpoint": "desktop",
    "viewportWidth": 1440,
    "anchor": {
      "ref": { "mc": "src/components/Hero.tsx:14:7", "mci": null, "i": 0, "sel": "body > main > section > h1", "mcu": null },
      "tag": "h1",
      "component": null,
      "text": "Built to outlast",
      "context": ["src/app/page.tsx:9:5"],
      "fx": 0.5,
      "fy": 0.5
    },
    "body": "Make this one line on desktop",
    "attachments": []
  }
]

.midcode/README.md says what the folder holds and, in a project without Tailwind 4, how its styles are written. app/midcode-scratch/ is midcode’s free canvas: leave it as it is. The rest is in What midcode adds to your project.

Other languages

In .svelte files the same rules hold with Svelte’s syntax: text and class="…" written out are editable, {expression} and class={…} aren’t, and props come from $props(). A Svelte component gets variants and states the same way as a React one (that code is shared, but no Svelte project is on record as tried with them); variables are for React components only.

In the next release the same goes for Vue, Astro, plain HTML and the templates a server renders, where whatever the language computes ({{ title }}) can’t be edited in place. For a component’s props to show as controls, declare them: defineProps in a .vue file, interface Props in the frontmatter of an .astro file. Vue and Astro components get variants and states too, written through that same declaration (a variant member with its default, data-variant and group/<name> on each root); variables are for React components only.

Also in the next release, where pages are built from Markdown (Hugo, Jekyll, Eleventy), a heading, a paragraph or a one-line list item is edited on the canvas when it’s only words. One with emphasis, code, a link or a shortcode in it is changed in the file. Markdown content takes no classes or attributes: style it from the template around it.

Limits

  • An element moves only within its file and its function: an item drawn by a .map() stays in its list.

  • midcode finds a text by its exact words, at least two characters long. It doesn’t follow a value through functions.

  • The style panel reads the classes it has controls for (size, spacing, layout, type, fill, border, radius, shadow). Any other class stays as written.