Svelte Shy Header: Peekaboo Sticky Header with CSS

Rodney Lab - Aug 10 '22 - - Dev Community

☺️ Svelte Shy Header: What on Earth is a Shy Header?

I wanted a Svelte shy header solution to add to a site I was working on and came across a Tweet on a solution by Jake Archibald and Robert Flack from Google’s Chromium team. I needed the solution for a demo site; adding some text in the header to describe the setup a little. As an extra feature to improve user experience (UX), I wanted the header to re-appear if the user scrolled upwards again. That is even if they were already halfway down the page. The idea being to save them having to scroll all the way back up to the top to see site setup blurb.

I saw no need to use a library as the implementation was fantastic. Jake and Robert’s solution leaned heavily on modern CSS, with a I wondered how much I could Sveltify it though. For example, make use of Svelte style directives, element dimension bindings and what not. Basically a kitchen sink, all-out Svelte approach.

If you are new to Svelte you might find this post useful as a showcase of some of Svelte’s templating and styling features. If you have more experience, you probably have some ideas on how to make it even Sveltier! Reach out in a comment below if you do.

Original HTML / CSS / JavaScript Solution

Jake’s full solution is on CodePen and uses plain HTML, CSS and JavaScript. Here is a summary:

<div class="header-shifter"></div>
<div class="header">Header</div>
<p>First paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>
<p>Paragraph</p>
Enter fullscreen mode Exit fullscreen mode
body {
  margin: 0;
}

.header {
  position: relative;
  background: blue;
  color: white;
  padding: 20px;
}
Enter fullscreen mode Exit fullscreen mode
// This is adaptation of https://jsbin.com/mamisar/edit?html,css,js,output
// By https://twitter.com/flackrw

const header = document.querySelector('.header');
const headerShifter = document.querySelector('.header-shifter');

header.style.position = 'sticky';
header.style.top = 'calc(var(--computed-height) * -1 - 1px)';
header.style.bottom = 'calc(100% - var(--computed-height))';

function fixHeaderoffset() {
  const { height } = header.getBoundingClientRect();
  header.style.setProperty('--computed-height', height + 'px');

  const y = Math.min(
    header.offsetTop, 
    document.documentElement.scrollHeight - innerHeight - height
  );
  headerShifter.style.height = y + 'px';
  header.style.marginBottom = -y + 'px';
}

addEventListener('scroll', () => fixHeaderoffset());
addEventListener('resize', () => fixHeaderoffset());
fixHeaderoffset();
Enter fullscreen mode Exit fullscreen mode

🔫 Svelte Shy Header: Sveltification Plan of Action

Here’s my initial thinking on the Sveltification process:

  • you can easily bind a JavaScript variable to an element’s height with Svelte’s dimension bindings,
  • Svelte has some nice syntactic sugar for adding event listeners. We will try to work these in for the scroll and resize events,
  • there’s also a Svelte way to bind an element to a JavaScript variable. This will simplify any element style updates we keep in the JavaScript code. We will try to move some of them down to the element using style directives.

Enough talk, time for some action.

🧱 Svelte Shy Header: Code Along

If you are new to Svelte, hopefully this little project can provide a gentle introduction. Here’s how you can spin up a new project. Start by running these commands in the Terminal:

pnpm create svelte@next svelte-shy-header
cd svelte-shy-header
pnpm install
pnpm install @fontsource/playfair-display # installs a font used later for self-hosting
pnpm dev
Enter fullscreen mode Exit fullscreen mode

You will get some options, choose a Skeleton project and whatever you like for the others. The last command will start up a dev server and spit out a URL you can paste into you browser. We will only create or change two files so just paste the code in when we get to that point. Also make sure you play with, breaking and repairing stuff, to understand better what it is doing!

🏠 Home Page

I thought why not throw in a background CSS pattern? I opted for customising the Colorful Stingrays pattern from www.svgbackgrounds.com. Here is the full code for the home page (replace existing content in src/routes/index.svelte if you are coding along):

