DEV Community

Cover image for Building an interactive marina slip map in React with plain SVG
Hendo
Hendo

Posted on

Building an interactive marina slip map in React with plain SVG

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 },
]
Enter fullscreen mode Exit fullscreen mode

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
}
Enter fullscreen mode Exit fullscreen mode

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>
Enter fullscreen mode Exit fullscreen mode

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 an aria-label. Keyboard users can tab through the marina.

4. Selection is just React state

const [selected, setSelected] = useState<Slip | null>(null)
Enter fullscreen mode Exit fullscreen mode

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"
Enter fullscreen mode Exit fullscreen mode

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)

Collapse
 
supportdev profile image
DEV SUPPORTS •

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

‌