Reference

LandlordKit design system

Civic precision: a compliance tool built to read like a well-made document. Paper surfaces, a single confident brand hue, urgency as a first-class colour axis, and tabular figures everywhere a date or a dollar appears.

Surfaces and brand

Warm paper ground, pure white cards raised on top, deep teal-blue for action.

Sunken

--surface-sunken

Background

--background

Card

--card

Muted

--muted

Accent

--accent

Input border

--input

Primary

--primary

Foreground

--foreground

Urgency tones

Five tones, one meaning each. Every component that shows urgency reads its colour from the same map, so an overdue deadline looks identical everywhere.

ToneRampWhen to use itExample
Critical
A deadline has passed, or data is wrong in a way that invalidates a notice.Standard lease overdue
Warning
A deadline is close enough to plan around, or a value is unconfirmed and will block a notice later.Rent control status unknown
Information
Neutral compliance information: an upcoming date, or which rule applied and why.14-day notice period, in effect until September 20, 2026
Satisfied
A requirement is satisfied on time, or a window is now open.L1 filing opens
Neutral
Structural or archival. No urgency implied.Tenancy ended

Typography

Source Serif 4 for display and headings, Inter for everything functional. Numbers are always tabular so columns line up.

DisplayThe rent increase takes effectSource Serif 4
Page titleThe rent increase takes effectSource Serif 4
SectionThe rent increase takes effectSource Serif 4
BodyThe rent increase takes effectInter
SmallThe rent increase takes effectInter
Numeric2026-09-21 · $1,895.00 · 2.1% · 1,024 units

Layout width

Two variants, chosen by route in the app shell rather than page by page. No page sets its own max-width.

data

max-w-[90rem]

Tables and portfolio surfaces: the calendar, the lists, a property or unit page.

measured

max-w-3xl

Reading and form surfaces: a notice, settings, onboarding, every create and edit form.

The route-to-variant mapping is data in lib/page-width.ts and is covered by unit tests, so a new page picks a side deliberately. The measured column is centred in the content area, not pinned to the left edge against the sidebar.

Waiting

Every route in the product is a Server Component doing real database work, so there is always a gap between the click and the answer. Two shapes cover it, chosen by the same rule that decides the page's width.

A data surface

The dashboard, the calendar, a list, a detail page.

A measured surface

Settings, onboarding, and every create, edit and prepare screen.

A skeleton here is a SHAPE and never a mock. Reproducing a table’s exact columns is wrong more often than right, because the column count depends on data nobody has loaded, and a fallback that changes shape as it resolves is worse than one that never claimed to match. The pulse is decoration: prefers-reduced-motion stops it and the meaning survives in the layout, with role="status" announcing it once for the page rather than once per bar. Which route gets which shape is derived from page-width.ts by a test, not kept by hand.

The accessibility floor

Four rules the whole system is built on, and none of them is a matter of taste. Three are enforced by tests; the fourth is a stylesheet rule nothing can opt out of by accident.

One focus treatment, and it is an outline

Full strength, 2px, offset by 2px. Tab into these and every one looks the same.

A link

An outline rather than a ring, for three reasons. It needs no offset colour, so one declaration is right on paper, on a white card and on a tinted callout alike. It is drawn at full strength, so it measures 6.3:1 in light and 8.9:1 in dark. And it survives forced-colors mode, where a box shadow is discarded outright. Components no longer set outline-none, and a test fails the build if one starts.

Contrast is measured, not judged

lib/contrast.ts parses globals.css and converts each oklch value to sRGB; contrast.test.ts asserts every pair in both themes.

  • 4.5:1Body and secondary textWCAG 1.4.3. Includes muted text at text-xs, which is where the disclaimer and every table label live.
  • 3:1The border of a form controlWCAG 1.4.11. Was 1.37:1, which is a box whose edge you infer from its shadow.
  • 3:1The focus outlineWCAG 2.4.11. Was 2.27:1: the token was fine and every component painted it at half opacity.
  • 3:1A tone used as a markStatus dots and the accent rule down a stat tile. Amber was 2.55:1 on a white card.

Motion is a preference

prefers-reduced-motion switches off every transition and animation in the product.

Nothing here is load bearing. Every animation is decoration on a state change that is also carried by layout, colour or text, so switching them off costs nothing at all. That is the property to preserve: if a new component ever needs its animation to be understood, it needs a second cue rather than an exemption.

Bypass and reach

A skip link on every shell, and one keyboard route to anything.

Every page in the app puts fourteen navigation links before its content, which is what WCAG 2.4.1 is about. A skip link is the first focusable element in the document on all three shells, and Ctrl K opens the quick jump over every page, property, unit and tenancy. Both are visible controls rather than folklore: the skip link appears on focus, and the jump box is drawn in the sidebar with its shortcut printed on it.

Components

The working set.

Buttons

Primary for the one action that moves the task forward.

Status badges

Tone plus a dot, so meaning survives without colour.

CriticalWarningInformationSatisfiedNeutral

Callouts

Home for the engine's plain-English explanations. These sentences are the product, so they never get buried as grey helper text.

Why this date

The earliest lawful increase is September 1, 2026: 90 days after service and at least 12 months after the last lawful increase on August 1, 2025.

Rent control status not confirmed

Rent increase notices are blocked until the status is set. Units first occupied on or before November 15, 2018 are rent controlled.

This could not be saved

The effective date is only 62 days after service. It must be at least 90.

Stat tiles

Tone drives an accent rule down the left edge rather than a full tint. At dashboard scale, tinted tiles turn into noise.

Active tenancies

12

Across 4 properties

Needs attention

2

Deadlines already passed.

Due in 30 days

5

