Collaboration
Why your green rule never fires
Two conditional style rules, one row, and the order you put them in decides the colour — plus the four places the styling engine quietly disagrees with the filters on your API.
You add two conditional style rules. amount > 100 paints the row red. amount > 1000 paints it green. Then every row over a thousand comes out red, and the green rule appears to do nothing at all.
Nothing is broken. Rules are checked in priority order and evaluation stops at the first match. A row of 5000 is over 100, so the red rule matches, and the green rule below it is never reached.
The order is the logic
Rules are evaluated in ascending priority — the order you drag them into. The first enabled rule whose conditions match decides the row's style, and the rest are not evaluated for that row.
That gives you a short checklist when a colour surprises you:
- Narrowest first. If one rule's conditions are a superset of another's and sits above it, the lower rule is dead code.
> 1000belongs above> 100, not below it. - Disabled rules are skipped entirely, so you can park a rule without deleting it and the next one down takes over.
- A rule with no conditions never matches. An empty rule is inert rather than universal, which is the safer of the two options when you are halfway through writing one.
The conditions are the ones you already know
Each rule holds one or more conditions joined by AND or OR, drawn from the same operator set as filtering: equals, not equals, the four comparisons, LIKE, IN, NOT IN, starts with, ends with. The OR is worth noting on its own — the REST API's filter syntax has no way to express it, so a rule like overdue or unassigned is something you can say here and not there.
Conditions can target any column in the view, and two column types behave better than you might expect:
Formula columns. A formula doesn't always declare a numeric type, but it often produces numbers. The evaluator checks the values at runtime as well as the declared type, so a computed line_total compares as a number rather than as text — 9 sorts below 10 instead of after it.
Relationship columns. The condition is compared against the label the cell displays, not the underlying foreign key. You write customer = Acme Ltd, which is the thing you can actually read on screen, rather than looking up that the row points at id 42. For a many-to-many cell holding several labels, LIKE %globex% matches within it.
It runs in your browser
Styles are resolved client-side against the rows already loaded — 100 per page. Nothing is added to your database, no query gets slower, and there is no index to create. A row that arrives over SSE because a colleague just created it picks up its colour as it lands.
Rules belong to the view, not to you and not to a saved preset. So everyone who opens that view sees the same rows lit up the same way, without anyone having to load anything — which is the point when the colour is what the team is coordinating on. It also means a rule you add is a rule your colleagues get, so the palette is a shared decision rather than a personal one.
The flip side of client-side evaluation is worth stating plainly. Styling is not searching. It colours what is on screen. If the row you care about is on page seven, it gets its colour when you reach page seven — so for finding things, filter; for recognising things once they are in front of you, style.
It is not quite the same engine as your API filters
The operator names match, but the styling engine is a separate implementation running in the browser, and it disagrees with the server in four places. All four are the kind of thing you notice once and then never forget:
| Filter on the API | Style rule in the grid | |
|---|---|---|
LIKE acme | matches Acme Ltd — a bare value is wrapped as %acme% | matches only acme — the pattern is anchored at both ends |
| Case | case-sensitive; Postgres gets a plain LIKE | case-insensitive, and Unicode-normalised first |
active = t | accepted — t, f, y, n, 0 and 1 all parse | silently false; only true, 1 and yes read as true |
| An unparseable value | rejected with an error you can see | no error — the condition simply never matches |
None of these is wrong on its own. Together they mean a style rule and the equivalent API filter can legitimately disagree about the same row, so it is worth writing %acme% explicitly when you mean a substring — that behaves identically on both sides.
What it doesn't do (yet)
- Rows, not cells. The resolved style is applied to the whole row. You choose which column each condition tests, but you cannot paint one cell differently from its neighbours.
- Background colour, text colour, bold, italic and strikethrough. That is the full set. There is no font size, no borders, no icons, no in-cell data bars.
- Eight of each colour. Both palettes are theme-aware, with a light and a dark value per entry, and every text colour clears WCAG AA against white, against its own pastel background, and against the dark surface. Colours are validated as six-digit hex on the way in — an arbitrary string used to slip through the API and render inline, invisible in dark mode.
- Page-scoped, as above.
One last thing, given what this feature is for. If people are relying on a glance to tell them something, colour is the one channel a meaningful share of them do not have — roughly one man in twelve cannot separate your red rule from your green one. That is exactly why bold, italic and strikethrough are in the list rather than decoration around the edges of it. Pair the colour with one of them and the row still says what it means to everybody looking at it.
Verified 23 Aug 2026: 54 unit tests written for this post against the two utilities that decide a row's colour — priority ordering and first-match-wins, disabled and empty rules, AND/OR logic, all eleven operators, null and zero handling, boolean and numeric coercion, LIKE pattern translation, and the two preset-colour class maps, including that a background hex never resolves to a text class or the reverse. The eight text colours were checked for WCAG AA contrast against white, against their own pastel background and against the dark surface; the worst pair is orange at 5.11:1. The four server-side behaviours in the comparison table were read from FilterValueCoercion and WorkspaceDatabaseService, which emit a plain case-sensitive LIKE and accept t/f/y/n for booleans..