<script>
    import Header from '$lib/components/Header.svelte';
    import '@fontsource/playfair-display/latin.css';
</script>

<Header />
<main class="container" />

<style>
    :global(body) {
        margin: 0;
        font-family: Playfair Display;
    }
    :global(:after, :before) {
        box-sizing: border-box;
    }
    :global(:root) {
        --font-size-6: 3.052rem;

        --font-weight-bold: 700;

        --max-width-full: 100%;
        --spacing-6: 1.5rem;

        --colour-brand: hsl(19 97% 51%);
        --colour-dark: hsl(345 6% 13%);
    }
    .container {
        height: 500vh;
        width: var(--max-width-full);

        /* CREDIT: https://www.svgbackgrounds.com/ */
        background-color: #3d315b;
        background-image: url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='600' height='100' viewBox='0 0 600 100'%3E%3Cg stroke='%23FFF' stroke-width='0' stroke-miterlimit='10' %3E%3Ccircle fill='%23FB5607' cx='0' cy='0' r='50'/%3E%3Ccircle fill='%23FC7839' cx='100' cy='0' r='50'/%3E%3Ccircle fill='%23F92A82' cx='200' cy='0' r='50'/%3E%3Ccircle fill='%23F9EA92' cx='300' cy='0' r='50'/%3E%3Ccircle fill='%23F4DA52' cx='400' cy='0' r='50'/%3E%3Ccircle fill='%23EFCA08' cx='500' cy='0' r='50'/%3E%3Ccircle fill='%23FB5607' cx='600' cy='0' r='50'/%3E%3Ccircle cx='-50' cy='50' r='50'/%3E%3Ccircle fill='%23fc6824' cx='50' cy='50' r='50'/%3E%3Ccircle fill='%23ff505a' cx='150' cy='50' r='50'/%3E%3Ccircle fill='%233D315B' cx='250' cy='50' r='50'/%3E%3Ccircle fill='%23f6e273' cx='350' cy='50' r='50'/%3E%3Ccircle fill='%23f1d237' cx='450' cy='50' r='50'/%3E%3Ccircle fill='%23fa9500' cx='550' cy='50' r='50'/%3E%3Ccircle cx='650' cy='50' r='50'/%3E%3Ccircle fill='%23FB5607' cx='0' cy='100' r='50'/%3E%3Ccircle fill='%23FC7839' cx='100' cy='100' r='50'/%3E%3Ccircle fill='%23F92A82' cx='200' cy='100' r='50'/%3E%3Ccircle fill='%23F9EA92' cx='300' cy='100' r='50'/%3E%3Ccircle fill='%23F4DA52' cx='400' cy='100' r='50'/%3E%3Ccircle fill='%23EFCA08' cx='500' cy='100' r='50'/%3E%3Ccircle fill='%23FB5607' cx='600' cy='100' r='50'/%3E%3Ccircle cx='50' cy='150' r='50'/%3E%3Ccircle cx='150' cy='150' r='50'/%3E%3Ccircle cx='250' cy='150' r='50'/%3E%3Ccircle cx='350' cy='150' r='50'/%3E%3Ccircle cx='450' cy='150' r='50'/%3E%3Ccircle cx='550' cy='150' r='50'/%3E%3C/g%3E%3C/svg%3E");
    }
</style>
Enter fullscreen mode Exit fullscreen mode

This is a typical Svelte file with three sections: script tag where we run the JavaScript Code, the Svelte markup, then the style tag. We place the Header in its own component file (imported in line 2). This makes it easier to recycle it for use on other pages. On top you can easily create a Svelte component library, letting you share the code between your other Svelte projects.

🙈 Svelte Shy Header Component

If coding along, create a src/lib/components/Header.svelte file (you will need to create a couple of new folders). That is the file we look at now. This file will eventually have the three sections we have in the home page, but we will start with the middle section:

<svelte:window on:scroll={fixHeaderOffset} on:resize={fixHeaderOffset} />

