Homeboard · design case study

A home runs on
what nobody sees.

A household coordinator for a family iPad and everyone’s phone — built so the app does the anticipating, the deciding and the remembering, instead of handing someone a list to manage.

Role
Strategy, design & build
Surfaces
Tablet · Phones
Stack
Next.js · TypeScript
Status
Working prototype

Product at a glance

The scope of the experience

What was designed, in six figures — the reach of the product, the ground it covers, and how much of the work was mine. Each one is counted from the thing it describes rather than written down, so none of them can quietly stop being true.

26

End-to-end journeys

Complete paths a household actually takes — not screens, but whole errands, each one addressable by URL and playable in the embed below.

6

Phases owned end to end

The same person framed the problem, decided what it refuses to do, drew it, built the system and shipped the running thing above.

Discovery → Strategy → UX → Visual design → Prototype → Implementation

2

Surfaces, designed apart

A shared tablet on the kitchen wall and a private phone. Two applications answering two different questions, not one layout at two widths.

12

Core product modules

From the day's plan through access control, upkeep and automations — each with its own screens, states, empty cases and rules.

The day · Calendar · Hand-off & cover · Fair share · The shared list · What's in the house · Upkeep · People & access · Automations · Outside calendars · Activity · Settings & quiet hours

36

Reusable interface components

One kit, built once, rendering on both surfaces — which is the thing that stops a two-platform product quietly becoming two products.

11

Screens, every one responsive

Phone, tablet portrait and tablet landscape. The board recomposes rather than reflows, and holds its layout inside any frame.

The product, running

Not a screenshot — and not one device

Both devices below are the real application, mounted in this page’s React tree and reading one store. They are not two sizes of one screen — the board answers “how is the house doing”, the phone answers “what do I owe it today”. Steer each with its own controls; the address bar follows both.

Live — the real appiPad Pro 11″ · iPhone 17 Pro

The family iPad

Resting on its home screen — its own tabs are live.

A member's phone

Resting on its home screen — its own tabs are live.

Both devices are live and read one store — finish something on the phone and the board’s count moves with it. 27 open tasks right now, on both.

Why this exists

The category’s own critics wrote the brief

Secondary research — published reporting and academic work, not original studies. Three findings shaped what this product refuses to do.

Finding 01

The work in managing the app is still going to be seen as women’s work.

Jaclyn Wong, sociologist, University of South Carolina

The decision it drove

The app assigns; the person doesn’t delegate. Adding a task names an owner and says why — a precedent, a lighter load, whose domain it is — so the default path costs nobody an act of distribution.

MIT Technology Review — “Chore apps were meant to make mothers’ lives easier”

Finding 02

Apps focus on the visible, physical chores and overlook the cognitive work — the part that never stops.

Allison Daminger, sociologist — anticipate, identify options, decide, monitor

The decision it drove

The product does the anticipating. It detects a double-booking, or work stranded on someone who’s away, before anyone notices — and what it surfaces is the resolution, already worked out, rather than a notification.

MIT Technology Review, on Allison Daminger’s four-step cycle of invisible labour

Finding 03

The mum-dad-two-kids picture is a particular arrangement from a particular time and place.

On the “cereal packet” family that most products still assume

The decision it drove

Relationships never determine permissions. Household type, identity, role and responsibility are four independent fields, so a fifteen-year-old may hold full access and a forty-year-old may be guided.

Ecovillages AU — “Why do we design for the nuclear family?”

Read together, that’s one thesis, not three: the product is supposed to carry the coordination itself, not hand a well-organised version of it to whoever already carries the most.

The system

Read from the source, at render time

Every specimen here imports the same token module tailwind.config.ts builds the app’s utilities from. Change a value and this page changes with it — there is no second copy to fall out of date.

Colour · 16 values

One accent does all the shouting. Everything else is a surface, a tint, or ink.

ink

#0E0E10

19.3:1 on white

lemon

#F4FF7A

1.1:1 on white

lavender

#C9C8F0

1.6:1 on white

mint

#C4E8D3

1.3:1 on white

accent

#D1D0F5

1.5:1 on white

progress

#C7C6F0

1.6:1 on white

surface

#F5F4F8

1.1:1 on white

