Skip to content

Fullscreen Mobile Chat Under a Fixed 66px Header

Adityo Guni Waluyo

Fullscreen mobile wrapper under a 66px fixed header using mt-66px plus calc 100dvh-66px, uncapped transcript, and a compact messaging header.

On my phone the chat box just floated in the middle of the page. The header at the top was already fixed, but the wrapper below was still measured from the top of the viewport. Part of it got clipped behind the header, the rest left an awkward empty gap at the bottom. My first guess was that overflow on the wrapper was wrong. It was not.

The root cause is more basic. An element with position: fixed is removed from the normal document flow and creates no space [1], positioned against the initial containing block which for visual media is the viewport [1]. Content below does not automatically know the header height. If the chat wrapper stays at h-screen or a raw 100vh, it still counts the full screen including the area covered by the header. On a small screen the composer that should sit at the bottom just floats mid-page.

To check the viewport side I re-read the viewport concepts page on MDN. It explains that a viewport is the user agent feature that establishes the initial containing block for continuous media [2]. That is why fixed sticks to the viewport, not to its parent. The nuance matters: the layout viewport can differ from the visual viewport when the keyboard appears or during pinch zoom.

Manual offset plus calc for the remaining height

The fix has to be paired: add a top offset equal to the header, then fill the rest with a calculation. In commit 16d5d0c the header in this project is 66px tall (a short title Tanya AI plus a Beta pill on the right), so I changed the mobile wrapper from a floating min-h-[85vh] to mt-[66px] h-[calc(100dvh-66px)] with overflow-hidden. The 66px value is specific to this project, not a universal rule — adjust it to the header you actually use.

The calculation comes from the calc() function that performs calculations for CSS property values [3]. For the viewport unit I use dvh instead of plain vh. Tailwind documents h-dvh as a utility whose height tracks the dynamic viewport that changes as browser UI expands or contracts [4], and arbitrary classes like h-[calc(100dvh-66px)] are supported via the h-[<value>] syntax [4]. On phones dvh is more accurate than vh because the address bar showing or hiding is taken into account.

<div class="order-1 mt-[66px] flex h-[calc(100dvh-66px)] flex-col overflow-hidden lg:hidden">
  <ChatMobileThread turns={turns} ... />
</div>

I also trimmed the header inside: removed the back button and the long greeting line, replaced it with the page title plus the Beta pill on the right. Closer to a messaging app, and it saves height.

Transcript without a hard cap

In the previous iteration the transcript was capped with max-h-[45vh]. Once the wrapper is sized dynamically that cap breaks the scroll chain. Users have to scroll the transcript first, hit the end, then scroll the wrapper. It feels like scrolling twice for one list.

The fix: remove max-h-[45vh] and let the transcript be flex-1 with overflow-y-auto inside a flex-col wrapper whose height is already fixed. Header takes what it needs, transcript takes the rest, composer stays pinned at the bottom. The composer that used to float now sits at the bottom of the screen like in WhatsApp.

<div class="flex min-h-0 flex-1 flex-col">
  <div class="flex shrink-0 items-center gap-2 border-b px-3 py-2">
    <p>Tanya AI</p><span>Beta</span>
  </div>
  <div class="min-h-0 flex-1 overflow-y-auto px-3 py-3">
    {/* bubbles */}
  </div>
</div>

Since the history column is no longer shown as a collapsible section on mobile, the historyOpen state and its back handler were removed from AiChatWorkspace. One state gone, one onBack prop gone, and an unused icon import cleaned up. I prefer deleting over hiding — if the feature does not exist on mobile, the code should not linger as dead weight.

If the header in another project has a different height, the formula stays the same: measure the fixed header, add the same margin-top, then height: calc(100dvh - header). For a h-14 header (56px) that pair is mt-14 and h-[calc(100dvh-3.5rem)]. For a related mobile split where only the layout shell changes while answer logic stays single-sourced, see the mobile chat component that reuses TurnBody.

Sources

Related articles