Skip to main content

  • Foundations
    • Border radius
    • Breakpoints
    • Colours
    • Colour scheme
    • Elevation
    • Focus and outline
    • Sizing and spacing
    • Layout
    • Motion
    • Typography
    • Z-index
  • Elements
    • Buttons
    • Code
    • Icons
    • Figure and caption
    • Forms
    • Links
    • Lists
    • Media
    • Tables
  • Components
    • Accordion
    • Alerts
    • Avatar
    • Blockquote
    • Breadcrumbs
    • Card
    • Chiplets
    • Details and summary
    • Dialog
    • Empty state
    • Stats
    • Global footer
    • Horizontal rule
    • Kanban
    • Key-value list
    • Meter
    • Objects
    • Pagination
    • Polka background
    • Post-it note
    • Progress
    • Speech bubbles
    • Status dot
    • Skeleton
    • Skip link
    • Spinner
    • Toggles
    • Tooltip
    • Webmentions
  • Patterns
    • Article header
    • Author card
    • App list and app sheet
    • Error page
    • Related posts
    • Search results
    • Utilities

John Peart

  • Search
  • Menu
John Peart

Search results

Query input, filter chips and result list.

The live search page at /search/. A single input fetches a JSON index, filters client-side on title, excerpt and collection, and renders matches as a key-value list.

Anatomy

  1. Search form — a <search> wrapper containing a <form role="search" class="card"> with a label and an autofocused type="search" input.
  2. Results region — <section id="results"> holds the list; rendered as a ul.key-value.
  3. Result item — a <li class="key-value--item key-value--search"> with a link, collection label, title, excerpt and optional image.
  4. No-match state — a <p class="no-results">No results found</p> when the filter returns nothing.

Search form

<search>
  <form role="search" aria-label="Search for a post" class="card">
    <label class="label" for="searchBox">Search</label>
    <input
      type="search"
      id="searchBox"
      placeholder="Enter a search term"
      aria-autocomplete="list"
      aria-controls="results"
      autofocus
    >
  </form>
</search>

Results

Results render into the same key-value layout the rest of the site uses for indexes. Each row carries the collection as the label, the title as the key, and the excerpt as the description. When an entry has an image — post hero, file icon or decorative object — it's prepended inside the link.

    Results

  • Blog post

    Eurovision 2024 — a year of Northern Lights

    A long read on the Malmö contest, staging, song selection and the return of live audiences.

  • Weeknote

    Weeknote: Eurovision voting night

    The usual scorecard and some notes on the UK jury.

<section id="results">
  <ul id="search-results" aria-label="Search results" class="key-value" role="listbox">
    <h2 class="key-value--heading">Results</h2>
    <li class="key-value--item key-value--search key-value--with-spacing">
      <a class="key-value--link key-value--card-link" href="/posts/…">
        <p class="key-value--label">Blog post</p>
        <p class="key-value--key">Eurovision 2024 — a year of Northern Lights</p>
        <p class="key-value--description">…</p>
      </a>
    </li>
  </ul>
</section>

No results

    Results

    No results found

Templates

The layout uses native <template> elements that the client-side script clones into the DOM — no framework needed.

<template id="search-result-template">
  <li class="key-value--item key-value--search key-value--with-spacing">
    <a class="key-value--link key-value--card-link">
      <p class="key-value--label"></p>
      <p class="key-value--key"></p>
      <p class="key-value--description"></p>
    </a>
  </li>
</template>

<template id="search-result-image-template">
  <img class="key-value--image">
</template>

Index

A JSON file at /search.json lists every indexable page — title, date, URL, excerpt, optional image, and collection name. The script fetches it once and filters in memory.

async function loadIndex() {
  const res = await fetch('/search.json');
  return res.json();
}

function searchPosts(posts, query) {
  const q = query.toLowerCase().trim();
  if (q === '') return [];
  return posts.filter(p =>
    (p.title      && p.title.toLowerCase().includes(q)) ||
    (p.excerpt    && p.excerpt.toLowerCase().includes(q)) ||
    (p.collection && p.collection.toLowerCase().includes(q))
  );
}

Accessibility

  • Wrap the form in <search> and give it role="search".
  • Connect the input and result list with aria-controls and aria-autocomplete="list".
  • The result list is labelled aria-label="Search results".
A line drawing of John Peart

John Peart

www.johnpe.art

Mastodon logo

Mastodon

BlueSky logo

Bluesky

LinkedIn logo

LinkedIn

Github logo

GitHub