Create a Tailwind Mobile Header in Astro With A Svelte Island
Published on: September 22, 2023
Back to the Blog indexWelcome to this series of Tailwind-Astro-Svelte tutorials. We’re going to implement a small Mobile Header based on those technologies. Of course, you may find it easier or more difficult than lots of other approaches out there, but I think it’s interesting to learn new ways to solve common problems. So here we go!
Set up Tailwind in Astro
First, let’s install Tailwind as an Anstro integration with the following command:
npx astro add tailwind
This will generate a tailwind.config.cjs file and the astro.config.mjs also gets updated, including the Tailwind integration:
import { defineConfig } from 'astro/config';
import tailwind from "@astrojs/tailwind";
export default defineConfig({
integrations: [tailwind()]
});
This is all we have to do to get Tailwind working in our Astro installation. As a test, you can use a Tailwind utility class on any part of your pages or components, for example in the index.astro page, like the next example:
---
import Layout from "../layouts/Layout.astro";
import Card from "../components/Card.astro";
---
<Layout title="Welcome to Astro.">
<h1 class="bg-indigo-500">Hey</h1>
<main>
I’ve installed the basic Astro template to start a new project. When you visit http://localhost:4321/, you can see the h1 “Hey” heading with a tailwind bg-indigo-500
Tailwind background color.
Creating the Header Astro Component
Now, let’s focus on the Header Astro Component. We’re going to place this header before the <main>
element of each page.
Let’s go to the Layout.astro
file and after the <body>
opening tag, write a <Header />
tag that will be the component we need to create.
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="description" content="Astro description" />
<meta name="viewport" content="width=device-width" />
<link rel="icon" type="image/svg+xml" href="/favicon.svg" />
<meta name="generator" content={Astro.generator} />
<title>{title}</title>
</head>
<body>
<Header />
<slot />
</body>
</html>
The <slot>
in the Layout.astro
file will allow us, for each Astro page that uses it, to insert the respective content in that Layout container or space.
Of course, there will be an error because we haven’t created the <Header />
component and this is precisely
what we’re going to do. Go to (or first create the folder), to the /src/components
folder, and create a new file called Header.astro
. Write a <div>
tag with anything inside it and save the file.
<div class="text-white bg-indigo-500">Hello Astro</div>
Import the Header component in the Layout file, so you can see that message on the screen.
import Header from "../components/Header.astro";
Just don’t forget to import it inside the frontmatter fences (---) at the top of the file.
Once you get assured you’ve imported your Header component, let’s create the navbar markup and a logo example. It will be a common pattern found in many other Websites. In Header.astro, let’s write:
<header class="bg-indigo-800">
<div class="container mx-auto bg-gray-400">
<div>
<div>Logo</div>
<nav>
<ul>
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
</div>
</div>
</header>
I have added two div
tags: one to be a container
for the header layout and a child one that will help with the positioning of the Logo and navigation. This will result in something like this:
Let’s stack the logo and the navigation using a flex column layout:
<header class="bg-indigo-800">
<div class="container mx-auto bg-gray-400">
<div class="flex flex-col items-center">
<div>Logo</div>
<nav class="">
...
The nav
element will have a with of 100% the width of the viewport, and each link’s text will be centered:
...
<nav class="w-full">
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
...
Adding the Menu and Close buttons
As part of the navigation, we’ll create two buttons we need to open and close the navbar.
Let’s add a div
container with both buttons just before the nav
element, and align it to the right (self-end
)
...
<div class="self-end pt-2 pr-4 bg-violet-200">
<button
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5"
></path>
</svg>
</button>
<button
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</button>
</div>
<nav class="bg-pink-50 w-full flex flex-col">
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
I’m using heroicons for the mobile buttons, but you can choose any other icons you are comfortable working with.
For the moment we have a mobile interface like this:
Of course, this is an initial approach to our problem. The next step is to make the mobile buttons interactive to open and close the menu. It’s time for Svelte to enter the scene.
Showing and Hiding the Menu
As the menu inside the nav
element has to be shown and hidden when clicking the menu and the close buttons, we have to convert them to Svelte components. These components are the islands of interactivity inside Astro, which I believe, is one of the star features of the framework.
Of course, we need to install the Astro Svelte integration with the following command:
npx astro add svelte
Follow the instructions in the command line and the installation is done quickly. You don’t have to do anything else.
After that, copy the nav
HTML markup from the Header.astro
and paste it in a new Menu.svelte
file you have to create inside the components folder. For a better organization, you may create a svelte
folder inside the components one, too. I did exactly that and in the Header
component, I imported the Svelte component, like so:
---
import Menu from "./svelte/Menu.svelte";
---
<header class="bg-indigo-800">
<div class="container mx-auto bg-gray-400">
<div class="flex flex-col items-center">
<div>Logo</div>
<Menu />
</div>
</div>
</header>
The Menu
component should be now, like so:
<nav class="bg-pink-50 w-full flex flex-col">
<div class="self-end pt-2 pr-4">
<button
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M3.75 5.25h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5m-16.5 4.5h16.5"
></path>
</svg>
</button>
<button
><svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke-width="1.5"
stroke="currentColor"
class="w-6 h-6"
>
<path
stroke-linecap="round"
stroke-linejoin="round"
d="M9.75 9.75l4.5 4.5m0-4.5l-4.5 4.5M21 12a9 9 0 11-18 0 9 9 0 0118 0z"
></path>
</svg>
</button>
</div>
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
We’re going to abstract the buttons into their own Svelte components. Create two new files, CloseBtn.svelte and MenuBtn.svelte, and copy the buttons’ markup from the Menu.svelte component.
<script>
import MenuBtn from "./MenuBtn.svelte";
import CloseBtn from "./CloseBtn.svelte";
</script>
<div class="self-end pt-2 pr-4 bg-violet-200">
<MenuBtn />
<CloseBtn />
</div>
<nav class="bg-pink-50 w-full">
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
Notice that as we’re using Svelte inside Menu.svelte
, we import the button components inside a <script></script>
block, not inside ---
fences as we do in Astro components.
If you take a look at your interface update, we have the same result as before we made the Svelte refactoring and we’re good to go for the rest of the implementation.
Adding state to our Menu component
To show and hide the nav
element, we need some sort of data that tells us if it’s closed or opened. This state
data will be reactive and also used by the button components to “toggle” the menu.
So, let’s add that state variable in the Menu.svelte
component like so:
<script>
import MenuBtn from "./MenuBtn.svelte";
import CloseBtn from "./CloseBtn.svelte";
let navIsOpen = true;
</script>
...
Then, the nav
element that includes the menu items should react to this state variable whenever it changes. How do we change its visibility when it reacts to the navIsOpen
variable? We can use CSS for this, activating a class when the change occurs. With Svelte we have a very convenient syntax for this which is class:nameOfClass
. In the nav
element let’s create a “reacting” class like so:
<nav class:navIsOpen class="bg-pink-50 w-full hidden">
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
Notice that the initial state of this navbar is invisible and in CSS we represent this invisibility with the Tailwind utility class hidden
.
But, when the state variable navIsOpen
changes to true
, the navbar has to respond to this change and the way it does is by activating the .navIsOpen
class, using CSS:
<style>
.navIsOpen {
@apply block;
}
</style>
Change manually the variable navIsOpen
, at the top of the file, to true or false to test how the navbar toggles on each change. This is how reactivity works in Svelte.
Don’t forget that the navIsOpen
state variable has the same name as the navbar class used to update its visibility. We can use this naming convention in Svelte, but we can also use another class name if we want, like so:
<nav class:showNav={navIsOpen} class="bg-pink-50 w-full opacity-0">
<ul class="text-center">
<li><a href="/">About</a></li>
<li><a href="/">Products</a></li>
<li><a href="/">Reviews</a></li>
<li><a href="/">Contact</a></li>
</ul>
</nav>
And, in CSS, we’d change the class used to make the navbar visible like this:
.showNav {
@apply block;
}
Implementing the Menu and Close buttons
So far, we can update manually the state of the navbar, but we need to update them whenever the user pushes the Close or Menu buttons, represented in our application as two separate components.
Remember that the navIsOpen
variable is reactive and it can be used to hide or show, not only the navbar but also the buttons at each respective state change. Let’s make it clear implementing it right away
Hide or show the buttons depending on state change
Just to illustrate the reactivity between components, in the MenuBtn component create a prop
called navIsOpen
; the same name as the navbar state variable. Soon we will see how both relate:
//Menu.svelte
<script>
import MenuBtn from "./MenuBtn.svelte";
import CloseBtn from "./CloseBtn.svelte";
let navIsOpen = false;
</script>
<div class="self-end pt-2 pr-4 bg-violet-200">
<MenuBtn {navIsOpen} />
<CloseBtn {navIsOpen} />
</div>
<script>
//MenuBtn.svelte
export let navIsOpen;
</script>
And in the button
opening tag, set a class directive, as we did in the Menu component and a CSS utility class block
<script>
export let navIsOpen;
</script>
<button class:navIsOpen class="block">
...
In the style
block of the component set the navIsOpen
class so that the button is invisible:
<style>
.navIsOpen {
@apply hidden;
}
</style>
This means that when the navigation is hidden, so is the menu button, under the MenuBtn component.
Conversely, we can create the same mechanism in the CloseBtn
component to show it when the navigation is open:
<script>
export let navIsOpen;
</script>
<button class:navIsOpen class="hidden"
...
<style>
.navIsOpen {
@apply block;
}
</style>
Test again setting the navIsOpen
state variable in Menu.svelte
component and see how all components react to it, closing and opening whenever a change is detected.
Implement the buttons’ actions
Of course, the buttons’ actions are to hide and open the navbar. On each of the button components implement the click event that will trigger the expected behavior.
Note: Remember to set the Svelte component Menu.svelte
in the Header.astro
component as a client-only component with the directive: <Menu client:only="svelte"/>
Remember that we don’t need the Svelte components server-rendered.
First things first. Let’s set up the navIsOpen
state variable in Menu.svelte
to false
to implement the click event on the MenuBtn.svelte
component:
//Menu.svelte
...
let navIsOpen = false;
...
Next, let’s set up the event on:click
in the MenuBtn.svelte
component:
<script>
export let navIsOpen;
function toggleNavbar(e) {
console.log(e.currentTarget);
}
</script>
<button class:navIsOpen class="block" on:click={toggleNavbar}
>
The click event handler toggleNavbar
is a function that we need to implement to activate the navigation which at the moment is hidden
. What we did in the previous code snippet is to log the button element.
Forwarding events to activate the navigation
How do we get to activate the navigation from the button component if this is its direct child? We can use Event Forwarding which allows us to dispatch an event so that it can be “listened to” in another component in the Svelte tree, which, in this case, is its parent Menu.svelte
.
To accomplish this, we need to import the Svelte dispatcher called createEventDispatcher()
that we will be called inside the button on:click
handler, like so:
//MenuBtn.svelte
<script>
import { createEventDispatcher } from "svelte";
export let navIsOpen;
const dispatch = createEventDispatcher();
function toggleNavbar(e) {
dispatch("openNavigation");
}
</script>
...
Here we “dispatch” the custom event “openNavigation” in the toggleNavbar
button handler.
In the Menu.svelte
component we’ll listen to that custom event and update the navIsOpen
state variable to true
:
<script>
import MenuBtn from "./MenuBtn.svelte";
import CloseBtn from "./CloseBtn.svelte";
let navIsOpen = false;
function toggleNavbar() {
navIsOpen = !navIsOpen;
}
</script>
<div class="self-end pt-2 pr-4 bg-violet-200">
<MenuBtn {navIsOpen} on:openNavigation={toggleNavbar} />
In fact, what we’re doing is “toggling” the navigation by changing the “truthiness” of the navIsOpen
state variable in the toggleNavbar()
handler of the custom event openNavigation
.
For the CloseBtn
component we have to proceed identically. In the Menu.svelte
component, we will have the following:
<script>
import MenuBtn from "./MenuBtn.svelte";
import CloseBtn from "./CloseBtn.svelte";
let navIsOpen = false;
function toggleNavbar() {
navIsOpen = !navIsOpen;
}
</script>
<div class="self-end pt-2 pr-4 bg-violet-200">
<MenuBtn {navIsOpen} on:openNavigation={toggleNavbar} />
<CloseBtn {navIsOpen} on:openNavigation={toggleNavbar} />
</div>
Finally, we can use the buttons to close and open the navbar! Notice how reactivity plays seamlessly between components for hiding and showing the buttons and the navbar, with just a piece of state defined in the parent component Menu.svelte
. And remember that we’re doing all this as an Astro island!
Awesome!
This way we’re wrapping up. In the following post, we’ll improve the UI implementation of our responsive navbar because as you have noted, there are some elements that are off and not looking well. See you then!