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).
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.
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.
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. |
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.