<div style:height="{headerShifterHeight}px" class="header-shifter" />
<div
    bind:this={header}
    bind:clientHeight={headerHeight}
    style:margin-bottom="-{headerShifterHeight}px"
    class="header"
>
    <header class="wrapper">☺️ Svelte Shy Header 🙈</header>
</div>
Enter fullscreen mode Exit fullscreen mode

In line 1, we add the event listeners for scroll and resize events, but the Svelte way! fixHeaderOffset is the function we will invoke on these events. We will define it later.

Next in like 3, we have the header-shifter div. We keep the original class name but also add:

style:height="{headerShifterHeight}px"
Enter fullscreen mode Exit fullscreen mode

As you might guess, we are setting the element’s height here, using CSS. This is a Svelte style directive. In place of height you can have any regular CSS property of even a custom CSS property (often called CSS variable). headerShifterHeight is a number variable which we will define in the script tag. The px is just to add expected units for valid CSS. An alternative would have been to write something like this, making use of the element’s style attribute and JavaScript template strings:

<!-- LESS OPTIMAL -->
<div style={`height=${headerShifterHeight}px`} class="header-shifter" />
Enter fullscreen mode Exit fullscreen mode

It is Svelte best practice to avoid this, just because, using the original code, the Svelte compiler optimises the output. It will only update styles on the element if headerShifterHeight changes.

header Element

Let’s see the header div now (lines 49 above). Here it is again:

<div
    bind:this={header}
    bind:clientHeight={headerHeight}
    style:margin-bottom="-{headerShifterHeight}px"
    class="header"
 >
Enter fullscreen mode Exit fullscreen mode

We just saw a style directive and the class attribute is nothing new. That leaves the two bind statements. The first bind:this={header} is another Sveltism. This lets us bind the element itself to the JavaScript variable header. That saves us writing out a query selector when we get to writing the script tag. It’s just syntactic sugar making the code a little cleaner.

The second bind directive it where we link the header’s height to the headerHeight JavaScript variable. This will save us calling getBoundingClientRect later in the script tag.

💄 Svelte Style

Paste this code in if you are coding along

<style>
    .header-shifter {
        background-color: var(--colour-dark);
    }
    .header {
        --_computed-height: var(--computed-height, 165px);
        position: relative;
        background: var(--colour-dark);
        color: var(--colour-brand);
        padding: var(--spacing-6);
    }
    .wrapper {
        width: var(--max-width-full);
        margin: var(--spacing-6) auto;
        max-width: var(--max-width-full);
        font-size: var(--font-size-6);
        font-weight: var(--font-weight-bold);
        text-align: center;
    }
</style>
Enter fullscreen mode Exit fullscreen mode

We are adding a touch more style vs. the original version, but not really changing anything fundamentally. The main change is the --_computed-height custom CSS property in line 18. The CSS custom property spec lets us specify a fallback value as a second argument. CSS will use this, second, value if --computed-height is not set. You will notice we do not set it anywhere here in the CSS. As in the original solution we set it using JavaScript. However, if the user has JavaScript disabled in their browser, we can rely on the fallback value we provide here. This is not strictly necessary (the property is only used in styles added by JavaScript), but provides nice demo of how you can define defaults. Props to Geoff Rich for this technique.

🐇 Svelte Shy Header: Java Script

Finally, we can see the much trailed script tag! Once again, paste this in (right at the top of src/lib/components/Header.svelte) if you are coding along.

<script lang="ts">
    /* This is code is based on an adaptation by https://twitter.com/jaffathecake of
    https://jsbin.com/mamisar/edit?html,css,js,output By https://twitter.com/flackrw 
    */

    import { onMount } from 'svelte';

    let header: HTMLDivElement;
    let headerHeight: number;
    let headerShifterHeight = 0;

    function fixHeaderOffset() {
        header.style.setProperty('--computed-height', `${headerHeight}px`);

        headerShifterHeight =
            Math.min(
                header.offsetTop,
                document.documentElement.scrollHeight - window.innerHeight - headerHeight,
            ) - 1;
    }

    onMount(() => {
        header.style.position = 'sticky';
        header.style.top = 'calc(var(--_computed-height) * -1 - 1px)';
        header.style.bottom = 'calc(100% - var(--_computed-height))';
        fixHeaderOffset();
    });
