Components
Component mode puts a component alone on the canvas with its variants and states. The exact code midcode writes for each, typed props, and element settings.
A component is edited in one place and shows everywhere it’s used. midcode opens it on a canvas of its own, one step deeper than the page: its variants side by side, its states underneath, and every edit written into the component’s own file, never into the page that uses it.
This page covers that canvas, the code behind variants and states, the Props section you see when you select an instance on a page, and the settings of form fields, embeds and players.
Open a component
Any of these opens it:
Double-click an instance on the page (on the instance itself, not on a text inside it).
Right-click an instance → “Edit component”.
Select an instance and click the edit button in the right panel’s header (“Edit Card”).
Click the component in the Assets tab.
The bar over the canvas says where you are: the page, then the component, then its file. To go back, click the arrow (“Back to the page”) or the page’s name, or press Esc once nothing is selected.
The component has to be on a page: every cell of the canvas is the real page that renders it, cropped to the component. If nothing uses it yet, midcode says “Card isn’t on any page yet”: drop it on a page from Assets first.
Component mode works with React components and Svelte components. A React component has to be in a file midcode marks: .tsx and .jsx in Next.js and Vite projects, not a plain .js file. For Svelte components the code is there, and what it writes was checked by running it on its own, but no Svelte project is on record as tried with component mode in the running app. From the next release, Vue and Astro components open on their own canvas too: see Vue and Astro.
The grid
Columns are variants, rows are states.
| Primary | Ghost | |
|---|---|---|
| Default | The component as it is | What Ghost changes |
| Hover | What changes on hover | Ghost, hovered |
Click an element in a cell and the right panel edits it for that cell. The rows “Breakpoint”, “Variant” and “State” at the top of the panel say which, and can change it.
What a cell doesn’t set, it takes from its parent: Pressed from Hover, a state from Default, a variant from Primary. Those values show dimmed. A value the cell sets itself shows its label in blue; right-click it → “Remove override” to inherit again.
With nothing selected (or the component’s root selected), the right panel’s “Component” section lists the variants and states. Click one to edit there.
The bar also has the breakpoint the component is shown at, “Variables” (see Variables), “Send to agent” (the component, its props, variants, states and where it’s used, pasted into your agent) and a button that shows the component’s file beside the canvas (see Code view).
Variants
A variant is a version of the component: Ghost, Large, Dark. It starts as a copy of the primary and keeps only what you change in it.
Select the component’s root. A dashed box appears to the right of the last column.
Click it (“New variant”), or the plus beside “Variants” in the right panel.
Type a name. “Ghost dark” is written
ghost-dark.
To use a variant on a page, select an instance and pick it under Props → “Variant”. To rename or delete one, right-click its column header or its row in the Component section.
Rename changes the name in the type, in every class that carries it, and in every instance that picks it.
Delete asks first. Its classes come out of the file, and the instances that used it go back to the primary.
The primary can’t be renamed or deleted: it’s the component as it is.
States
A state is how the component looks while it’s hovered, pressed or focused.
Select the root. A dashed box appears under the last row.
Click it (“New state”), or the plus beside “States”, and choose Hover, Pressed or Focus.
A new row writes nothing. It shows what it inherits until you change something in it; that first change writes the first class. To take a state away, right-click its header → “Remove state”. A state that already has classes in the file can’t be dropped from the canvas: remove them in the code.
Props
Select an instance of a component on a page and the right panel has a “Props” section: one control for each prop the component declares, typed from its code. Nothing has to be registered. midcode reads the props type, the defaults in the destructuring, and JSDoc comments.
| The prop’s type | Control |
|---|---|
string | A text field |
number | A number field; a slider when it has @min and @max |
boolean | A switch |
A union of strings ('sm' | 'md' | 'lg') | A choice |
string, named like an image (image, logo, avatar, heroImage) | A path with a file picker |
string, named like a color (color, background, fill) | A color picker |
string, named like a link (href, url, ctaHref) | A link field that suggests your pages |
ReactNode, a function, an object | Shown as code; a click opens it in the editor |
The last word of the name decides: imageAlt and linkLabel stay text.
JSDoc tags on a prop tune its control:
| Tag | Does |
|---|---|
| First line of the comment | The description, shown on hover |
@label Plan name | The label, instead of the prop’s name in words |
@min 0, @max 500, @step 5 | The range and step of a number |
@unit px | The unit shown beside a number |
@control color | Forces the control: color, image, link, text, textarea, number or boolean |
@default 24 | The default, when it isn’t in the destructuring |
interface PricingCardProps {
/** @label Plan name */
name: string
/**
* Monthly price
* @unit $
* @min 0
* @max 500
* @step 5
*/
price?: number
/** Shown as the recommended plan */
featured?: boolean
size?: 'sm' | 'md' | 'lg'
/** @control color */
accent?: string
/** @control textarea */
summary?: string
ctaHref?: string
}
export function PricingCard({ name, price = 24, featured = false, size = 'md', accent = '#ff5b2e', summary, ctaHref = '/signup' }: PricingCardProps) {That gives “Plan name” as a text field, “Price” as a slider from 0 to 500 in steps of 5, “Featured” as a switch, “Size” as three buttons, “Accent” as a color, “Summary” as a text area and “Cta href” as a link.
In a JavaScript project with no types, midcode goes by each prop’s default and name, and a /** @type {'sm' | 'md'} */ comment on a destructured prop makes it a choice. Code that midcode can edit has the do’s and don’ts for you or your agent.
A prop whose value is written as code on that instance (price={plan.price}) shows as code, and a click opens it. A label in full color means the prop is written on this instance; right-click it → “Reset to default” removes it.
In component mode the same section is a read-only list of what the component declares: each instance sets its own where it’s used.
Svelte
Props work the same for Svelte components, read from let { … } = $props() or Svelte 4’s export let.
Vue and AstroNext release
The next release recognizes Vue and Astro components where they’re used: Layers names them, a click on one selects the instance, and the Props section shows the same controls, read from defineProps (with a type, an object or an array) in a Vue component and from interface Props in an Astro component. A value that’s code is written the way the file binds it: :count="3" in Vue, count={3} in Astro.
They have component mode too. A .vue file is a component, and so is an .astro file outside your pages and layouts. The Assets tab lists them (pages, layouts and App.vue aside; Nuxt names them with their folders, so components/base/Button.vue is BaseButton), and each opens on its own canvas with the same grid of variants and states.
A state is the same classes as anywhere (hover:, active:, focus-visible:). A variant is a variant prop, declared the way the component already declares its props, with data-variant and a group/<name> class on each root:
| Component | The variant is written as |
|---|---|
Vue, defineProps<Props>() | A variant?: 'primary' | 'ghost' member in the type, its default in withDefaults(…) or in the destructuring, and :data-variant="variant" on each root of the template |
Vue, defineProps({ … }) | variant: { type: String, default: 'primary' }, with the list of variants in a JSDoc @type above it |
| Astro | A variant?: 'primary' | 'ghost' member in interface Props, its default in const { variant = 'primary' } = Astro.props, and data-variant={variant} |
A component with no props yet gets the declaration it needs. Renaming or removing a variant follows the instances that set it. Vue, Nuxt and Astro have the details and what isn’t written (a Vue component on the Options API, one that lists its props as defineProps([...])).
Variables are for React components and, from the next release, Svelte, Vue and Astro components.
Element settings
Form fields, embeds and players get a section of their own in the right panel, above the style groups. Each control writes one attribute on the element.
| Element | Section | What you set |
|---|---|---|
<input> | Input | Type, Name, Placeholder, Value, Min, Max, Step, Required (which ones show depends on the type) |
<textarea> | Text area | Name, Placeholder, Rows, Required |
<select> | Dropdown | Name, Required, and its options: add, remove, drag to reorder, with a label and a value each |
<form> | Form | Action, Method, and “Add field” |
<button> | Button | Type (Button, Submit, Reset), Disabled |
<label> | Label | For: the id of the field it names |
<iframe> | Embed | Link, Title, Full screen |
<video> | Playback | Controls, Autoplay, Loop, Muted, Plays inline |
<audio> | Audio | The file (“Replace…”), Controls, Autoplay, Loop, Muted |
“Add field” puts a labeled field (Text, Email, Phone, Message, Dropdown, Checkbox) before the form’s last button, with an id and a name no other field has, or adds a Submit button. An embed’s Link takes whatever you’d paste: a share link, an embed code, a place name (see Insert). A select’s options can only be edited when they’re plain <option> elements, not a list rendered from code.
Save a form to a tableNext release
In the next release the Form section also has “Save to a table…”, which writes a brief for your agent to store what people send in your database.
What midcode writes
Variants and states are plain Tailwind, with no JavaScript: a typed prop, a data attribute, and class prefixes. Start from this component:
interface CardProps {
title: string
}
export function Card({ title }: CardProps) {
return (
<article className="rounded-2xl p-6 shadow-lg">
<h3 className="text-xl font-semibold">{title}</h3>
</article>
)
}The first variant
Adding “Ghost” sets the component up, in one edit: the variant prop with its list and its default, data-variant on the root, and a named group so the elements inside can follow.
interface CardProps { title: string+ variant?: 'primary' | 'ghost' } -export function Card({ title }: CardProps) {+export function Card({ title, variant = 'primary' }: CardProps) { return (- <article className="rounded-2xl p-6 shadow-lg">+ <article className="rounded-2xl p-6 shadow-lg group/card" data-variant={variant}> <h3 className="text-xl font-semibold">{title}</h3> </article> ) }The group is named after the component (ProductCard gets group/product-card). A component that returns more than one element gets data-variant and the group on each. The next variant only adds its name to the list: 'primary' | 'ghost' | 'outline'.
Without TypeScript, the list lives in a JSDoc comment that the Props section reads:
export function Card({ title, /** @type {'primary' | 'ghost'} */ variant = 'primary' }) {A style in a variant
In the Ghost column, select the card and set Shadow to None and Border to 1. Then select the title and set its weight to 400.
- <article className="rounded-2xl p-6 shadow-lg group/card" data-variant={variant}>- <h3 className="text-xl font-semibold">{title}</h3>+ <article className="rounded-2xl p-6 shadow-lg group/card data-[variant=ghost]:shadow-none data-[variant=ghost]:border" data-variant={variant}>+ <h3 className="text-xl font-semibold group-data-[variant=ghost]/card:font-normal">{title}</h3>On the root the prefix is data-[variant=ghost]:. On anything inside it, group-data-[variant=ghost]/card:. The primary has no prefix.
Shadow → None wrote shadow-none rather than removing a class: the primary’s shadow-lg would still show through. “None”, “Auto” and “Default” in a variant or a state write the reset when that’s the case.
A state
In the Hover row, give the card a larger shadow and color the title. (The lines below start from the card as its first variant left it.)
- <article className="rounded-2xl p-6 shadow-lg group/card" data-variant={variant}>- <h3 className="text-xl font-semibold">{title}</h3>+ <article className="rounded-2xl p-6 shadow-lg group/card hover:shadow-xl" data-variant={variant}>+ <h3 className="text-xl font-semibold group-hover/card:text-[#ff5b2e]">{title}</h3>| State | On the root | Inside it |
|---|---|---|
| Hover | hover: | group-hover/card: |
| Pressed | active: | group-active/card: |
| Focus | focus-visible: | group-focus-visible/card: |
If the component has no group yet (no variants), the first state style on an inner element also adds group/card to the root.
A class for a breakpoint, a variant and a state at once stacks the prefixes in that order: lg:data-[variant=ghost]:hover:shadow-lg.
A prop on an instance
Each control writes one prop where the component is used, as one edit:
-<PricingCard name="Pro" />+<PricingCard name="Pro" price={49} featured size="lg" accent="#111111" />Text is written as a string, a number in braces, a switch that’s on as the bare name and off as featured={false}. Setting a prop to its default removes it instead. Picking a variant is the same thing: <Card title="Oak" variant="ghost" />.
Svelte components
A Svelte component gets the same setup through its props:
<script lang="ts">
let { title, variant = 'primary' }: { title: string; variant?: 'primary' | 'ghost' } = $props()
</script>
<article class="rounded-2xl p-6 shadow-lg group/card" data-variant={variant}>The roots are the file’s top-level elements, and the group is named after the file. In Svelte 4 syntax the prop is export let variant = 'primary'.
Without Tailwind 4
In a project that midcode styles itself, every one of these classes carries the prefix first: mid:group/card, mid:data-[variant=ghost]:border, mid:group-hover/card:text-[#ff5b2e]. See Without Tailwind.
Attributes
Element settings write the attribute the way the file spells it: autoPlay and htmlFor in JSX, autoplay and for in Svelte. From the next release they also write in Vue, Astro and HTML files and in server templates.
When the element passes its props on (<input {...props} /> inside a component), midcode minds who should own the value. On a page, the attribute is written on the instance you selected, so one field’s placeholder doesn’t change every field. In component mode it’s written on the element in the component’s file, before {...props}: a default that each use can still override.
Limits
midcode’s variants live in one prop, called
variant. Other props with a list of values (asize) are ordinary props: they get a choice under Props, not columns on the canvas.Variants already written in code are recognized and left alone:
cva,tailwind-variants, or conditions on thevariantprop. The Component section lists them and says to edit them in the code or ask your agent.A variant can’t be added when the component is a class component, returns a fragment or no element of its own, has a root whose classes are a variable (
className={classes}), has a root that already usesdata-variantfor something else, or has another component as its root that doesn’t pass its props on ({...props}). The right panel says which, and how to fix it.Variants and states are for React and Svelte components and, from the next release, Vue and Astro components. Variables are for React components and, from the next release, for Svelte, Vue and Astro components too.
In Svelte components, variants and states have not been tried in a running project: see Open a component and Svelte and SvelteKit.
The states are Hover, Pressed and Focus. There’s no row for disabled, checked or open.
Text styles and link styles apply to every variant and state: pick them in Primary, Default.
Props whose value is an expression, and
children, are edited in the code or on the canvas, not in the Props section.A component that no page renders can’t be opened in component mode.