Open Freight dashboard on its own page (1280px wide as designed)
Preview width
Showing at 1280px wide.
Freight dashboard
A freight analytics screen for a fictional carrier, Lintas Cargo, rebuilt to survive any data. Every panel renders from data, and a sample-data switch at the top previews six states: no data, 1 record, 2 records, typical, so many, and broken. All figures are sample data.
Consumer provides: the data, the filter logic and the actions. Keep the empty, one-item and broken handling below when you plug in real data.
Layout: grid, flex and gap only
- Page: a vertical flex stack with
gap. There are no margins between siblings;main :is(h1, p, dl, …) { margin: 0 }removes the element defaults inside the layout. - Key figures:
repeat(auto-fit, minmax(min(100%, 11rem), 1fr)). There are as many columns as fit; on a tiny screen one column is the full width, never wider. - Board: the same auto-fit grid at
min(100%, 20rem).- The trend and heatmap panels span two tracks only inside
@container board (min-width: 44rem), so a span never creates an overflowing implicit column. - Lanes spans
1 / -1. align-items: startstops short panels stretching to match tall neighbours.
- The trend and heatmap panels span two tracks only inside
- Panel: a flex column with
gap,container-type: inline-size, and a header that wraps (flex-wrap, titleflex: 1 1 12rem). - Bars: a three-column grid (
minmax(6rem, 38%) minmax(0, 1fr) auto). When the panel is under 22rem, a container query moves the bar under its label.
Defensive CSS
| Risk | Guard |
|---|---|
| Long unbroken words and IDs push layout wide | min-width: 0 on every grid and flex child; overflow-wrap: anywhere on labels, IDs and reasons; break-word on panels, so table columns don’t shrink letter by letter |
| Grid track wider than a phone | minmax(min(100%, X), 1fr) |
span 2 on a one-column grid |
span only inside a container query |
| Wide tables and charts | only inside .scroll boxes (overflow: auto, overscroll-behavior: contain, scrollbar-gutter: stable); the page never scrolls sideways |
| Very long lists | .scroll.tall caps height at --scroll-max with a sticky header; lists show a first page plus a count |
| Big numbers in small cards | font-size: clamp(1.125rem, 13cqi, 1.5rem) from the card’s own width, after a plain 1.5rem fallback |
| Empty containers collapsing | one .empty block with min-block-size: 9rem, a heading, a sentence and an optional link; .note:empty { display: none } |
| Missing custom property | var(--space-md, 16px) style fallbacks on layout spacing; var(--v, 0) on bar widths |
| Bar widths | width: calc(var(--v) * 1%), min-width: 2px for small positives, max-width: 100% |
| Truncation hiding meaning | no text-overflow: ellipsis anywhere; content wraps |
Data states
| Panel | No data | 1 | 2 | Typical | So many | Broken |
|---|---|---|---|---|---|---|
| Key figures | 0 and “—”, “No shipments in this period” | values shown | values shown | values with comparison | 15,482,907 wraps and shrinks inside its card | “Not available” plus the reason; card border turns dashed |
| On-time trend | empty state with a link | one dot, “a trend needs at least two” | line plus “a comparison, not a trend”; axis extends to 0% | line, target, keyboard readout | 365 days, thinned ticks, PageUp/PageDown step a week | line breaks at bad points, dashed marks at each gap, readout says “no data” or “invalid value” |
| Commodity bars | empty state | one bar, “nothing to compare” | two bars | sorted bars | top 7 plus one “Other (53 commodities)” row with no bar, so the group does not dwarf the rest | long names wrap, a blank name reads “Unnamed commodity”, null shows “No data”, negative shows “Invalid”, both listed last without bars |
| Exceptions | “No exceptions. Every shipment is on schedule.” | one item | two items | 5 of 37 plus “Show all 37” | 5 of 12,480 | fallbacks: “No shipment ID”, “Lane not recorded”, “Unknown status”, “No reason given”; long reasons wrap |
| Heatmap | empty state | one row, “Nothing to shade” if all zero | two rows | shaded table | 34 origins in a scrolling box with a sticky header | missing counts show a hatched “—” read as “No data”; a blank origin reads “Unknown origin” |
| Lanes | “No lanes in this period” in the table body | one row, sort buttons disabled | two rows | sortable, filterable | 500 lanes, 10 per page, “Showing 11–20 of 500”, Previous/Next | long lane names wrap within 22rem, missing numbers show “—” and always sort last, impossible percentages get a “Check data” badge |
Rules behind the table:
- Never draw what isn’t there. Invalid values are gaps or “No data”, never zero.
- Always say why a number is missing.
- Change wording as the amount of data changes: one day is not a trend, and two days are a comparison.
- Group or page long data rather than shrinking it. Keep counts in words (“Showing 5 of 12,480”).
- The sample-data switch is a preview tool for this page only; remove it in a product.