Arriving this month.

Monthly rent roll

$21,340.00

Active tenancies.

Stat tiles as filters

A count on a dashboard is really a question, and the tile is the nearest place to answer it. Give a tile onSelect and it becomes the filter for its own number: a button, aria-pressed, ringed when it is the one applied. A tile reading zero should not be selectable.

Showing 10 of 10 rows.

Data table

The default for any list of records. Type in the filter, click a header to sort, click a row to open it, open a row's explanation with the chevron, and use the button without opening anything. Narrow the window until this table has less than the 56rem it measures and it becomes the card stack below. That is a container query, not a viewport one: the sidebar appears at the width a viewport query would switch on, so a table at 1024px had only 700px to fit into.

  • Current rent
    $2,140.00
    Earliest increase
    3 days ago
    Maximum without LTB approval
    $2,184.94 (+$44.94, 2.1%)
    Notice pending
  • Current rent
    $1,450.00
    Earliest increase
    in 4 weeks
    Maximum without LTB approval
    No guideline cap (exempt unit, Form N2)
  • Current rent
    $1,895.00
    Earliest increase
    in 3 months
    Maximum without LTB approval
    $1,934.80 (+$39.80, 2.1%)

Data table, small screens

Once the table runs out of room every row becomes its own card: the title column is the heading, the rest become label and value pairs, and the row action and its explanation stay on screen instead of sitting in a fifth column off the right edge. Each card lays itself out from its own width, so this is the real 390px rendering, not an approximation of it.

  • Current rent
    $2,140.00
    Earliest increase
    3 days ago
    Maximum without LTB approval
    $2,184.94 (+$44.94, 2.1%)
    Notice pending
  • Current rent
    $1,450.00
    Earliest increase
    in 4 weeks
    Maximum without LTB approval
    No guideline cap (exempt unit, Form N2)
  • Current rent
    $1,895.00
    Earliest increase
    in 3 months
    Maximum without LTB approval
    $1,934.80 (+$39.80, 2.1%)

Data table facets

A text filter finds rows that mention something; a facet selects rows that ARE something. Typing the word served into the box also matches not served, so a status or a form type gets a drop-down and an exact match instead. Facets compare the row's plain value, never the rendered label, and combine with each other and with the filter box. A facet does not need a column of its own: this one narrows by rent control status, which the table only shows inside a sentence.

  • Current rent
    $1,895.00
    Earliest increase
    in 3 months
    Maximum without LTB approval
    $1,934.80 (+$39.80, 2.1%)
  • Current rent
    $1,450.00
    Earliest increase
    in 4 weeks
    Maximum without LTB approval
    No guideline cap (exempt unit, Form N2)
  • Current rent
    $2,140.00
    Earliest increase
    3 days ago
    Maximum without LTB approval
    $2,184.94 (+$44.94, 2.1%)
    Notice pending

Data table column contract

Cells are React nodes, so nothing in the component can read them. Each row carries a parallel values record of plain data, and that is what sorts and filters.

card: title
The card heading, and the row's link target. First column by default.
card: meta
A label and value line in the card body. The default for everything else.
card: action
Pinned to the foot of the card, above the row link so it stays clickable.
card: hidden
Table only. For a column the card already says another way.
sortable, filterable
Opt in per column. Sorting reads values, never the rendered cell, so a column can show “in 3 months” and sort by the real date.
row href, row tone
Makes the whole row a navigation target, and draws the left accent rule from the shared tone map.
minTableWidth
How much room the columns need before a table is the better shape, measured rather than guessed: open the page wide, read the table's scrollWidth, and pick the next size up. Below it the rows become cards. Guessing low puts the row action back off the right edge, which is the one thing this component exists to prevent. Check the tier against what the page can offer, too: a measured route gives a table inside a card about 41rem at any screen width, so anything from 3xl up would leave it cards at 1440px, which is a threshold nothing can reach rather than a measurement. Use xl or 2xl there.
facets
Exact-match drop-downs beside the filter box, for the questions a search box answers wrongly. Each names a column key and a list of option values, and those values have to be the stable ones in the row's values record rather than the sentence in the cell.
row expansion
A second layer per row: a disclosure at the end of the row holding the engine's full explanation in a Callout, spanning the whole table width. The scannable layer is a one line summary in a cell; the explanation stays one click away rather than being printed at table density or demoted to grey helper text.

Table primitives

Underneath DataTable: a tinted header rail, roomy rows, and a left accent rule carrying urgency without tinting the row. Reach for these directly only for a table that is not a list of records.

DateWhatWhere
3 weeks ago
Standard lease overdue72 Victoria St S, Unit 2
in 9 days
N4 termination18 Allen St E, Unit 1

Card and description list

The detail-page workhorse.

Rent increase outlook
Ninety days' notice, and twelve months since the last increase.
Current rent
$1,895.00
Last lawful increase
August 1, 2025
Earliest lawful increase
September 1, 2026

Empty state

Always a title plus a next action, never a bare sentence.

No properties yet

Add your first building, then its units and tenancies.

Danger zone

Destructive actions are quarantined with the consequence spelled out. Delete never sits next to Save.

Danger zone

Delete is for data entry mistakes only. Records with notice history are protected.

Legal disclaimer

Non-negotiable on every compliance output. The wording comes from the engine, so it cannot drift page to page.

This is general information and document preparation assistance, not legal advice. Confirm requirements with the Landlord and Tenant Board (LTB) or a licensed paralegal or lawyer before acting.

Page header

Back link, eyebrow, serif title, description, actions.

Notices

72 Victoria St S, Unit 2

Notice of Rent Increase

Filled onto the official LTB Form N1, v. 01/04/2022.