Skip to content

Tailwind CSS

How midcode edits a Tailwind CSS 4 project: the classes it swaps, how breakpoints, states and variants are written, and which class expressions it can change.

View as Markdown

In a project that uses Tailwind CSS 4, midcode’s right panel never writes CSS. It changes the utility classes on the element you selected, the way you would by hand: one class out, one class in, in the file where the element is written. Your @theme tokens fill the controls. Nothing is added to your config or to package.json.

A Tailwind 3 project, or one with no Tailwind at all, is styled another way: see Without Tailwind.

How midcode tells which Tailwind you have

When a project opens, midcode reads its CSS files: up to six folders deep, leaving out node_modules, public, dist, build, out and folders whose name starts with a dot.

Styles are written asWhen midcode finds
Tailwind 4 classesA CSS file with @import "tailwindcss" or an @theme block. This page.
mid: classes, beside Tailwind 3No such file, and a tailwind.config.js, .ts, .mjs or .cjs at the root of the project.
mid: classesNeither.

What midcode writes

One class out, one class in. Each control of the style panel owns one property: text color, font size, horizontal padding, gap, radius. Setting it removes the class that held that property at the breakpoint you’re on and puts the new one in its place.

app/page.tsx
-<section className="px-6 py-16">+<section className="px-6 py-24">

The rest of the attribute stays as you wrote it. midcode doesn’t sort classes, reformat the line or touch a class it has no reason to change.

What the new class looks like depends on the value:

