Index āŗ 13. Layout components
Learn how to create layouts that are used by multiple pages in your app, either by using slots directly or by using the higher-level abstraction of layout components.
Topics covered
- Using slots for layout.
- Layout components.
- Passing CSS class lists to components and fragments.
- Named slots.
Life without layout components
Slots become especially useful for layout.
Imagine you have a basic site with three pages: the home page, an about page, and a contact page. For consistency, the pages should share the same header, navigation, and footer.
First, letās take a look at how weād achieve this without using layout components and slots:
pages.script.js
// The data model for our site.
export const [ HOME, ABOUT, CONTACT_ME ] = ['home', 'about', 'contact-me']
export const pages = {
[HOME]: { title: 'Home', link: '/' },
[ABOUT]: { title: 'About', link: '/about' },
[CONTACT_ME]: { title: 'Contact me', link: '/contact-me' }
}
Header.component.js
import { pages } from './pages.script.js'
import Navigation from './Navigation.component.js'
export default ({ pageId }) => kitten.html`
<header class='Header'>
<${Navigation} pageId=${pageId} class='navigation'/>
<h1>${pages[pageId].title}</h1>
</header>
<style>
.Header { border-bottom: 1px solid gray; }
.Header .navigation {
color: white;
padding: 1em;
background-color: cadetblue;
}
</style>
`
Navigation.component.js
import { pages } from './pages.script.js'
export default ({ pageId, CLASS }) => kitten.html`
<nav class='Navigation ${CLASS}'>
<ul>
${Object.entries(pages).map(([__pageId, page]) => kitten.html`
<li>
<if ${ __pageId === pageId }>
<span class='currentPage'>${page.title}</a>
<else>
<a href='${page.link}'>${page.title}</a>
</if>
</li>
`)}
</ul>
</nav>
<style>
.Navigation { background-color: red; }
.Navigation ul { list-style-type: none; display: flex; padding: 0; }
.Navigation li:not(:first-of-type) { margin-left: 1em; }
.Navigation a { color: white; }
</style>
`
index.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { HOME } from './pages.script.js'
export default () => kitten.html`
<${Header} pageId=${HOME} />
<main>
<markdown>
## Welcome to my home page!
There are many home pages but this one is mine.
I hope you enjoy it.
</markdown>
</main>
<${Footer} />
`
about.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { ABOUT } from './pages.script.js'
export default () => kitten.html`
<${Header} pageId=${ABOUT} />
<main>
<markdown>
## Hey, look, itās me!
Information about me.
</markdown>
</main>
<${Footer} />
`
contact-me.page.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
import { CONTACT_ME } from './pages.script.js'
export default () => kitten.html`
<${Header} pageId=${CONTACT_ME} />
<main>
<markdown>
## Get in touch!
Normally, thereād be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
</markdown>
</main>
<${Footer} />
`
So while this works, you probably see a couple of problem areas:
- While weāve encapsulated the navigation component in the header component, we still have to manually add the header and footer components to every page. Thatās error-prone redundancy that we should refactor out.
- Where would we put styles that we want to affect all our pages? We donāt really have a place for that.
Enter layout components.
Better living through layout components
Letās start our refactor by pulling out the common page structure into a layout component:
Site.layout.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
export default ({ pageId, SLOT }) => kitten.html`
<${Header} pageId=${pageId} />
<main>
${SLOT}
</main>
<${Footer} />
`
š”Kitten supports a separate extension, layout.js, which is just an alias for component.js, if you want to make it clear that youāre using a component for layout.
Now that we have a layout component with a slot for our content, letās refactor the pages to use it:
index.page.js
import Site from './Site.layout.js'
import { HOME } from './pages.script.js'
export default () => kitten.html`
<${Site} pageId=${HOME}>
<markdown>
## Welcome to my home page!
There are many home pages but this one is mine.
I hope you enjoy it.
</markdown>
</>
`
about.page.js
import Site from './Site.layout.js'
import { ABOUT } from './pages.script.js'
export default () => kitten.html`
<${Site} pageId=${ABOUT}>
<markdown>
## Hey, look, itās me!
Information about me.
</markdown>
</>
`
contact-me.page.js
import Site from './Site.layout.js'
import { CONTACT_ME } from './pages.script.js'
export default () => kitten.html`
<${Site} pageId=${CONTACT_ME}>
<markdown>
## Get in touch!
Normally, thereād be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
</markdown>
</>
`
Well, thatās a bit nicer.
š”
${SLOT}
is shorthand for${SLOT.default}
. As youāll see later, slots also support named slots andSLOT.default
is a special slot that stacks all slotted content that isnāt addressed to a specific named slot.
Now, if we want to add global styles, the layout component becomes a natural place to do it:
import Header from './Header.component.js'
import Footer from './Footer.component.js'
export default ({ pageId, SLOT }) => kitten.html`
<${Header} pageId=${pageId} />
<main>
${SLOT}
</main>
<${Footer} />
<style>
body {
font-family: system-ui, sans-serif;
padding: 1em;
}
</style>
`
But what if we wanted to style the header or footer component?
Passing CSS class lists to components
In Kitten, you can declare a class (or class list) on a custom component just like you can on any other HTML element, using the class
attribute.
So if we want to change all the text in the header so that it displays in small caps and add a bit of top margin between the footer and the rest of our page, we can do that like this:
import Header from './Header.component.js'
import Footer from './Footer.component.js'
export default ({ pageId, SLOT }) => kitten.html`
<${Header} pageId=${pageId} class='header' />
<main>
${SLOT}
</main>
<${Footer} class='footer' />
<style>
body {
font-family: system-ui, sans-serif;
padding: 1em;
}
.header {
font-variant: small-caps;
}
.footer {
margin-top: 2em;
}
</style>
`
However, to make it work, we also have to modify the header and footer components to add the class weāve specified to the class attribute of their root element.
Kitten passes the class attribute as a special CLASS
property to your component. All magic Kitten properties are in UPPERCASE to differentiate them from the properties you declare and use while authoring and to make them stand out.
Here are the relevant parts of the header and footer components after the changes have been made:
header.component.js
export default ({ pageId, CLASS }) => kitten.html`
<header class='Header ${CLASS}'>
<${Navigation} pageId=${pageId} class='navigation'/>
<h1>${pages[pageId].title}</h1>
</header>
ā¦
`
footer.component.js
export default ({ pageId, CLASS }) => kitten.html`
<header class='Header ${CLASS}'>
<${Navigation} pageId=${pageId} class='navigation'/>
<h1>${pages[pageId].title}</h1>
</header>
`
š The idiom in Kitten is to pass CSS classes to components and have them apply to the componentās root element. Having a root element for components is also a Kitten idiom. However, you can have components without a root element (with multiple sibling elements) and, if you really want to, you can apply the passed class list to any element or to multiple elements. It just might get a bit harder to follow whatās happening and to maintain your code if you do.
š” You can see this project in the layout folder under examples.
To round out the layout example, letās see how youāre not limited to just the one default slot.
Using named slots, you can have as many as you like.
Named slots
Say we wanted pages to be able to add or change elements in the heading and the footer.
We can do that using named slots.
Site.layout.js
import Header from './Header.component.js'
import Footer from './Footer.component.js'
export default ({ pageId, SLOT }) => kitten.html`
<${Header} pageId=${pageId} class='header'>
${SLOT.header}
</>
<main>
${SLOT}
</main>
<${Footer} class='footer'>
${SLOT.footer}
</>
<style>
body {
font-family: system-ui, sans-serif;
padding: 1em;
}
.header {
font-variant: small-caps;
}
.footer {
margin-top: 2em;
}
</style>
`
Note that weāve added two named slots, header
and footer
and referenced them via the SLOT
property as SLOT.header
and SLOT.footer
.
Also note that weāre actually placing these as default slots in the header and footer components.
The Site layoutās default slot remains unchanged.
To make this work, letās implement slot support in the header and footer components.
Header.component.js
import { pages } from './pages.script.js'
import Navigation from './Navigation.component.js'
export default ({ pageId, SLOT, CLASS }) => kitten.html`
<header class='Header ${CLASS}'>
<${Navigation} pageId=${pageId} class='navigation'/>
<h1>${pages[pageId].title}</h1>
${SLOT}
</header>
<style>
.Header {
border-bottom: 1px solid gray;
}
.Header .navigation {
color: white;
padding: 1em;
background-color: cadetblue;
}
</style>
`
Footer.component.js
export default ({ SLOT, CLASS }) => kitten.html`
<footer class='Footer ${CLASS}'>
${SLOT}
<markdown>
Copyright (c) 2023-present, Me.
The content on this site is released under [Attribution-NonCommercial-ShareAlike 4.0 International (CC BY-NC-SA 4.0)](https://creativecommons.org/licenses/by-nc-sa/4.0/).
The source code of this site is released under [GNU AGPL 3.0](https://www.gnu.org/licenses/agpl-3.0.en.html).
Powered with love by Kitten. š± š
</markdown>
</footer>
<style>
.Footer {
border-top: 1px solid gray;
padding-top: 1em;
text-align: center;
font-size: small;
}
.Footer p { margin: 0.25em; }
</style>
`
Finally, letās create some content to go in these named slots.
On the About page, weāll add a little shout-out to our funding page. And on the Contact Me page, letās add a notice to the header that weāre on holiday so no one tries to contact us.
about.page.js
import Site from './Site.layout.js'
import { ABOUT } from './pages.script.js'
export default () => kitten.html`
<${Site} pageId=${ABOUT}>
<markdown>
## Hey, look, itās me!
Information about me.
</markdown>
<content for='footer'>
<div class='funding'>
<p>If you want to fund my work, you can donate to <a href='https://small-tech.org/fund-us'>Small Technology Foundation</a></p>
</div>
</content>
</>
<style>
.funding {
padding: 1em;
margin-bottom: 1em;
background-color: aquamarine;
border-radius: 1em;
}
.funding a {
color: white;
}
</style>
`
contact-me.page
import Site from './Site.layout.js'
import { CONTACT_ME } from './pages.script.js'
export default () => kitten.html`
<${Site} pageId=${CONTACT_ME}>
<markdown>
## Get in touch!
Normally, thereād be a form here. Look in the guestbook and markdown examples if you want to see examples of such forms.
</markdown>
<content for='header'>
<p class='awayMessage'>Iām away on holiday at the moment</p>
</content>
</>
<style>
.awayMessage {
text-align: center;
font-weight: bold;
font-size: 1.5em;
background-color: yellow;
}
</style>
`
Notice that weāve kept the navigation component in the header component which means that when we override the headerās styles in the Site layout component to make the title display in in small caps, the navigationās font changes also.
What if we wanted to be able to style the navigation separately in our layout template?
Well, we could create a new named slot in the header component that inserts content about the heading and use that to pass in the navigation component. Then, we could override the style in the layout component.
Letās do that as our final refactor:
Header.component.js
import { pages } from './pages.script.js'
export default ({ pageId, SLOT, CLASS }) => kitten.html`
<header class='Header ${CLASS}'>
${SLOT.aboveHeading}
<h1>${pages[pageId].title}</h1>
${SLOT}
</header>
<style>
.Header {
border-bottom: 1px solid gray;
}
</style>
`
Site.layout.js
import Header from './Header.component.js'
import Navigation from './Navigation.component.js'
import Footer from './Footer.component.js'
export default ({ pageId, SLOT }) => kitten.html`
<${Header} pageId=${pageId} class='header'>
<content for='aboveHeading'>
<${Navigation} pageId=${pageId} class='navigation'/>
</content>
${SLOT.header}
</>
<main>
${SLOT}
</main>
<${Footer} class='footer'>
${SLOT.footer}
</>
<style>
body {
font-family: system-ui, sans-serif;
padding: 1em;
}
.header {
font-variant: small-caps;
}
.navigation {
font-variant: none;
color: white;
padding: 1em;
background-color: cadetblue;
}
.footer {
margin-top: 2em;
}
</style>
`
Note that if you wanted the content slotted into the header to go above the title instead of below it, you could have moved ${SLOT.header}
inside the <content for='aboveHeading'>ā¦</content>
tag.
You can basically nest named and default slots any way you like that makes sense for the design of your site or app and best expresses your intent.
š” You can even have multiple content areas target the same named slot and, just like the default slot, they will be stacked in the order in which they were encountered in your component or page.
In addition to the custom slots you can define and use in your components and fragments, there are also some special page slots built into Kitten that are useful for placing content at various places in the final web page. Letās learn about those next.
Next tutorial: Special page slots