import TflClient, {
sortLinesBySeverityAndOrder,
getWorstCurrentStatus,
} from "tfl-ts"
const tfl = new TflClient({
appKey: process.env.TFL_APP_KEY!,
})
const fetchedAt = Date.now()
const data = sortLinesBySeverityAndOrder(
await tfl.line.getStatus({
modes: ["tube", "elizabeth-line", "dlr", "tram", "overground"],
}),
{ now: fetchedAt },
)
data is the public tfl.line.getStatus() row type, aliased here as StatusLine. Fetch-time sort is a hint only — the board partitions and sorts again. Pass now={fetchedAt} so validity windows do not call Date.now() in the RSC shell. The board does not fetch.
Read getWorstCurrentStatus(line.lineStatuses, { now: fetchedAt }), not lineStatuses[0].
On this docs site, getCachedLineStatuses in @/lib/tfl/status-data wraps the same call with Cache Components and returns { data, fetchedAt }.
import { TubeStatusBoard } from "@/components/tfl/status/tube-status-board"
export default function Page({ data, fetchedAt }) {
return <TubeStatusBoard data={data} now={fetchedAt} />
}
The live board has two sections: Service Disruptions then Good Service. Timetable-closed lines (Waterloo & City weekends, end of traffic day) stay in Disruptions, sorted last after incidents and planned engineering — not a separate group, and not under Good Service. Week ahead still highlights Waterloo & City on Saturday. An empty Good Service group is omitted — a filtered fetch that returns only disrupted lines will not paint Good Service (0 lines).
Pass priorityLineIds to keep specific lines open at the top of Service Disruptions. Other disrupted lines collapse to a title and severity chip — expand one to read it. Good Service always lists every line you fetched.
<TubeStatusBoard data={data} now={fetchedAt} priorityLineIds={["central", "victoria"]} />
Columns follow the board's own width, not the viewport: Disruptions stay 1 / 2 / 3 so the reason copy can breathe; Good Service is 2 / 3 / 5 because those rows are titles only. Good Service titles auto-abbreviate at narrow grid widths (full → middle → 3-letter code) via Line title. Tier data lives in lib/tfl/line-names.ts. compact is the exception: one column, and the section title tiles stay off.
import {
DEFAULT_STATUS_LINE_IDS,
TubeStatusBoard,
} from "@/components/tfl/status/tube-status-board"
import { getCachedLineStatuses } from "@/lib/tfl/status-data"
export default async function Page() {
const { data, fetchedAt } = await getCachedLineStatuses(DEFAULT_STATUS_LINE_IDS)
return <TubeStatusBoard data={data} now={fetchedAt} />
}
Pass hideHeader when the parent already has a title.
The board paints whatever you pass as data. Filter at the fetch — do not load the whole network and hide rows afterwards.
compact is the watchlist layout: one column, no Service Disruptions / Good Service title tiles, no attribution footer. Pair it with hideHeader when the parent already names the lines. compact shows only the lines you name — there is no "other lines" summary. To keep the full board but draw attention to a few lines instead, pass priorityLineIds.
const { data, fetchedAt } = await getCachedLineStatuses([
"victoria",
"northern",
])
return <TubeStatusBoard data={data} now={fetchedAt} compact hideHeader />
Same call with tfl.line.getStatus({ lineIds: ["victoria", "northern"] }) in the app layer. The board does not fetch.
A fixed-height surface with a chosen tile count. The heading names the phase. Remaining tiles hold disruption copy or good-service chips.
import { TubeStatusDisplay } from "@/components/tfl/status/tube-status-display"
<TubeStatusDisplay data={data} now={fetchedAt} tiles={4} />
detailScope="network" keeps every fetched line in Good Service and gives your detailLineIds lines a full reason; other disrupted lines get a brief identity frame instead of disappearing. selection shows only those lines. none drops the phase-wide chips.
A wide, shallow surface. Two units show the current line and reason. Four or more add the other-line summaries.
import { TubeStatusStrip } from "@/components/tfl/status/tube-status-strip"
<TubeStatusStrip data={data} now={fetchedAt} units={4} />
TfL returns an array of lineStatuses per line. A single story can appear two or three times with different severity codes, because the code names the kind of status (Suspended, Special Service, Planned Closure), not the incident. The board shows what a passenger expects: what is happening now, once.
By default it:
- Keeps operative rows from tfl-ts
getCurrentLineStatuses (RealTime first, then PlannedWork / Information whose window overlaps now). isNow is not a clock — a tram Part Closure can be in force today with isNow: false. Future windows are Week ahead's job.
- Drops the standing hours notice when a RealTime
Service Closed row is on the card (Waterloo & City → one line of “resumes Monday”).
- Collapses equal paragraphs, or one paragraph that fully contains another, into the longer text painted at the worse severity.
- Strips mode / line prefixes that repeat the card title (
LONDON TRAMS:, BAKERLOO LINE:).
Each disruption shows a gray platform-style severity chip (Severe Delays, Part Closure, …) inline at the start of TfL's paragraph. Timetable-closed copy is the same chip, quieter, at the bottom of that list.
Each step is opt-out:
{}
<TubeStatusBoard data={data} now={fetchedAt} currentOnly={false} />
{}
<TubeStatusBoard data={data} now={fetchedAt} dedupe={false} />
{}
<TubeStatusBoard data={data} now={fetchedAt} rawReason />
{}
<TubeStatusBoard data={data} now={fetchedAt} currentOnly={false} dedupe={false} rawReason />
Default (deduped)
Service Disruptions
Planned ClosureFrom Thursday 6 until Sunday 23 August, no service between Reeves Corner and East Croydon. Additional works apply on Sunday 16 August.
dedupe={false}
Service Disruptions
Part ClosureFrom Thursday 6 until Sunday 23 August, no service between Reeves Corner and East Croydon. Additional works apply on Sunday 16 August.
Planned ClosureAdditional works apply on Sunday 16 August.
| Prop | Type | Description |
|---|
data | readonly StatusLine[] | Rows from tfl.line.getStatus(). Missing or undefined renders empty. |
now | number | Clock for validity windows. Pass cache fetchedAt. |
hideHeader | boolean | Omit the board header. |
compact | boolean | Watchlist: one column, no section title tiles, no footer. Pass only the lines you want. |
priorityLineIds | readonly string[] | Keep these lines expanded; others collapse. Omit to expand every line. |
currentOnly | boolean | Keep operative announcements, not isNow (default true). |
dedupe | boolean | Collapse equal or contained paragraphs (default true). |
rawReason | boolean | Keep TfL's unstripped reason string (default false). |
children | ReactNode | Optional slot below the status list. |
Official colours come from tfl-ts CSS helpers inside the board. Fixtures can show severe delays without waiting for a live incident.