Surface Header
A site header that takes the colour of the section under it: clear over a photo, the section's own colour over a dark fill, the regular header everywhere else.
Default
Scroll inside the frame. The header reads the section under its middle.
Usage
<script setup>
import { SurfaceHeader } from "@/components/ui/surface";
const route = useRoute();
const menuOpen = ref(false);
</script>
<template>
<SurfaceHeader
v-slot="{ tone }"
:initial="route.meta.headerTone"
:disabled="menuOpen"
class="sticky top-0 z-50 h-14 border-b"
>
<Logo :data-mono="tone === 'photo' ? '' : undefined" />
</SurfaceHeader>
</template>Declaring sections
A section says what it is painted with in data-surface: photo, dark or light. Undeclared sections read as light, so a site changes nothing until it declares one.
The nearest declared element under the header wins, so a light card inside a dark section can declare itself light. Page Scrollbar reads the same attribute.
<!-- A full-bleed photo: the header goes clear, with light ink -->
<section data-surface="photo" class="relative -mt-14 h-svh">
<img src="/hero.jpg" alt="" class="absolute inset-0 size-full object-cover" />
</section>
<!-- A dark fill: the header takes this section's colour -->
<section data-surface="dark" class="bg-zinc-950 text-white">...</section>
<!-- Undeclared, or "light": the regular header -->
<section>...</section>First frame
The tone is measured in the browser. For a page whose top section is a photo, pass the first frame in initial so the server renders the clear header too.
<!-- pages/index.vue: the hero is a photo, so the server renders the clear header -->
<script setup>
definePageMeta({ headerTone: "photo" });
</script>When it reads
On scroll and resize, once a frame, and again whenever the page's nodes come or go or the page changes size, so a navigation, late content or an accordion opening under the header all count.
Hit testing goes blind twice: a modal's scroll lock makes the page pointer-events: none until its exit animation ends, and a view transition covers the page with its snapshot. Then the boxes of the declared sections are read instead. They are already the new page's, so the header has its tone before a closing menu finishes and from the first frame of a navigation.
Composable
useSurfaceTone gives any floating element the same reading. Scroll To Top uses it to pick its glass.
<script setup>
import { useSurfaceTone } from "@/components/ui/surface";
// Any element that floats over the page: a floating button, a sticky toolbar.
const fab = ref(null);
const { tone, background } = useSurfaceTone(fab);
</script>API Reference
SurfaceHeader
Accessibility
Keyboard shortcuts and ARIA behavior.
- Only the colours change. Content, order and focus stay the same in every tone.
- The colour change is instant when reduced motion is on.