# 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.

- Page: https://midcode.app/docs/editor/components
- From the midcode docs. Every page as Markdown: https://midcode.app/llms.txt

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](https://midcode.app/docs/editor/pages.md).

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](#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](https://midcode.app/docs/editor/variables.md)), "Send to agent" (the component, its props, variants, states and where it's used, pasted into [your agent](https://midcode.app/docs/agents/overview.md)) and a button that shows the component's file beside the canvas (see [Code view](https://midcode.app/docs/editor/code.md)).

## 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.

1. Select the component's root. A dashed box appears to the right of the last column.
2. Click it ("New variant"), or the plus beside "Variants" in the right panel.
3. 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.

1. Select the root. A dashed box appears under the last row.
2. 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 |

```tsx title="components/PricingCard.tsx"
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](https://midcode.app/docs/agents/editable-code.md) 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 Astro (next release)

The next release recognizes Vue and Astro components where they're used: [Layers](https://midcode.app/docs/editor/layers.md) 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](https://midcode.app/docs/frameworks/vue.md), [Nuxt](https://midcode.app/docs/frameworks/nuxt.md) and [Astro](https://midcode.app/docs/frameworks/astro.md) have the details and what isn't written (a Vue component on the Options API, one that lists its props as `defineProps([...])`).

[Variables](https://midcode.app/docs/editor/variables.md) 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](https://midcode.app/docs/editor/insert.md)). 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 table (next 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](https://midcode.app/docs/data/database.md).

## 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:

```tsx title="components/Card.tsx"
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.

```diff title="components/Card.tsx"
 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:

```jsx
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.

```diff
-    <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.)

```diff
-    <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:

```diff title="app/page.tsx"
-<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:

```svelte title="src/lib/Card.svelte"
<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](https://midcode.app/docs/styling/without-tailwind.md).

### 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 (a `size`) 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 the `variant` prop. 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 uses `data-variant` for 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](#open-a-component) and [Svelte and SvelteKit](https://midcode.app/docs/frameworks/svelte.md).
- 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.
