DEV Community

Devanshu Biswas
Devanshu Biswas

Posted on

The LinkedIn profile header, rebuilt: the banner-and-avatar overlap is one negative margin, a white ring, and object-fit

Open LinkedIn, X, Facebook, GitHub or Notion and the top of every profile is the same object: a wide cover photo with a round avatar sitting over its lower edge, then a name, a headline, a place, and a row of actions. It is one of the most copied layouts on the web. I rebuilt it from scratch — editable in place, with swappable photos and a mobile reflow — and the whole recognisable part comes down to about three honest CSS tricks. Here is the anatomy.

The overlap is one negative margin plus a z-index

This is the signature move, and it is smaller than people expect. Keep the avatar in normal document flow and give it a negative margin-top a little larger than half its height. A 128px avatar with margin-top:-72px climbs up so its top half sits over the photo and its bottom half over the white body. A z-index makes it paint above the banner.

.banner      { height:140px; }              /* the photo strip   */
.avatar-wrap { width:128px; height:128px;   /* fixed circle      */
               margin-top:-72px;            /* pull UP over it   */
               position:relative; z-index:2;/* paint on top      */ }
Enter fullscreen mode Exit fullscreen mode

The big win over absolute positioning is that everything below the avatar — the name, the buttons — simply flows underneath it automatically. No coordinates to maintain, no space to reserve by hand, and it reflows for free at any width.

The white ring is just a border

Sitting a round photo directly on a busy banner looks muddy where they meet. A 4px border in the page's background colour reads as a clean ring that cuts the avatar out of the photo behind it — the same device Twitter and GitHub use. Because the element already has border-radius:50%, a solid border becomes a ring with zero extra work.

.avatar {
  border-radius:50%;
  border:4px solid #fff;                 /* the ring */
  box-shadow:0 2px 8px rgba(15,23,42,.18);
  object-fit:cover;
}
.avatar-wrap.otw .avatar { border-color:#01754f; }  /* #OPEN TO WORK */
Enter fullscreen mode Exit fullscreen mode

Swap the ring colour to green and you have LinkedIn's #OPEN TO WORK state for one line.

object-fit: cover so real photos never stretch

People upload photos of every shape. If you drop one into a fixed box with plain width and height, the browser squashes it. object-fit: cover fills the frame while keeping the image's aspect ratio and cropping the overflow — like background-size: cover but on a real <img>, so it keeps its alt text and stays accessible. Set it on both the banner and the avatar and any picture, portrait or landscape, looks deliberately framed.

Edit in place with contenteditable

Rather than a separate form, the name, headline and location you read are the same nodes you edit. One button flips a boolean, toggles contentEditable on those three elements, and adds an .editing class so CSS can show a dashed outline and reveal the camera chips.

editBtn.addEventListener("click", () => {
  editing = !editing;
  pcard.classList.toggle("editing", editing);
  fields.forEach(el => el.contentEditable = editing ? "true" : "false");
  if (editing) pname.focus();
  render();
});
Enter fullscreen mode Exit fullscreen mode

There is no hidden mirror of the data — the DOM node is the single source of truth for what it says, and an input listener keeps a side-panel read-out honest on every keystroke.

Swap a photo with an object URL, no upload

A camera chip is a button that clicks a hidden <input type="file" accept="image/*">. When a file is chosen, URL.createObjectURL(file) hands you a tiny blob: URL pointing straight at the picked file in memory — no server, no base64 — and you drop it into the <img>'s src. The one responsibility this hands you is cleanup: revoke the previous URL before making a new one so the browser can free it.

bannerFile.addEventListener("change", e => {
  const file = e.target.files[0]; if (!file) return;
  if (lastBannerUrl) URL.revokeObjectURL(lastBannerUrl);   // free the old blob
  lastBannerUrl = URL.createObjectURL(file);               // blob: object URL
  bannerImg.src = lastBannerUrl;
  bannerImg.style.display = "block";
});
Enter fullscreen mode Exit fullscreen mode

Folding onto a phone needs no new markup

The action row is a flex container with flex-wrap, so on a narrow card it wraps on its own. A @media query then does the deliberate shrink: shorter banner, smaller avatar with a reduced negative margin to match, a scaled-down name, and buttons switched to a full-width vertical stack. Same DOM, two layouts.

That's the whole header — a negative margin, a white ring, object-fit: cover, contenteditable text, an object URL, and a flex row that wraps. Learn this one pattern and you can rebuild the top of almost any social product.

Edit it live, drop in a photo, flip to a phone width:
https://dev48v.infy.uk/design/day39-linkedin-profile.html

Top comments (0)