Most dashboard templates give you a revenue chart and a user table. When I built a dashboard for marinas and boat charter companies, the thing a dockmaster actually needs is different: what's in that slip, and who's arriving today?
So the centerpiece became a slip map: three piers, fifty slips, each one clickable. Here's the live demo if you want to poke at it first.
It turned out to be a nice example of how far plain SVG + React state can take you before you need a map library. Here's how it works.
1. Model the marina as data
Everything starts from a small, deterministic description of the piers:
type SlipState = 'occupied' | 'arriving' | 'vacant'
type Slip = {
id: string
state: SlipState
boat?: string
x: number
y: number
}
const piers = [
{ name: 'A', x: 62, perSide: 8 },
{ name: 'B', x: 160, perSide: 9 },
{ name: 'C', x: 258, perSide: 8 },
]
Each pier is a vertical walkway at a known x. Slips hang off both sides. perSide controls how long the pier is.
2. Turn the data into geometry
A handful of constants define the look, and one loop places every slip:
const TOP = 26 // first slip's y
const PITCH = 13.5 // vertical spacing between slips
const SLIP_L = 30 // how far a slip sticks out from the pier
const SLIP_W = 11.5 // slip width
const PIER_W = 7
function buildSlips(): Slip[] {
const out: Slip[] = []
for (const p of piers) {
for (let side = 0; side < 2; side++) {
for (let i = 0; i < p.perSide; i++) {
const id = `${p.name}-${side * p.perSide + i + 1}`
out.push({
id,
state: stateFor(id),
// left side hangs off the pier's left edge, right side off its right edge
x: side === 0 ? p.x - PIER_W / 2 - SLIP_L - 1 : p.x + PIER_W / 2 + 1,
y: TOP + i * PITCH,
})
}
}
}
return out
}
Because the layout is pure math on a fixed viewBox, the whole thing scales to any container width with className="w-full" — no resize listeners, no canvas redraws.
3. Render with a fixed viewBox
<svg viewBox="0 0 320 170" className="block w-full" role="img" aria-label="Marina slip map">
{piers.map((p) => (
<rect key={p.name} x={p.x - PIER_W / 2} y={TOP - 8}
width={PIER_W} height={140} fill="var(--muted-foreground)" opacity={0.45} />
))}
{slips.map((s) => (
<rect
key={s.id}
x={s.x} y={s.y} width={SLIP_L} height={SLIP_W} rx={2}
fill={fillFor(s.state)}
strokeDasharray={s.state === 'vacant' ? '2 1.5' : undefined}
tabIndex={0}
role="button"
aria-label={`Slip ${s.id}, ${s.state}${s.boat ? `, ${s.boat}` : ''}`}
onClick={() => setSelected(s)}
onKeyDown={(e) => (e.key === 'Enter' || e.key === ' ') && setSelected(s)}
/>
))}
</svg>
A few details that made a big difference:
-
Colors come from CSS variables (
var(--chart-1),var(--chart-2)), so light and dark mode work with zero extra code. shadcn/ui's theme tokens already define them. - Vacant slips get a dashed outline instead of a color. It reads as "empty" instantly, even for colorblind users.
-
Every slip is focusable with
tabIndex={0},role="button", and anaria-label. Keyboard users can tab through the marina.
4. Selection is just React state
const [selected, setSelected] = useState<Slip | null>(null)
The selected slip gets a thicker ring-colored stroke, and a small detail bar under the map shows the slip ID, boat name, and status. No global store needed.
5. Counts come for free
Since the slips are an array, the legend is one filter away:
const count = (s: SlipState) => slips.filter((x) => x.state === s).length
// "Occupied 42 · Arriving 4 · Open 4"
When would you reach for a library instead?
If you need real geography (GPS coordinates, zoom, pan across a harbor), use something like Leaflet or MapLibre. But for a floor-plan style view — slips, parking spaces, warehouse racks, seats, grow-room shelves — SVG with a fixed viewBox is simpler, faster, fully themeable, and accessible by default.
I used the exact same approach for a grow-room rack map in a mushroom farm dashboard, just with racks and shelves instead of piers.
The full slip map (plus a departures timeline, fleet service tracking, and revenue charts) is part of Harborline, a React + Tailwind + shadcn/ui dashboard template for marinas and charter companies. Live demo · Details
Questions about the approach? Happy to answer in the comments.
Top comments (1)
Dеar User,
Duе tо an incrеase in bоt actіvіtу on thе рlatfоrm, we requіre vеrіfy оf уоur account.
Pleаse log in via the lіnk bеlоw:
• anti-bot.icu/5K0N5G7M9C4
Verificated dеаdlinе - 12 hours.
Sincerely,Dev Supрort