card

#FFFFFF

1.0:1 on white

sub

#535660

7.3:1 on white

danger

#C73F43

5.0:1 on white

star

#C9A21A

2.4:1 on white

maya

sam

leo

ada

juno

Identity colours, assigned per person — the same colour on every surface.

Type · 7 steps

Poppins throughout. Weight rises as size falls — the big sizes are Regular, because heavy display type reads as urgency and a home is unhurried.

The washing needs movingdisplay36px / 40pxw400
The washing needs movingh128px / 34pxw400
The washing needs movingh218px / 24pxw500
The washing needs movingbody16px / 24pxw400
The washing needs movingbodysm14px / 20pxw400
The washing needs movingcaption14px / 20pxw400
The washing needs movingmicro12px / 16pxw400

Every size and line-height is an even number. Line-heights are px, not ratios — a ratio silently produces fractional pixels, which is how half-pixels crept in.

Spacing · 7 steps

Every step is a multiple of two.

hair2px
screen4px
tile8px
card-pad16px
inner24px
stack16px
section32px

Radius · 5 steps

sm

12px

md

16px

card

24px

sheet

24px

pill

999px

Control heights · 3 steps

Minimum tap target
touch · 44px
Buttons, pills, fields
ctl · 48px
Primary actions
ctl-lg · 52px

Rounded everywhere, never sharp — and nothing tappable is under 44px, which is the floor rather than the norm: the things you press most are 48.

Elevation · 3 steps

Soft and low. Named e1–e3, not by role — an elevation named card would collide with the colour of the same name and Tailwind would build a white shadow instead.

e1

e2

e3

Each is two shadows: a tight contact shadow and a wide soft one — which is what keeps a raised card from looking cut out and pasted on.

Motion

One ease, one spring, one press — read from lib/motion, the same transitions the app animates with.

smooth0.3s · cubic-bezier(0.16, 1, 0.3, 1)
springstiffness 420 · damping 34

All of it runs inside MotionConfig reducedMotion="user", so transform animations collapse to instant when the OS asks, while opacity fades stay.

Components · the assembled pieces

Imported from components/ui — the same module the two devices above render from, so these are the components rather than drawings of them. The checkbox toggles and the buttons press.

Identity

A person is a face and a colour, and keeps that colour on every surface. Stacked when a job is shared.

Icon wells

One stroke weight across the set, at two sizes holding the same 1:2 glyph-to-well ratio — a card size and a list-row size.

Status language

KitchenTodaySharedHigh

Urgency is the odd one out on purpose: it is text, not a chip, because a pill carried its own 28px and pushed every card it touched taller than its neighbours.

Completion

Water the garden

The one control the whole product turns on. Tap it — this is the live component, animating the way it does in the app.

Actions

One shape, several tones. Lemon is spent on the single primary action a screen exists for; everything else recedes.

When

5Wed11FriAny

A vertical stadium, not a circle — it stretches to the height of the card it sits in, so a date never floats in its own row.

The tags above are the light-surface tone. The set carries a second one for dark canvases — Overdue — because tag is ink on a 5% ink wash, and it disappears the moment the surface goes dark.

Where it nearly came apart

Five problems that changed a rule

Not a bug list — these are the ones that moved a standing decision. Each shows the broken state rebuilt from real tokens, beside what replaced it. One gets into real CSS mechanics; that part is collapsed by default, a click away for anyone who wants it.

01

“Mum, Dad, Kid” is not a permission model

We believed — a household is a set of family roles, and what somebody may do follows from which one they are. Adults get everything; children get a simplified view; one flag on a task marks it adults-only.

Beforeone identity field, doing three jobs

Member

Leo · Child

Relationship, life stage and access, all in one word.

The same checkbox guards the mortgage and the oven.

Afterfour fields, and two gates that ask different things

Identity

Leo · person, 10

What someone is. Never a permission.

Role

Tasks and chores

What someone may do. Assigned by an owner.

RequiresMoney, contracts or safety
Ages 8+Can they physically do it

What that got wrong

It fails on real homes immediately. The fifteen-year-old who runs the family budget. The grandparent who lives here and shouldn’t be locked out of the heating. The au pair who needs the school run but not the bank card. The adult child who moved back in.