</script>
Enter fullscreen mode Exit fullscreen mode

I should have mentioned I am using TypeScript. Drop the lang=ts and type annotations (:HTMLDivElement & :number) in lines 13, 20 and 21 if you opted for JavaScript instead.

Let’s see what we have. In lines 8 and 9 we declare the variables which we bound in the template code. Then in line 10 we declare and initialise the headerShifterHeight which we also used in the markup. Just by using let here, we make the variable reactive. This means when it changes, Svelte will automatically update the style directives (as well as anything else linked) we added earlier.

The fixHeaderOffset function in the original implementation remains, though we have a few differences. We got rid of the getBoundingClientRect call to get the header height as we now have the value bound to headerHeight by Svelte. We renamed y to headerShifterHeight, just so the template code is a little clearer. Then, remember we use style directives to set the height on the header-shifter element and bottom margin on the header element, so removed those two lines.

SvelteonMount

Finally, all we have left is onMount. This is a Svelte lifecycle method. An alternative it to use Svelte actions. We explore both in the tutorial on creating a Svelte video blog, where we use these in lazy loading video and images.

We set the CSS style props here, instead of using style directives just because, we will not need them defined if the user has JavaScript disabled. In this instance, we want a regular postion: relative header, rather than a sticky one managed in JavaScript. Since the onMount function will not load with JavaScript disabled, the CSS override here on header.style.position is only set with JavaScript enabled.

The top and bottom CSS properties are only relevant when JavaScript is enabled (we use relative positioning otherwise). Taking that into account we set those two properties in onMount rather than using style directives. Note how we use --_computed-height instead of --computed-height. This is because, as we mentioned above, the former will have a default value, available before we first calculate and set --computed-height.

Finally, we put the fixHeaderOffset call in onMount because we need to access the bound JavaScript variables (header and headerHeight) when the function is invoked. These are only available once we mount the component. So placing the initial fixHeaderOffset call here avoids calling it when those variable are still undefined.

🙌🏽 Svelte Shy Header: Wrapup

In this Svelte shy header post. We saw some useful Svelte methods and how they relate to those you might see in plain HTML / CSS / JavaScript sites. In particular we have touched on:

  • how to use Svelte style directives for component styling and when another approach might be preferred,
  • full code for implementing a Svelte shy header as a reusable component,
  • how to bind component dimensions to JavaScript variables with Svelte.

As a next step you might consider following one of the tutorials to learn more about Svelte. If you already know the basics, also try adding this stick header to a Svelte component library. You might also want to take CSS custom properties to the next level, setting them in Svelte actions. Let me know what you end up creating. You can drop a comment below or reach out for a chat on Element as well as Twitter @mention

You can see the full code for this Svelte shy header project, in all its glory in the Rodney Lab Git Hub repo.

🙏🏽 Svelte Shy Header: Feedback

If you have found this post useful, see links below for further related content on this site. I do hope you learned one new thing from the video. Let me know if there are any ways I can improve on it. I hope you will use the code or starter in your own projects. Be sure to share your work on Twitter, giving me a mention so I can see what you did. Finally be sure to let me know ideas for other short videos you would like to see. Read on to find ways to get in touch, further below. If you have found this post useful, even though you can only afford even a tiny contribution, please consider supporting me through Buy me a Coffee.

Finally, feel free to share the post on your social media accounts for all your followers who will find it useful. As well as leaving a comment below, you can get in touch via @askRodney on Twitter and also askRodney on Telegram. Also, see further ways to get in touch with Rodney Lab. I post regularly on SvelteKit as well as Search Engine Optimisation among other topics. Also subscribe to the newsletter to keep up-to-date with our latest projects.

. . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .