Skip to content
ZA

Blog3 min read

How I Built Flexible Airline Seat Maps Without Creating a CSS Museum

I share how I moved from layout-specific CSS exceptions to a reusable, data-driven approach for airline seat maps supporting configurations such as 3-3, 2-4-2, and 3-4-3.

Airline seat map illustration showing reusable CSS layouts for 3-3, 2-4-2, and 3-4-3 aircraft seating configurations, with aisle gaps and selectable seats.
One reusable seat-map approach adapting across 3-3, 2-4-2, and 3-4-3 aircraft cabin layouts.

Building a seat map sounds easy at first.

You show some seats, leave a gap in the middle for the aisle, let the user select one, and everybody goes home happy.

Then the airline sends another layout.

First it is 3-3. Then it is 2-4-2. Then 3-4-3. Suddenly, my “simple” seat map starts looking like a collection of CSS exceptions collected over many years by different developers who may or may not still remember why they exist.

That was the challenge I faced while working on flight booking flows.

A seat map is not just a grid of boxes. It has to show available seats, selected seats, occupied seats, seat numbers, an aisle, and different cabin layouts—without breaking when the aircraft configuration changes.

The main issue was the aisle.

For a 3-3 configuration, the gap comes after the third seat:

A B C | D E F

But for a 2-4-2 layout, the gaps come after the second and sixth seats:

A B | C D E F | G H

And for 3-4-3:

A B C | D E F G | H I J

My first instinct was the classic frontend-developer reflex: add another selector.

css
.layout-3-3 .seat:nth-child(3) {
  margin-right: var(--aisle-gap);
}

Then another one.

css
.layout-2-4-2 .seat:nth-child(2),
.layout-2-4-2 .seat:nth-child(6) {
  margin-right: var(--aisle-gap);
}

Then another one for 3-4-3.

At that point, I realised I was not designing a reusable seat-map system. I was slowly opening a CSS branch office for every aircraft in the sky.

The better approach was to make the seat layout data-driven.

Instead of hardcoding the visual structure into the component, I used the aircraft layout as configuration:

javascript
const layouts = {
  '3-3': [3, 3],
  '2-4-2': [2, 4, 2],
  '3-4-3': [3, 4, 3],
};

This gave me one source of truth for the seat groups and aisle positions.

Then the UI could render each group separately:

tsx
<div className="seat-row">
  {layout.map((group, groupIndex) => (
    <div className="seat-group" key={groupIndex}>
      {group.map((seat) => (
        <button className="seat" key={seat.code}>
          {seat.code}
        </button>
      ))}
    </div>
  ))}
</div>

And CSS handled the spacing:

css
.seat-row {
  display: flex;
  gap: var(--aisle-gap);
}

.seat-group {
  display: flex;
  gap: 8px;
}

Now the aisle was no longer a special margin applied to a mysterious nth-child. It became a natural gap between seat groups.

This also made the rest of the seat-map logic easier to manage:

  • Available seats could be selected.
  • Occupied seats could be disabled.
  • Selected seats could keep their own visual state.
  • Different layouts could use the same component.
  • A new aircraft layout could be added through configuration instead of rewriting the UI.

The important lesson for me was simple: when the same UI changes based on a pattern, the pattern should live in data—not in a growing list of CSS exceptions.

Because when the next airline sends a 1-2-1 business-class layout, I would rather add one configuration object than start negotiating with nth-child(7) again.