Display, flow and positioning
Before Flexbox and Grid there is normal flow: what the browser does with no layout instructions at all. Most of a good layout is normal flow left alone, and knowing what it does for free stops you writing CSS to recreate it.
Normal flow
Two rules, and they explain most of what you see on an unstyled page.
Block elements stack vertically and fill the available width. A div, p,
h1 or section takes the full width of its parent and sits below the previous
one, however little content it has.
Inline elements flow along the line and wrap like words. A span, a, em or
img sits in the text, and when the line runs out it continues on the next.
That is why a page of paragraphs is already a reasonable layout, and it is why widths come free and heights do not: a block's width is given to it by its parent, while its height comes from its content. The box-model lesson's warning about fixed heights follows from this.
The display values that matter
display: block; /* stacks, fills the width */
display: inline; /* flows in text; ignores width, height, vertical margin */
display: inline-block; /* flows in text, but respects width, height and padding */
display: flex; /* children become flex items — one dimension */
display: grid; /* children become grid items — two dimensions */
display: inline-flex; /* a flex container that sits in a line of text */
display: none; /* removed; no space kept, hidden from screen readers */
display: contents; /* the box disappears; children join the parent's layout */
Note what display does: it sets how the element lays out its children and how
it behaves in its parent. display: flex makes the element itself a block-level
box whose children are flex items.
The two inline gotchas
width and height do nothing on display: inline. Set width: 200px on a
<span> and nothing happens. So does vertical margin. Padding applies but does not
push neighbours apart vertically — it overlaps them.
Inline elements are separated by the whitespace in your HTML. This:
<span class="chip">Atta</span>
<span class="chip">Rice</span>
has a gap between the chips that you did not ask for, because the newline between the tags is a space, and a space is content. It is roughly a quarter of the font size and it changes with the font.
For years people fought this with font-size: 0 on the parent, or by removing the
newlines, or with HTML comments between tags. Do not. Use Flexbox — a flex
container ignores whitespace between its children entirely. That gap disappearing is
one of the quiet reasons Flexbox felt like such a relief.
display: contents, briefly
.wrapper { display: contents; }
The wrapper's own box vanishes and its children become direct children of the
grandparent's layout. Useful when a wrapper exists for a component's sake but is in
the way of a Grid. It has an accessibility caveat — in some browsers it has removed
the element's semantics, so avoid it on a <ul>, a <table> or anything whose role
matters.
Positioning
position changes where a box sits relative to normal flow. Five values, and only
three are common.
static
The default. The box is in normal flow, and top/right/bottom/left and
z-index are ignored. This is worth knowing because "my top: 20px does
nothing" is nearly always a box that is still static.
relative
.badge-holder {
position: relative;
top: 10px;
}
The box stays in flow — it keeps its original space — and is then shifted visually. So the gap it left behind remains, and it can overlap its neighbour.
That is rarely what you want on its own. The real reason to use position: relative
is as an anchor: an absolutely positioned child is placed relative to the
nearest positioned ancestor.
.card { position: relative; } /* the anchor, with no offsets */
.card .badge { position: absolute; top: 8px; right: 8px; }
position: relative with no offsets is one of the most common declarations in CSS,
and it exists purely to establish that containing block.
absolute
.badge {
position: absolute;
top: 8px;
right: 8px;
}
The box is removed from flow. Its space collapses, its siblings close up as if it were not there, and it no longer affects the height of its parent — which is why a container of only absolutely positioned children has zero height.
It is placed relative to the nearest ancestor with position other than static.
If there is none, it is placed relative to the page, and your badge ends up in the
top-right corner of the document rather than of the card. That is the single most
common absolute-positioning bug, and the fix is position: relative on the
parent.
Using inset is shorter for all four:
.overlay { position: absolute; inset: 0; } /* fill the parent exactly */
fixed
.header {
position: fixed;
inset-block-start: 0;
inset-inline: 0;
}
Removed from flow and positioned relative to the viewport, so it stays put while the page scrolls.
Two things to know. It leaves no space, so the content beneath it slides underneath —
you need padding-top on the body equal to the header's height, or a spacer.
And a transform or filter on an ancestor makes fixed behave like absolute,
which is a genuinely baffling bug when it happens.
A fixed header on a phone costs you a meaningful share of a small screen. Consider whether it earns it.
sticky
.section-heading {
position: sticky;
top: 0;
background: white;
}
The useful one, and the one with the most gotchas. A sticky element behaves as
relative until it would scroll past its offset, then behaves as fixed — but
only within its parent. Scroll past the parent and the sticky element leaves with
it.
Three reasons sticky silently does nothing:
- No offset.
position: stickywith notop,bottom,leftorrightnever sticks. There is nothing to stick to. - An ancestor has
overflow: hidden,autoorscroll. Sticky positions against the nearest scrolling ancestor, and that ancestor becomes the scroll container. This is the commonest cause, and theoverflowis usually on something three levels up that somebody added for an unrelated reason. - The parent is not taller than the sticky element. There is no room to stick within.
It also needs a background. Without one, the page's content scrolls visibly behind your sticky heading.
Floats
.thumbnail { float: left; margin-right: 1rem; }
float was the layout tool for fifteen years, and you will find it in every older
codebase. Its problem was that a floated element is taken out of flow for height
purposes, so its parent collapses to zero height — which is what the "clearfix" hack
existed to fix.
Do not use floats for layout. You have Flexbox and Grid.
Do use float for the one thing it was designed for: wrapping text around an
image. That is genuinely still the only way to do it, and it works perfectly:
.article img.inset {
float: left;
margin: 0 1rem 0.5rem 0;
max-width: 40%;
}
Choosing
A short decision list, because the tools overlap:
| Need | Use |
|---|---|
| Things stacked down the page | normal flow — write nothing |
| Text wrapping around an image | float |
| A row or column of items | Flexbox |
| Rows and columns aligned together | Grid |
| A badge in a card's corner | absolute inside a relative parent |
| A header that follows the scroll | sticky (or fixed, if it must always show) |
| An overlay covering its parent | absolute with inset: 0 |
| A full-screen modal | the <dialog> element from module 1 |
The first row is the one people forget. A stack of sections with margins between them needs no layout CSS at all.
Check your work
The two rules of normal flow. Blocks stack and fill the width; inlines flow and wrap.
Why widths come free and heights do not. Width is given by the parent; height comes from the content.
What does nothing on an inline element. width, height, and vertical margin.
Where the mystery gap between inline items comes from. The whitespace in your HTML is a space, and a space is content. Flexbox ignores it.
Why top: 20px sometimes does nothing. The element is still position: static.
What position: relative is really for. Being the anchor for an absolutely
positioned child.
What absolute does to the parent's height. Nothing — the child is out of flow,
so a parent of only absolute children has no height.
The commonest absolute-positioning bug. No positioned ancestor, so it anchors to the page.
What breaks position: fixed. A transform or filter on an ancestor makes it
behave like absolute.
Three reasons sticky does nothing. No offset, an ancestor with overflow set,
or a parent no taller than the element.
What floats are still for. Wrapping text around an image.
Practice
- Write five
divs with no CSS. Describe their layout in one sentence. - Set
width: 200pxandmargin-top: 40pxon a<span>. Theninline-block. - Put two inline-block chips on separate lines of HTML and measure the gap. Remove the newline, then instead make the parent a flex container.
- Set
top: 20pxon a static element. Then addposition: relative. - Put a badge in a card's corner with
absoluteand norelativeparent. Find where it goes. - Add
position: relativeto the card and watch it move. - Put three absolutely positioned children in a container and measure the container's height.
- Use
position: absolute; inset: 0to make an overlay. - Build a fixed header and scroll. Note what happens to the first paragraph, then fix it.
- Add
transform: translateZ(0)to an ancestor of your fixed header and watch it break. - Make a sticky heading with no
top. Then addtop: 0. - Add
overflow: hiddento a grandparent of a working sticky element. - Float an image and wrap text around it. Then measure the parent's height with and without a float.
- List the layout method you would use for each row of the table above, from memory.
Official documentation
- MDN — Block and inline layout in normal flow — What the browser does before you write any layout CSS.
- MDN — position — All five values, with the containing-block rules for
absolute. - MDN — Stacking context — Read this before the z-index lesson later in this module.
Next: Flexbox.
Stuck on this lesson?
Being stuck is part of it — but being stuck alone for three days is not. Our internship programme pairs this curriculum with code review and one-to-one help from working developers, and it is free.
About the internship