App Headroom for Vaadin Flow - Vaadin Add-on Directory
A Vaadin Flow component that hides the app header when the user scrolls down and restores it when they scroll up — the "headroom" pattern.## Usage
```java
AppHeadroom.applyTo(myAppLayout);
```
`applyTo` is the only entry point: it validates that `myAppLayout` is backed by
the standard `vaadin-app-layout` web component, creates an `AppHeadroom`, and
returns the instance for chaining. This works the same regardless of whether
`myAppLayout` is a plain `AppLayout` or a subclass (e.g. `vaadin-flow-app-nav-layout`'s
`AppNavLayout`) — `AppHeadroom` attaches itself as a peer element alongside the
layout (never as a light-DOM child of it), so it never shows up in
`myAppLayout.getChildren()` and needs no cooperation from whatever the layout
subclass does with its own children.
Call `headroom.remove()` to detach headroom behavior from the layout without
affecting the layout itself.
## Configuration
All setters return `this` for chaining. Call them before or after attaching — they map to HTML attributes read by the web component.
| Method | Default | Effect |
|-----------------------------------|---------|------------------------------------------------------------|
| `setTopOffset(int px)` | 100 | Distance from page top within which chrome is always shown |
| `setHideTolerance(int px)` | 30 | Scroll-down distance required to trigger hide |
| `setShowTolerance(int px)` | 30 | Scroll-up distance required to trigger restore |
| `setTransitionDuration(int ms)` | 600 | Duration of the show/hide slide and padding transitions |
```java
AppHeadroom.applyTo(myAppLayout)
.setTopOffset(64)
.setHideTolerance(10)
.setShowTolerance(10);
```
## Pinning individual bars
A bar that's already pinned to the viewport (`position: fixed`) and shaped
like a vertical rail rather than a horizontal bar (taller than wide) is left
alone automatically — a plain, observable geometry fact, not something
anything has to declare. For cases where that inference isn't right, override
it explicitly:
```java
AppHeadroom.applyTo(myAppLayout).setBottomBarPinned(true);
```
`setTopBarPinned(boolean)` / `setBottomBarPinned(boolean)` take precedence
over the automatic geometry check.
## Server-visible pinned state
```java
var headroom = AppHeadroom.applyTo(myAppLayout);
headroom.addPinnedChangeListener(event -> System.out.println("pinned: " + event.isPinned()));
boolean currentlyPinned = headroom.isPinned();
```
`isPinned()` reflects whether the chrome is currently shown, kept in sync
with the client's own scroll-driven state (also reset to `true` if `headroom`
is detached via `remove()`). `addPinnedChangeListener` notifies on every
change instead of polling.
## Restricting activation by device type/orientation
By default the effect is active on every device. To restrict it, supply a
predicate over the session's detected device type and current orientation —
re-evaluated automatically whenever either becomes known or changes (e.g. on
rotation), with nothing further to wire up:
```java
AppHeadroom.applyTo(myAppLayout).setActivationPredicate((deviceType, orientation) ->
deviceType == AppHeadroom.DeviceType.PHONE
|| (deviceType == AppHeadroom.DeviceType.TABLET && orientation == AppHeadroom.Orientation.LANDSCAPE));
```
Device type (`PHONE` / `TABLET` / `DESKTOP`) is classified from touch
capability and physical screen size (not viewport width, which fluctuates as
a desktop user resizes their browser window). `isActive()` reflects the
predicate's last evaluation. The screen-size threshold that separates
`TABLET` from `PHONE` (default `768`px, the shorter physical screen side)
is itself overridable — must be called before the layout attaches, since
device type is only ever detected once:
```java
AppHeadroom.applyTo(myAppLayout).setTabletMinShortSidePx(600);
```
The landscape-mode bottom bar's stacking order is a plain CSS custom
property, not a Java API — set `--headroom-landscape-bottom-bar-z-index`
directly on your `vaadin-app-layout` (default `200`) if it needs to sit
above or below other fixed-position chrome in your app.
## How it works
- Always shown within the first `topOffset` px from the top of the page (default: **100 px**)
- Hides after the user scrolls down more than `hideTolerance` px past a high-water mark (default: **30 px**)
- Restores after the user scrolls up more than `showTolerance` px from the hidden position (default: **30 px**)
- Guards against bottom overscroll/bounce on iOS causing a false restore
- Animates with `transform: translateY(±100%)` and a 600 ms ease transition — no layout shifts, no JavaScript-driven height recalculations
The component is a LitElement web component (``) backed by `AppHeadroom.java`. It injects global CSS that enables body-scrolling on touch devices and animates `vaadin-app-layout`'s internal `::part(navbar-top)` and `::part(navbar-bottom)` slots.
## Credits
The "headroom" name and hide-on-scroll-down/show-on-scroll-up interaction
pattern originate from [Headroom.js](https://github.com/WickyNilliams/headroom.js)
by Nick Williams (MIT License). This is an independent implementation built
for Vaadin Flow/Lit — it doesn't use any of that project's code — but the
pattern and name are its idea.
View on GitHub