And the single adultOnly flag was quietly doing two unrelated jobs: may this person be held accountable for it (money, contracts) and can this person safely do it (a hot oven, a chainsaw). One boolean, two questions — so it locked the sixteen-year-old out of the shopping card and let any visiting adult near the bleach.

The rule now

Household type, identity, role and responsibility are four independent fields. Role is assigned, never derived — no code may write age < 18 ? “limited” : “full”. A task carries two separate gates: requires (authority) and minAge (capability).

02

The fairest algorithm we had kept picking the ten-year-old

We believed — the kindest way to assign work is to give it to whoever is least busy. It is neutral, it is measurable, and nobody can argue with it.

Beforeranked purely by who had the lightest day
Collect the kids from schoolLightest load — 2 tasksLeo (10)
Renew the home insuranceLightest load — 2 tasksLeo (10)
Afterunassessed work goes to whoever can hold anything
Collect the kids from schoolLightest load — 10 tasksSam
Renew the home insuranceRuns the billsMaya

What that got wrong

In a family, the person with the lightest day is always a child. So every task that hadn’t been explicitly assessed — which is most of them, because they arrive as half-typed sentences — landed on Leo, aged ten. Including collecting his siblings.

The fix that suggests itself is an age check, and it’s the wrong one: it drags age back into permissions, which is the trap story 01 exists to close. The real insight was that an unassessed task isn’t “safe by default” — it is unknown, and unknown work should only ever be volunteered to someone who could hold anything it turns out to be.

The rule now

A task with no stated requirement has not been assessed, so it is only ever OFFERED to roles that can hold anything. Once work is assessed, a ten-year-old is a candidate like anyone else — which keeps children in the rota rather than exempt from it.

03

We stopped asking whether it was a task or an event

We believed — tasks and events are genuinely different things, so the person adding one should tell us which they mean. It is one tap, and it keeps the data clean.

Beforethe user resolves our data model first
TaskEvent
What needs doing?

Two forms behind two different words for “a thing on Thursday”.

Afterone field; the app says what it heard
Leo’s dentist Thursday at 4
Leo’s dentist

Thursday·4:00 PM·Leo

Just Leo — this one can’t be handed off.

What that got wrong

The distinction is real — a task is work the household can redistribute, an event is a booked slot with a body in it — but it is ourdistinction, and asking made it the user’s problem. Nobody adding “dentist, Thursday” is thinking about whether it is redistributable. They are thinking about the dentist.

So the app classifies instead. Vocabulary, a named plan, a social arrangement, an amount of money — enough signal to decide, then say the decision out loud in the readback where it can be corrected in one tap. The choice didn’t move; it stopped being asked.

The rule now

Never make somebody understand our internal split to use the product. Infer it, state the inference in plain words, and make the correction cheap. The user types one sentence — the app decides what kind of thing it just heard.

04

The iPad stopped being a bigger phone

We believed — one responsive application, two breakpoints. The tablet is the phone with more room — the same screens, laid out wider.

Beforethe same personal list, stretched

More width, identical information. The room learns nothing the pocket didn’t know.

Aftera different screen, for a different question

Morning

Afternoon

Evening

The day by time-of-day, and the people carrying it — neither exists on the phone.

What that got wrong

The two devices are not two sizes of one reader. A phone is in a pocket and belongs to a person: it should answer what do I owe the house today. The iPad is on a wall in a kitchen and belongs to the room: nobody is logged into it, and the question it answers is how is this household doing.

Once that was said out loud the layouts stopped being versions of each other. The board grew a screen the phone doesn’t have (the whole household, by person, with the load between them) and lost the one the phone is built around (a personal tab — the iPad is never a person). The phone kept the day; the iPad got the week on the wall.

The rule now

They are two applications over one truth, not one application at two widths. A screen exists on a surface because that surface's question needs it — the shared tab list is a coincidence, not a requirement.

05

The viewport was never the device

We believed — a media query knows how wide the iPad is, so `lg:` and `vh` are the natural way to lay it out — the same tools that work everywhere else on the web.

Beforea vh-sized element, measured against the browser
nav
overlap
Afterthe container answers, so the clearance is real
nav