midcode writesWhen you set
p-6, gap-6, w-6A length on your spacing scale: 24 px.
p-4.5A half step: 18 px.
p-[17px], w-[413px]Any other length.
w-[50%], h-[80vh]A percentage or another unit.
text-xl, text-heroA font size from the list. text-hero is your --text-hero.
text-[18px]A font size you type. A formula keeps its shape: text-[clamp(2rem,_5vw,_4rem)].
text-brand, bg-brandA color of your theme.
text-[#ff5b2e]A color from the picker.
rounded-lgA radius on the scale: 8 px.
rounded-[10px]Any other radius.

The spacing scale is your theme’s --spacing (Tailwind’s default is 0.25rem, 4 px a step). Dragging a resize handle on the canvas writes the same classes as typing the number.

Breakpoints

Tailwind is mobile first: text-sm md:text-lg is sm everywhere and lg from 768 px up. midcode reads an element’s classes as a table, one value per breakpoint, changes the cell you edited, and writes the table back as the fewest classes that say the same.

With the default breakpoints (Desktop 1440 as the primary, Tablet 768, Phone 390), Phone writes with no prefix, Tablet with md: and Desktop with lg:. Follow one property:

  1. A vertical padding set on Desktop, the primary: py-24.

  2. On Phone, set it to 40: py-10 md:py-24.

  3. On Tablet, set it to 64: py-10 md:py-16 lg:py-24.

  4. On Tablet, right-click the property’s blue label and choose “Remove override”: py-10 md:py-24.

  5. The same on Phone: py-24 again.

The rules behind it:

  • A change on the primary breakpoint is the rule: it applies wherever nothing overrides it.

  • A change on another breakpoint is an override there. It also carries on to the breakpoints beyond it, away from the primary, that were showing the same value. Set a font size on Tablet and Phone gets it too: text-5xl becomes text-4xl lg:text-5xl.

  • Neighbours with the same value share one class.

  • Mobile-first CSS can’t make a value stop at a larger screen. So a value that exists only on a smaller breakpoint gets a reset at the next one: a background set on Phone alone is bg-black md:bg-transparent. Other resets are text-inherit, shadow-none, rounded-none and [font-size:inherit].

Which screen a breakpoint uses

A breakpoint’s screen has to start above the next smaller breakpoint’s width and no later than its own, so that an override never leaks into a neighbour. midcode picks Tailwind’s named screen that fits (sm 640, md 768, lg 1024, xl 1280, 2xl 1536) and, when none does, an exact one in rem.

Your breakpointsScreens
1440, 768, 390lg:, md:, none
1440, 1100, 810, 390xl:, lg:, md:, none
1440, 1000, 768, 390lg:, min-[62.5rem]:, md:, none

The breakpoints themselves are yours to add, rename and resize: see The canvas and breakpoints.

States and variants

In component mode a cell is also a state and a variant, and the same writes get one more prefix.

CellOn the component’s rootOn an element inside it
Hoverhover:group-hover/card:
Pressedactive:group-active/card:
Focusfocus-visible:group-focus-visible/card:
Variant ghostdata-[variant=ghost]:group-data-[variant=ghost]/card:

card is the component’s group: its root carries group/card, named after the component (PrimaryButton gives group/primary-button). A variant and a state together are written variant first, and a breakpoint goes before both: md:data-[variant=ghost]:hover:bg-black.

Three edits on a card (a background on Hover, a background for the ghost variant, a title color on Hover) leave this:

components/Card.tsx
-<article className="group/card rounded-2xl p-6 bg-white" data-variant={variant}>-  <h3 className="text-lg">{title}</h3>+<article className="group/card rounded-2xl p-6 bg-white hover:bg-neutral-100 data-[variant=ghost]:bg-transparent" data-variant={variant}>+  <h3 className="text-lg group-hover/card:text-blue-600">{title}</h3>

Choosing “none” for a property in a state or a variant writes a reset, because leaving the class out would still show the value underneath. A shadow taken off on Hover is hover:shadow-none.

On a page, outside component mode, an element’s hover: classes are kept and not edited.

Where the classes are written

midcode edits the places where class names are written out as text. In JSX that is more than a plain string:

TSX
<div className="px-6 py-16" />
<div className={cn('px-6 py-16', className)} />
<div className={`px-6 ${wide ? 'max-w-6xl' : 'max-w-3xl'}`} />
<div className={clsx('px-6', { 'opacity-50': disabled })} />
  • Plain text (a string, the string arguments of a call, the fixed parts of a template literal, the strings of an array): classes are swapped in place, and a new one goes at the end of the first plain text.

  • A helper with no plain text (cn(className, variants[size])): the new classes become its first argument. This works for cn, clsx, classnames, classNames, twMerge, twJoin, cx and tw.

  • No class attribute yet: midcode adds className="…" right after the tag name. From the next release, a file whose JSX writes class (Solid, Qwik, Preact) gets class.

  • A class inside a condition (active && 'bg-black', a branch of a ternary, a key of clsx({ … })): midcode can find it. When you change that property, it takes the class out of that branch and writes the new one in the plain text, so the value stops depending on the condition. If the condition should stay, edit it in the code.

  • A piece of a computed name (text-${size}): never matched and never changed. midcode only reads whole words.

  • A class an instance passes in (<Card className="mt-8" />): changed where the instance is written. In component mode midcode only writes in the component’s own file.

Two things stop an edit. midcode says which and writes nothing:

  • The class to replace isn’t written out anywhere: it comes from a prop, or from a lookup like SIZES[size]. The message names the class and the file, and says it comes from a prop or a condition.

  • The whole attribute is a variable (className={className}, className={styles.card}). The message is “This element’s classes come from a variable. Edit them in the code.”

In a .svelte file (and, from the next release, .vue, .astro, .html and server templates) the same goes for class="…": its written text is edited, a class glued to an expression (btn-{size}) is left alone, and class={expression} is a variable. Content written in Markdown (Hugo, Jekyll, Eleventy) has no tag to hang a class on: midcode changes its words and says to style it from the template around it.

Your theme in the controls

midcode reads every @theme block of your CSS:

In @themeWhere it shows
--color-brand“Project colors” in every color control. Written text-brand, bg-brand.
--font-displayThe Font list, with sans, serif and mono. Written font-display.
--text-heroThe Size list, with Tailwind’s xs to 9xl. Written text-hero.
--radius-cardRadius: a corner of that size is written rounded-card.
--spacingThe unit every spacing class is counted in.

A value that points at another variable (--color-brand: var(--brand)) is resolved against :root, so its swatch shows the real color. Editing the tokens themselves, and text and link styles, is in Theme and design tokens.

Limits

  • midcode models one screen, one state and one variant per class. A class with any other prefix (dark:, max-md:, [&>svg]:, a screen name of your own, two states) is kept exactly as written and is never read or rewritten. The panel doesn’t show or edit dark-mode values.

  • Screens are Tailwind’s defaults. A theme that redefines them (--breakpoint-md: 900px) isn’t read: midcode still counts md: as 768 px.

  • A ! on a class (!p-4, p-4!) is kept when the class is replaced.

  • Text styles and link styles apply to every breakpoint, variant and state. They can’t be overridden per cell.

  • The panel edits classes on elements. A rule in your CSS (.hero { padding: 4rem }, @apply) shows on the canvas but isn’t rewritten: changing that property adds a class to the element.

  • Tailwind 3 classes aren’t rewritten. midcode adds mid: classes beside them.