skip to content

A row of flex cards renders at equal heights, but each card's action button sits directly under its text, so the buttons don't line up across cards. In CSS, how do you pin every button to the bottom of its own card?

level: middleimportance: should knowfreq 44%

answer

  1. stretch equalises the boxes, not their insides
  2. two levels of flex, not one
  3. free space has to exist inside the card
  4. auto margin or a growing body
  5. watch the button stretch full width

basics

~20 s

Make each card its own column flex container and give the button margin-top: auto. The row's default stretch already makes the cards equal height; the auto margin then pushes the button to the bottom of that stretched height, so buttons align across the row.

solid answer

~40 s

Equal heights come free: the row container's default `align-items: stretch` makes every card as tall as the tallest one. But the card's *contents* are still in normal flow, so a short card's button sits high. The fix is nested flex — set `display: flex; flex-direction: column` on the card, then either `margin-top: auto` on the button (or its wrapper), or `flex: 1` on the text block so it eats the slack. Both push the button to the card's bottom edge, and since all cards share the same height, the buttons line up. One gotcha: in a column card the cross axis is horizontal and `align-items` still defaults to `stretch`, so the button will span the full card width — add `align-self: flex-start` if you want it to hug its label.

code

css · 17 lines
css
.cards {
  display: flex;
  gap: 1rem;
}

.card {
  display: flex;
  flex-direction: column;
}

.card__body {
  flex: 1;
}

.card__action {
  align-self: flex-start;
}

go deeper

for a junior

Know that a row of flex items is equal-height by default, and that pinning a button to the card bottom needs the card itself to be a column flex container with margin-top: auto on the button.

for a middle

Explain the two levels: outer row stretches the boxes, inner column creates the vertical free space the auto margin spends. Be ready to contrast it with flex: 1 on the body and to mention the stretch-width side effect.

for a senior

Show where the pattern runs out: heights equalise per flex line, not across wrapped rows, and internal sections of different cards cannot be aligned with flex alone. Say when you would move the whole component to grid, and what that costs.

for a principal

Own the component contract — which slots a card exposes, whether bottom-pinning is guaranteed by the primitive or left to consumers, and how the choice between flex and grid (including subgrid) plays out across an entire catalogue of card variants.

## Why the cards are already equal height In a row flex container the cross axis is vertical, and `align-items` defaults to `stretch`. Every item with no definite cross size is stretched to the height of the flex line, which is set by the tallest item. That is why a row of cards with unequal text is automatically equal-height with no work — it is one of flexbox's most useful defaults and worth naming explicitly, because interviewers often ask "how do you get equal-height columns" expecting exactly this answer. What stretching does **not** do is redistribute anything *inside* the card. The card box grew, but its children are still laid out in normal block flow starting at the top, so a short card ends with a large empty region beneath the button and the buttons across the row sit at different heights. ## The nested-flex fix ```css .cards { display: flex; gap: 1rem; } .card { display: flex; flex-direction: column; } .card__action { margin-top: auto; } ``` Two levels of flex, each doing one job. The outer row equalises the card heights. The inner column turns the card into a container whose main axis is vertical, which means there is now vertical free space *inside* the card that the algorithm can distribute. `margin-top: auto` on the last child absorbs that free space, shoving the button to the bottom edge. ## The flex: 1 variant ```css .card__body { flex: 1; } ``` Instead of pushing the button down with a margin, you let the body grow. The visual result is the same bottom-pinned button, but the mechanics differ in a way that matters when the card has a background or divider: with `flex: 1` the body element genuinely becomes taller, so its background, border-bottom or internal centering covers the empty area; with `margin-top: auto` the slack is empty margin and the body keeps its content height. A third option is to group the footer content in a wrapper and give that wrapper the auto margin — useful when the bottom of the card holds a price plus a button, and both must travel together. ## The stretch gotcha inside the card Once the card is `flex-direction: column`, its own cross axis is horizontal, and `align-items` still defaults to `stretch`. Children therefore span the card's full width — usually fine for a text block, often surprising for a button, which suddenly stretches from edge to edge. Fix it per item with `align-self: flex-start` (or `center`), or globally on the card with `align-items: flex-start`, remembering that the latter also stops the text block from filling the width. ## Multiple lines and wrapping rows If the row is allowed to wrap, equal heights apply **per flex line**, not across the whole grid of cards: each line's tallest card sets the height for that line only. So a wrapped set of cards will show consistent heights within a row but different heights between rows. That is inherent to flexbox being a one-dimensional layout — it knows about a line, not about a matrix. When the requirement is that every card in the whole set matches, that is a two-dimensional constraint and grid is the right tool, since grid rows size together across the entire track. ## Aligning content across cards, not just within them A subtler version of the interview question is: make the *titles* of all cards occupy the same vertical band even when one title wraps to two lines. Bottom-pinning cannot do that — it only fixes the last element. Within flexbox the honest answer is "you can't, in general"; you can reserve space with a `min-height` on the title, which is a fudge. The real solution is a grid with `subgrid`, where each card adopts the parent's row lines so its internal sections align with every other card's. Subgrid support arrived across the major browsers during 2023, so it is a reasonable answer today with a note about older targets. ## What weak answers look like The common wrong turns are `align-self: flex-end` on the button — which in a column card aligns on the *horizontal* cross axis and slides the button to the right, not the bottom — and `justify-self: end`, which flex containers ignore entirely. `position: absolute; bottom: 0` does move the button visually, but it removes it from flow, so the card no longer reserves space for it and long text runs underneath. And `vertical-align: bottom` is an inline-level property with no meaning for a flex item at all. ## Quick checklist Row container gives equal heights. Card becomes a column flex container so there is internal free space. One declaration — `margin-top: auto` on the footer or `flex: 1` on the body — spends that space. Then check whether stretch has made your button wider than you intended.

  • What is the practical difference between margin-top: auto on the button and flex: 1 on the body?
    Both pin the button to the card's bottom. With `flex: 1` the body element actually grows, so its background, borders and any internal alignment cover the empty region. With `margin-top: auto` the body keeps its content height and the slack is empty margin. Choose by what should be painted in the gap.
  • The cards wrap onto multiple rows — why aren't all cards in the set the same height?
    Because flexbox equalises per flex line. Each line stretches its items to that line's tallest item, and lines are sized independently, so row two can be shorter than row one. Matching heights across every card is a two-dimensional requirement, which is grid's job — grid row tracks size across the whole track.
  • Why does the button suddenly span the full card width after you make the card a column container?
    Because the card's cross axis is now horizontal and `align-items` still defaults to `stretch`, so children fill the card's width. Set `align-self: flex-start` (or `center`) on the button to let it size to its content, or change `align-items` on the card if you want that for every child.

saying these in an interview costs you the question

  • Using align-self: flex-end to move a button down in a column card
  • Reaching for position: absolute and losing the space the button needs
  • Expecting equal heights across wrapped rows, not just per line
  • Thinking justify-self works on a flex item
  • Setting a fixed min-height on cards to fake alignment

context