Build two-layer grouping for a busy inbox or task list

Level
intermediate
Time to complete
90 min
Prerequisites
4

What you will buildA second, user-created grouping layer over your fixed categories: born from empty, bulk-assigned across the whole list, shown as a thin left colour-bar. About 90 minutes.

What You Will Build

You will add a second grouping layer to an app where items already sit in a fixed set of categories - an inbox, a task list, a pipeline. Call the new layer areas. Unlike the fixed categories, an area is user-created, cross-cutting (one item can be in several), born from an empty state, assignable to a whole existing list in a few clicks, and shown on each row as a thin colour-bar on the left that never eats row width.

The target is a busy operator who can open the feature with nothing in it, create a handful of areas, tag their entire backlog in minutes, and then scan the list by colour. This guide is the data model and the four interaction surfaces that get them there, drawn from a study of thirteen contact and project tools - and honest about the one part no tool ships, which you will have to design yourself.

The mental model: your fixed categories give each item one home; the area layer sits on top and cuts across them so one item can belong to several.

Prerequisites

  • An app where items already live in a fixed top-level taxonomy (folders, statuses, categories) and a list or pipeline view you control.
  • A data store you can add a table or a multi-value field to, and a UI layer you can edit.
  • A fixed, colourblind-safe colour palette chosen up front - 8 to 12 swatches, visibly distinct from any colours your app already uses.
  • A list view that renders a per-row indicator (you will add a thin left bar to it).

Tip

Choose the palette before you write any code, and make it visibly different from the colours your app already uses for categories or statuses. A free hex picker feels flexible and produces mud - low-contrast, clashing, indistinguishable swatches with no accessibility fallback. A small curated palette is the feature working; a hex picker is the feature failing slowly.

Mental Model

Two layers, and the whole design turns on keeping them straight.

The fixed taxonomy says what an item is - it gives each item exactly one home. The area layer says what an item is for - and an item can be in several at once. The single most important decision is where that membership lives. Store it on the record (a persistent property of the item itself) and it survives every filter, sort, and view change. Store it as a view-level "group by" and the user loses it the moment they change the view, because it was never on the records at all. These two feel identical on screen and behave nothing alike. You want membership on the record.

There is a sharper way to say it: an area is a Group, not a Tag. A Group is a static, hand-curated list - the members are exactly who you put in it and nobody else. A Tag is a fuzzy keyword match. Build the Group. Resist making areas "smart" or self-filling in version one; explicit membership is the thing an operator can trust and make decisions from.

Steps

Work in order. Steps 1 and 2 set the model and the palette; the rest build the four surfaces - birth, bulk assign, single edit, and the colour-bar.

Model membership as a tag on the record

Create a many-to-many relationship between items and areas - a join table, or a multi-value field on the item - so that membership is stored on the data, not on a view. This is the choice that makes the layer survive filters.

create table area (
  id          uuid primary key,
  name        text not null,
  color       text not null       -- a value from your fixed palette, not free hex
);

create table item_area (             -- the membership: many items <-> many areas
  item_id     uuid references item(id),
  area_id     uuid references area(id),
  primary_area boolean default false,  -- which area drives the row's colour-bar
  primary key (item_id, area_id)
);

You know this worked when you can add the same item to two areas and both memberships persist after you change the list's filter or sort.

Decide and store the primary-area rule now

Because an item can be in several areas but a row has one colour-bar, you must decide what the bar shows before you build it. The simplest durable rule: one membership per item is flagged primary, and the primary drives the bar. Store it (the primary_area flag above), default the first area an item joins to primary, and let the user change it.

-- the area whose colour the row's left bar will use
select a.color from area a
join item_area ia on ia.area_id = a.id
where ia.item_id = $1 and ia.primary_area = true;

You know this worked when every item that is in at least one area resolves to exactly one bar colour, and an item in zero areas resolves to none.

Sources

  1. Attio Lists: a grouping of records without altering the original; one record, many listsattio.com
  2. HubSpot bulk edit: Select all N records, add to static segment (100 per batch)knowledge.hubspot.com
  3. Linear: multi-select with X, Shift-click, Cmd/Ctrl+A, then L to apply to the selectionlinear.app
  4. Airtable record-colouring renders as colour flags on the left side of the primary fieldsupport.airtable.com
  5. Trello labels: ; hides names but the colour coding remains; colourblind-friendly palettesupport.atlassian.com
  6. NN/g bulk-actions design guidelines: select-all, show status, offer undonngroup.com