Arkendia Classrooms
Lesson 15 of 21 · 18 min

Positioning, overflow, transitions and simple animations

Your layout is solid. Now for finishing touches: a header that stays in view as visitors scroll, a badge pinned to the corner of a card, a button that responds smoothly when hovered. In this lesson you will learn how CSS positions elements, how stacking and overflow work, and how to add transitions and small animations responsibly.

Normal flow

By default, elements follow the normal flow: block elements stack vertically and inline elements run along lines of text. The position property lets individual elements leave that flow when you need to. Use it sparingly: most layouts should rely on Flexbox and Grid, and positioning is for specific effects.

The position values

ValueBehavior
staticDefault. Normal flow; top, left and so on have no effect.
relativeStays in the flow, may be nudged from its normal place, and becomes a reference point for absolutely positioned children.
absoluteRemoved from the flow; placed relative to the nearest positioned ancestor.
fixedRemoved from the flow; placed relative to the viewport and stays in place when scrolling.
stickyBehaves normally until it reaches a threshold, then sticks while scrolling in its container.

Offsets are set with top, right, bottom and left.

A badge on a card: relative and absolute

The classic combination: make the parent relative and the child absolute.

<article class="project-card">
  <span class="badge">New</span>
  <h3>Street portraits</h3>
</article>
.project-card {
  position: relative;
}
.badge {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
  padding: 0.25rem 0.75rem;
  background-color: #f59e0b;
  color: #1f2937;
  border-radius: 999px;
  font-size: 0.875rem;
}

The badge is pinned 0.75rem from the top right corner of the card, because the card is its nearest positioned ancestor. Without position: relative on the card, the badge would be positioned relative to the page, and end up in a surprising place. An absolutely positioned element takes no space in the layout, so make sure it will not overlap important content.

A sticky header

position: sticky is perfect for a header that stays visible while scrolling:

.site-header {
  position: sticky;
  top: 0;
  background-color: #ffffff;
  z-index: 10;
}

The element scrolls normally until it reaches top: 0, then sticks there. A sticky header must have a background, otherwise content will show through it as it passes beneath. Keep it slim, because a tall fixed header eats precious space on phones.

One more detail: when visitors click an anchor link such as #contact, a sticky header may cover the top of the target section. You can compensate with scroll-margin-top on the targets:

section[id] {
  scroll-margin-top: 5rem;
}

Add scroll-behavior: smooth on html if you want smooth scrolling to anchors, wrapped in a reduced-motion check as we will see below.

Stacking and z-index

When elements overlap, which is in front? By default, later elements in the HTML are drawn above earlier ones, and positioned elements are above non-positioned ones. The z-index property sets the order explicitly. It works only on positioned elements (and on flex and grid items).

.header { position: sticky; top: 0; z-index: 10; }
.modal  { position: fixed; z-index: 100; }

Do not escalate: values like 9999 become a maintenance problem. Use a small, documented scale (for example 10 for headers, 100 for modals). Beware also that z-index values only compete within the same stacking context. Some properties, such as position combined with z-index, opacity below 1, or transform, create a new one, so a child with a huge z-index cannot rise above an element outside its parent’s context.

Overflow

When content is bigger than its box, overflow controls what happens:

  • visible (default): the content spills out.
  • hidden: the excess is clipped.
  • auto: scrollbars appear only when needed.
  • scroll: scrollbars are always shown.
.table-wrapper {
  overflow-x: auto;      /* a wide table scrolls sideways instead of breaking the page */
}
.project-card {
  overflow: hidden;      /* clip the image to the card's rounded corners */
  border-radius: 0.75rem;
}

Use hidden with care: clipped content is invisible, and so is any focus outline that falls outside the box. For long words or URLs that burst out of a narrow column, overflow-wrap: anywhere is often a better fix.

Transitions

By default, a state change such as a hover is instantaneous. A transition animates the change over a short duration.

