CSS Flexbox For Adaptive Fitness Dashboards That Don’t Suck On Mobile

How I use Flexbox to keep a workout dashboard readable when the data gets weird and the layout has to adapt on the fly. Practical patterns from a real fitness tracking UI.
CSS Flexbox For Adaptive Fitness Dashboards That Don’t Suck On Mobile
Photo by ERIC ZHU / Unsplash

Why a fitness dashboard is a Flexbox stress test

I coach baseball and track my own training. That means I stare at workout dashboards a lot more than is healthy.

The data is messy. Some days I have three metrics. Other days it is twelve. Sets change, exercises change, heart rate drops out mid session, a new wearable shows up with a different idea of what a “set” is.

That kind of chaos is exactly where Flexbox shines. Not some pixel-perfect Dribbble shot. Real-world, ugly data.

So this is how I build adaptive fitness dashboards with CSS Flexbox that still feel clean on a phone in a sweaty gym.

The layout constraints from actual training

When I designed my workout dashboard, I wrote down the constraints first instead of picking breakpoints blindly.

  • Needs to work one-handed on a phone between sets.
  • Must handle 1 to 10+ stat cards without exploding.
  • Key metrics (RPE, total volume, last set) always above the fold.
  • Same codebase has to live inside a web app and an embedded view on a TV in the gym.

I tried grid for this at first. I think grid is fantastic for known structures. Fixed card counts. Stable track definitions.

For a workout that changes every week, Flexbox felt nicer. I can let content drive the layout instead of faking certainty with hard-coded templates.

The base shell: two flex axes, not one

My root layout is boring on purpose. One main axis, one secondary. No cleverness.

<main class="dashboard">
  <header class="dashboard-header">...</header>
  <section class="dashboard-body">
    <section class="primary-panel">...</section>
    <aside class="secondary-panel">...</aside>
  </section>
</main>
.dashboard {
  min-height: 100vh;
  display: flex;
  flex-direction: column;
}

.dashboard-header {
  padding: 1rem 1.5rem;
}

.dashboard-body {
  display: flex;
  flex: 1;
  gap: 1.5rem;
  padding: 0 1.5rem 1.5rem;
}

.primary-panel {
  flex: 2 1 0;
  min-width: 0;
}

.secondary-panel {
  flex: 1 1 260px;
  min-width: 0;
}

@media (max-width: 900px) {
  .dashboard-body {
    flex-direction: column;
  }
}

Two choices here matter more than the rest.

  • flex: 2 1 0 on the main panel means it greedily takes space but still shrinks when it must.
  • min-width: 0 keeps content from blowing up the layout when one number or label gets long.

If your flex items start overflowing horizontally “for no reason”, you probably forgot min-width: 0 or overflow control. I see that constantly in fitness dashboards that just assume every label is 8 characters long.

Workout summary strip: Flexbox as a content compressor

Top of the dashboard, I keep a compact summary row. Date. Session type. Total volume. Duration. RPE. Simple stuff.

<header class="session-summary">
  <div class="summary-main">
    <h1>Upper Body Power</h1>
    <p>Monday, Week 6</p>
  </div>
  <div class="summary-metrics">
    <div class="metric"><span>Volume</span><strong>14,320 kg</strong></div>
    <div class="metric"><span>Duration</span><strong>56 min</strong></div>
    <div class="metric"><span>RPE</span><strong>8.5</strong></div>
  </div>
</header>
.session-summary {
  display: flex;
  align-items: center;
  justify-content: space-between;
  gap: 1rem;
  padding: 1rem 1.5rem;
  border-bottom: 1px solid rgba(255, 255, 255, 0.06);
}

.summary-main {
  min-width: 0;
}

.summary-metrics {
  display: flex;
  flex-wrap: wrap;
  justify-content: flex-end;
  gap: 0.75rem 1.5rem;
}

.metric {
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  font-size: 0.85rem;
}

.metric strong {
  font-size: 1rem;
}

@media (max-width: 700px) {
  .session-summary {
    flex-direction: column;
    align-items: flex-start;
  }

  .summary-metrics {
    width: 100%;
    justify-content: flex-start;
  }
}

