All work
Bulk edit · 2026

Twelve selected, twelve different values

Every product I have used lies about bulk edit. I built the state model before any screen existed, generated six interface directions against it, and scored them on a rubric. The component runs on this page.

Status Runs here, not shipped
Contents
  1. 01What products get wrong
  2. 02The model, not the screen
  3. 03Why it holds at scale
  4. 04What isn’t proven
  5. +The model in full
  6. +How it was scored
01What products get wrong

Select twelve servers and change one setting. The twelve don’t agree with each other: some are production, some staging, some development. What should the field show?

Products give one of two answers, and both are lies. A blank says no value, which is false, and on save the form submits it over values nobody looked at. The first item’s value is worse, because a filled field reads as fact. Nothing on screen says this is one of twelve answers.

A blank lies quietly. The first value lies convincingly.

The third answer is honest and nearly useless: the word Mixed. Eleven production hosts and one staging read as Mixed, and so do four of each. It tells you neither which case you are in nor how much you are about to change.

A form that misbehaves damages one record, and you notice. Bulk edit damages every selected record at once, and nothing looks broken afterwards.

Before any concept existed, I wrote down the seven ways it goes wrong:

Hard caseWhat it demands
Mixed valuesThe field must never show a value the selected items do not share.
Method per field typeThe legal verbs change with the kind of field.
No-op honestyAn operation that changes nothing has to say so.
Per-item previewYou can see what happens to each item, not only the total.
Partial failureSome succeed, some fail, and the retry must know which.
Scope honesty“All on this page” and “all matching the filter” cannot be the same click.
KeyboardA tool used many times a day cannot be mouse-only.

How the model meets each one: the model in full

02The model, not the screen

The design of this thing is the state model, because the behaviour that matters is arithmetic, not layout. A drawing can show an answer without being able to produce the answer.

So the first artifact was a pure function, not a frame. plan() takes the selected items, a field, a method and a value, and returns what would happen to each item: before, after, and whether anything changes. It existed before any interface did, and every direction since was drawn against it.

plan(hosts, tags, 'add', 'monitored') db-01 api → api, monitored will change db-02 api, monitored already matches web-03 none → monitored will change

A typical product reports all three as updated. The model reports two changes and one host that already had the tag, before anything is committed, so every interface built on it can be honest for free. The model and its tests: github.com/omkarpkh/bulk-edit-mixed

The component below draws; model.js decides. What will change, what already matches, the groups and the legal verbs all come from the model.

Live · operate it change the field, the method, the value — then switch the fidelity. Same model, same markup, both times.

Try Log retention → Decrease by → 7 and open the clamped at 1 group. Or Tags → Clear all → Apply, which is the one operation that asks you to type the word.

03Why it holds at scale

A per-row diff is reviewable while the selection is small. At two hundred rows it is a long scroll, and scrolling is not review.

At scale nobody reviews rows. They review classes of change: staging to prod is one line, dev to prod another, and hosts already on prod a third that changes nothing. Each line opens to its members.

ILLUSTRATION PER-ROW DIFF ONE LINE PER ROW A LONG SCROLL — AND SCROLLING IS NOT REVIEW GROUPED BY TRANSITION THE SAME ROWS staging → prod dev → prod already prodno change ONE LINE PER TRANSITION · EACH OPENS TO ITS MEMBERS AT MOST 3 GROUPS FOR A 3-VALUE FIELD, AT ANY SIZE
An illustration, not a count. The computed version, with the command that produced it, is on the model page.

This holds at any size, because the number of distinct transitions is bounded by the field’s cardinality, not by the size of the selection. A field with three possible values produces at most three groups, whether you selected twelve hosts or twelve thousand.

Relative numbers are the exception: every host moves from its own value, so they group by outcome instead, as moved, clamped at the floor or already at the floor. That is also the only way a clamp shows up; in a flat list of two hundred rows, nobody finds it.

04What isn’t proven

Nobody has used this: n = 0. No sessions, no production deployment: it runs on this page, not in a product. Until an operator uses it under time pressure, everything here is the design’s claim, not its result.

The tie was never broken. On the rubric, directions 1 and 3 tie. The build started from direction 1 before a correction turned its win into that tie, and nothing since has chosen between them. The scores

The scale hole was found late. Three evaluation passes rated the per-row diff fine at any size, because its layout holds. None asked what happens at two hundred rows; planning the build did, and grouping is the fix. What it cost the scoring