.button {
  background-color: #0f766e;
  color: #ffffff;
  padding: 0.75rem 1.5rem;
  border-radius: 0.5rem;
  transition: background-color 0.2s ease, transform 0.2s ease;
}
.button:hover,
.button:focus-visible {
  background-color: #115e59;
  transform: translateY(-2px);
}

The transition goes on the base state, so that it works in both directions. It takes the property to animate, a duration and a timing function (ease, ease-in-out, linear). List only what actually changes rather than using all, which can animate things you did not intend.

Transforms

transform moves, scales and rotates an element without disturbing the layout around it:

.card:hover { transform: scale(1.03); }
.icon { transform: rotate(45deg); }
.nudge { transform: translateX(10px); }

Because transforms do not trigger layout recalculations, animating transform and opacity is usually smooth even on modest devices. Animating properties such as width or margin is more expensive.

Keyframe animations

When you need more than a change between two states, define an animation with @keyframes:

@keyframes fade-up {
  from {
    opacity: 0;
    transform: translateY(1rem);
  }
  to {
    opacity: 1;
    transform: translateY(0);
  }
}

.hero h1 {
  animation: fade-up 0.6s ease-out both;
}

The animation shorthand names the keyframes, the duration, the timing and so on. You can add animation-delay, animation-iteration-count (a number or infinite) and animation-fill-mode. The both keyword keeps the first and last states applied outside the animation. You can also use percentage steps (0%, 50%, 100%) for sequences.

Animate with restraint

Motion should serve a purpose: draw attention to a change, confirm an action, guide the eye. Used badly, it distracts or even causes harm. Some visitors experience dizziness or nausea from large movements, parallax and constant animation. Browsers expose a system setting for people who ask for less motion, and you can respect it:

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
    scroll-behavior: auto !important;
  }
}

This is one of the few places where !important is acceptable, since you deliberately want to override everything. Additional guidelines: keep durations short (roughly 150 to 400 milliseconds for interface feedback), avoid anything flashing, and never make essential information depend on an animation.

Tip. Try changing a hover state in the Styles pane of the developer tools: you can force a state such as :hover on an element to edit it live, which is much easier than keeping the mouse in place.

Warning. Do not use display: none or visibility: hidden on something that should be animated in: those properties cannot be transitioned smoothly in the usual way. Animate opacity and transform instead, and take care that hidden things are not left focusable.

Your turn

  1. Make your header sticky with a background and a suitable z-index, and add scroll-margin-top to your sections.
  2. Add a “Featured” badge to one project card using relative and absolute positioning.
  3. Add a transition on the card’s shadow and a slight lift on hover.
  4. Give your hero heading a fade-up animation on load.
  5. Add the reduced-motion media query and check that it disables the effects (you can simulate the preference in the developer tools).
See a solution
.site-header {
  position: sticky;
  top: 0;
  z-index: 10;
  background-color: #ffffff;
}
section[id] { scroll-margin-top: 5rem; }

.project-card {
  position: relative;
  transition: box-shadow 0.2s ease, transform 0.2s ease;
}
.project-card:hover {
  box-shadow: 0 8px 20px rgb(0 0 0 / 18%);
  transform: translateY(-3px);
}
.badge {
  position: absolute;
  top: 0.75rem;
  right: 0.75rem;
}

@keyframes fade-up {
  from { opacity: 0; transform: translateY(1rem); }
  to   { opacity: 1; transform: translateY(0); }
}
.hero h1 { animation: fade-up 0.6s ease-out both; }

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation: none !important;
    transition: none !important;
  }
}

Careful: with overflow: hidden on a card, a badge positioned outside its edge would be clipped, so keep the badge inside.

Key takeaways

  • relative keeps the element in flow and anchors absolute children; absolute places an element against its nearest positioned ancestor; fixed and sticky relate to the viewport and scrolling.
  • z-index orders overlapping positioned elements; use a small scale and remember stacking contexts.
  • overflow controls what happens to content that does not fit; hidden clipping can hide focus outlines.
  • Transitions animate between states; prefer animating transform and opacity.
  • Respect prefers-reduced-motion and use motion with purpose.
Advertisement
Log in to save your progress. Log in