I like flex-wrap: wrap here. On big screens the metrics form a tidy right-aligned cluster. On smaller screens they spill to two rows without me micromanaging where the breaks happen.

This is a recurring pattern for me. Use Flexbox to compress information until it absolutely has to wrap, then let it wrap without drama.

Exercise list: One layout, three modes

The main content is a list of exercises with set details and controls. Hooks for RPE sliders, load adjustments, notes, little PR icons.

I wanted one markup structure that behaves like three different layouts depending on space: table-ish on desktop, card-ish on tablet, stack on mobile.

<section class="exercise-list">
  <article class="exercise">
    <header class="exercise-header">
      <h2>Back Squat</h2>
      <span class="exercise-meta">5 sets · 3 reps</span>
    </header>
    <div class="exercise-body">
      <div class="exercise-main">
        <div class="sets">... sets table ...</div>
      </div>
      <aside class="exercise-side">
        <div class="stat-chip">PR</div>
        <div class="stat-chip">Avg RPE 8.2</div>
        <button class="note-btn">Add note</button>
      </aside>
    </div>
  </article>
</section>
.exercise-list {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.exercise {
  padding: 1rem 1.25rem;
  border-radius: 0.75rem;
  background: rgba(255, 255, 255, 0.03);
}

.exercise-header {
  display: flex;
  justify-content: space-between;
  gap: 0.5rem;
  margin-bottom: 0.75rem;
}

.exercise-header h2 {
  font-size: 1rem;
}

.exercise-body {
  display: flex;
  gap: 1.5rem;
}

.exercise-main {
  flex: 3 1 0;
  min-width: 0;
}

.exercise-side {
  flex: 1 1 180px;
  display: flex;
  flex-direction: column;
  align-items: flex-end;
  gap: 0.5rem;
}

.stat-chip {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  padding: 0.15rem 0.5rem;
  border-radius: 999px;
  font-size: 0.75rem;
  background: rgba(0, 255, 153, 0.08);
}

@media (max-width: 900px) {
  .exercise-body {
    flex-direction: column;
  }

  .exercise-side {
    flex-direction: row;
    align-items: center;
    justify-content: space-between;
    flex-wrap: wrap;
  }
}

@media (max-width: 600px) {
  .exercise-header {
    flex-direction: column;
    align-items: flex-start;
  }
}

This is where Flexbox keeps the brainload low. On desktop it feels like a two-column layout with details on the right. On tablet the aside flips under the sets. On mobile the header and aside both turn into little flexible clusters.

No different components. No “mobile-only card” variants. Same markup, just flex-direction changes and some wrapping.

Horizontal scrolling panels without the usual pain

One thing I learned from lifters: they like history. People want to know what they did last week and last month, but they do not want it to dominate the screen during the current session.

I use horizontal scroll panels for this. Flexbox again. Cards in a row that overflow nicely.

<section class="history-panel">
  <header>Last 5 sessions</header>
  <div class="history-row">
    <article class="history-card">...</article>
    <article class="history-card">...</article>
    <article class="history-card">...</article>
    <!-- etc -->
  </div>
</section>
.history-row {
  display: flex;
  gap: 0.75rem;
  padding-bottom: 0.25rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
}

.history-card {
  flex: 0 0 220px;
  scroll-snap-align: start;
  padding: 0.75rem 1rem;
  border-radius: 0.75rem;
  background: rgba(255, 255, 255, 0.04);
}

@media (min-width: 1000px) {
  .history-card {
    flex-basis: 260px;
  }
}

flex: 0 0 220px is doing a specific job here. Do not grow. Do not shrink. Always claim this width. Which makes swiping predictable on mobile and prevents “one huge card, two tiny ones” layouts I see all over.

Flexbox plus scroll-snap-type gives you a neat carousel effect for free. No library. No extra wrappers. Just a row that scrolls.

Using flex-grow for what actually matters

One opinion I have: if everything is flex: 1, you have not decided what matters.

On a workout dashboard, some things are more important than others. I want the current exercise and its sets to dominate. I want historical fluff to stay small. Same for little badges.

I use flex-grow to encode that priority.

.primary-panel {
  flex: 3 1 0;
}

.secondary-panel {
  flex: 1 1 260px;
}

/* Inside secondary panel */

.secondary-panel {
  display: flex;
  flex-direction: column;
  gap: 1rem;
}

.live-metrics {
  flex: 0 0 auto;
}

.history-panel {
  flex: 1 1 auto;
  min-height: 0;
}

This slight bias in growth makes the layout behave nicely on weird screens. On a narrow laptop the primary panel still wins. On a tall monitor the secondary panel can breathe without stealing focus.

The trick is to use flex-grow intentionally at a few levels, not everywhere.

Handling ugly labels and long metric names

Wearables love long names. Devices, zones, calculated fields. If you do not plan for that, your neat layout just shatters.

Flexbox has two simple tools for this problem.

  1. Allow items to wrap instead of squashing.
  2. Force truncation and keep the line count low.

I usually mix them.

<div class="metric-pills">
  <button class="pill">Garmin HRM-Pro Plus</button>
  <button class="pill">Zone 2 (65-75% HRR)</button>
  <button class="pill">Bar Velocity Tracker</button>
</div>
.metric-pills {
  display: flex;
  flex-wrap: wrap;
  gap: 0.5rem;
}

.pill {
  max-width: 100%;
  display: inline-flex;
  align-items: center;
  padding: 0.25rem 0.5rem;
  border-radius: 999px;
  border: 1px solid rgba(255, 255, 255, 0.1);
  font-size: 0.8rem;
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}

@media (max-width: 480px) {
  .pill {
    white-space: normal;
  }
}

On bigger screens I keep pills to one line and just truncate visually. You still see the important part. On tiny phones I let them break. Losing a clean pill shape is better than hiding half the label.

Again, the important part is that the flex container wraps, not some fixed grid that forces consistent sizes at the expense of legibility.

Mobile-first tweaks that actually came from the gym

I changed a few things only after using the dashboard mid workout. Not in a design tool. In a squat rack, breathing heavy, with chalk on my hands.

  • Buttons in the exercise-side area got pushed to the bottom on mobile. My thumb kept hitting them when I wanted to scroll.
  • I increased gap sizes between flex rows on small viewports. Crowded UI in the gym feels worse than at a desk.
  • I moved volume totals into the header on small screens so I see them without scrolling when I am tired and lazy.

These are tiny changes, but Flexbox makes them cheap to try. I mostly flipped directions and adjusted justify-content, not rewrote components.

@media (max-width: 640px) {
  .exercise-side {
    order: 2;
  }

  .exercise-main {
    order: 1;
  }

  .exercise-actions {
    margin-top: auto;
  }
}

Using order like this is controversial in accessibility circles if you go wild with it. I use it sparingly and keep the DOM order sensible, but for this UI, switching the visual stack on narrow screens was worth it.

Where Flexbox beats Grid for this use case

I like grid a lot. I still reach for Flexbox first on adaptive workout dashboards.

My reasons are simple.

  • Content counts change constantly. Flex wrapping feels more honest than hand-maintained grid templates.
  • I want main-axis logic: current vs history, primary vs secondary, above the fold vs scroll. Flexbox is built exactly for that.
  • I value smaller mental overhead during refactors. Flexbox is cheaper to rearrange when I decide to move history below the fold or merge panels.

If your dashboard has a more rigid structure, grid might be perfect. Think fixed cells for a competition scoreboard, not a personal workout log.

For my own training and my athletes, the chaos never stays put. Flexbox fits that reality better.

What I would do differently next iteration

After living with this Flexbox setup for a while, a few things go on the “next version” list.

  • Replace some Flexbox vertical stacks with flex-flow: row wrap clusters instead of separate components for tablet versus mobile.
  • Use container queries to tune layouts based on panel width, not global viewport. The embedded TV view really needs that.
  • Extract a small token system for flex values so I am not guessing between flex: 2 1 0 and flex: 3 1 0 every time.

The core idea stays the same though. Use Flexbox as a pressure valve. Let it absorb the chaos of real workout data while you keep the important parts readable for someone who is tired, sweaty and using a thumb on a small screen.

If your fitness UI survives that scenario, it is probably good enough for everything else.

Subscribe to my newsletter

Subscribe to my newsletter to get the latest updates and news

Member discussion