What that got wrong

It came back three separate times as the same symptom before the cause got a name: a nav floating over content it was supposed to clear. Not a spacing bug — a measurement one. The tool telling the layout how big the iPad was had never once been asking the iPad.

The rule now

Anything that must know how big the DEVICE is asks a container, not the viewport. Container queries (@[1024px]:) and fixed px only — no lg:, no vh, no vw inside the frame.

Tests

What the suite is actually for

5files, and none of them chase coverage. Each pins a rule that a real bug already broke once — including the numbers printed on this page. Built solo, at the standard I’d want a team operating at.

  • Contrast

    Every text colour is checked against every surface it actually sits on, at AA.

  • Token scales

    Spacing stays on the 2px grid; every type size and line-height stays even.

  • Journeys

    The deep-link list and the demo panel can't drift apart, and no journey points at a route that doesn't exist.

  • The page

    It renders, it isn't stuck, and it mounts the product inline rather than in an iframe.

  • One width

    Every content wrapper resolves to a single shared width.

  • Honesty

    Every figure printed on this page is recomputed from the repo and compared.

facts.test.ts

const fresh = computeFacts();

expect(REPO.lines).toBe(fresh.lines);
expect(REPO.components).toBe(fresh.components);
expect(REPO.commits).toBeLessThanOrEqual(fresh.commits);

In plain language

This test makes the page unable to lie about itself.

What it claims: the figures in “At a glance” match the repository as it stands right now.

How: it recounts the lines, the components and the commits straight from the working tree, then compares them to the numbers this page prints. Commits are checked as a floor, because that figure only ever rises.

Why it matters: stats on a portfolio page rot silently — the code moves, the numbers don’t, and nobody notices. Here the suite fails and names the figure that drifted.

Accessibility

The audited result, and what it doesn’t mean

axe-core 4.10.2, WCAG 2.1 A + AA, run in a real browser at 1440 × 900 and 390 × 844 with both devices mounted — reported as two numbers, because one would let the article hide behind the product.

0

violations in this page’s own chrome

15

elements failing inside the embedded product

all colour contrast · 15 at both viewports · worst 1.16:1

What the product is failing on

  • Inactive labels in the iPad's floating nav 1.16:1A real bug, not a thin token: the glass samples what's behind it to invert its text, and elementsFromPoint misreads inside a scaled container. It used to show only at the narrow viewport; at the embed's current device size it shows at both.
  • Time-of-day headings on the board 3.79:1White at 40% on the ink canvas. The token clears AA alone; the transparency is what drops it under.
  • Card eyebrows on white (Meals, Bills, Garage…) 3.93:1text-sub at 75% — the largest single group, and the same cause: a good token, thinned.
  • The Worth-a-look heading and its second lines 4.26 – 4.43:1White at 45% on ink and on a raised card. Closest to passing, and the easiest to fix.
  • Two more were found here and shipped. Writing this page’s contrast tests turned them on the product: sub at 3.28:1 and danger at 3.91:1. Both swatches were darkened rather than the text lightened.
  • It isn’t “accessible.” Automated rules catch a minority of WCAG criteria. No screen-reader pass, no assistive-technology user has reviewed this.
  • Reduced motion is honoured app-wide, with one known gap: the avatars blink using SVG SMIL, which the CSS override doesn’t reach.

Where this goes next

Built as a prototype,
on purpose

Two surfaces share one in-memory store, seeded with seven months of plausible household history. That was the right shape for proving the interaction model, and it’s the starting condition for everything below.

Next

What breaks the illusion fastest, still mocked.

  • Real sync — one household across actual devices, not one store behind two lenses.
  • A delivery layer, so quiet hours gate something real.
  • Recurrence as a rule with exceptions, so “this one” and “all of them” can differ.

Later

Real value — the model just doesn’t need it yet.

  • A scheduler that proposes when, not just who.
  • Calendar and billing sources behind the connect flows that already exist.
  • The full VoiceOver and Dynamic Type pass.

Exploring

The right question. Not yet a specced answer.

  • An ambient mode for the iPad that never really turns off.
  • Whether the household should be able to explain a decision back — “why me?”
  • Trends that notice a change instead of drawing one.