Kitten

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

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>
`
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:

  1. 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.
  2. 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 and SLOT.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>
  …
`
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>
`
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

Like this? Fund us!

Small Technology Foundation is a tiny, independent not-for-profit.

We exist in part thanks to patronage by people like you. If you share our vision and want to support our work, please become a patron or donate to us today and help us continue to exist.