Skip to content

3. Note Cards

Each note becomes a clickable card. Clicking it emits a note.opened event that carries the note’s id from the boundary, automatically. We won’t write a click handler — the boundary delegates.

function renderNoteItem(note: Note): string {
return `
<li>
<kit-boundary surface="note-card" entity-type="note" entity-id="${escape(note.id)}">
<kit-card
interactive
tabindex="0"
role="button"
meta-event="note.opened"
meta-prop-id="${escape(note.id)}"
>
<h3 slot="header">${escape(note.title)}</h3>
<p>${escape(note.body)}</p>
<small slot="footer">Updated ${new Date(note.updatedAt).toLocaleString()}</small>
</kit-card>
</kit-boundary>
</li>
`
}

What changed:

  • <article><kit-card interactive> (a styled, slotted card).
  • Added tabindex="0" and role="button" so the card is focusable and screen readers announce it as a button.
  • Added meta-event="note.opened" and meta-prop-id="..." so a click emits the event with payload: { id: 'n_welcome' }.

Wrap the whole layout in a two-column grid. src/render.ts:

export function renderApp(notes: Note[]): string {
return `
<div class="app-grid">
<section class="sidebar">
${renderNoteList(notes)}
</section>
<section id="editor" class="editor">
<kit-boundary surface="empty-editor" feature="notes">
<p class="muted">Select a note from the left.</p>
</kit-boundary>
</section>
</div>
`
}

And update main.ts:

document.getElementById('main')!.innerHTML = renderApp(seedNotes)

CSS:

.app-grid {
display: grid;
gap: 1.5rem;
grid-template-columns: minmax(16rem, 24rem) 1fr;
}
.muted { color: light-dark(oklch(45% 0 0), oklch(70% 0 0)); }

Listen for note.opened and render the editor

Section titled “Listen for note.opened and render the editor”

In main.ts, after installing the modules:

import type { Note } from './notes.js'
const editorEl = document.getElementById('editor')!
function renderEditor(note: Note) {
editorEl.innerHTML = `
<kit-boundary surface="note-editor" feature="notes" entity-type="note" entity-id="${note.id}">
<article>
<h2>${escape(note.title)}</h2>
<pre class="body">${escape(note.body)}</pre>
<small>Last updated ${new Date(note.updatedAt).toLocaleString()}</small>
</article>
</kit-boundary>
`
}
shell.runtime.on('note.opened', (event) => {
const id = String(event.payload?.id ?? '')
const note = seedNotes.find((n) => n.id === id)
if (note) renderEditor(note)
})

(Keep the escape helper handy — duplicate it from render.ts or import it.)

Click any card. The right pane renders the note. The console shows the event:

[kit:event] note.opened {
type: 'note.opened',
context: { surfaces: ['app', 'note-list', 'note-card'], surface: 'note-card', feature: 'notes', entity: { type: 'note', id: 'n_welcome' } },
payload: { id: 'n_welcome' },
source: { tagName: 'kit-card' },
}

We didn’t write a click listener on the card. The boundary delegated. The card declared its meaning. The runtime stitched in context.

The card is presentational. Adding role="button" and tabindex="0" makes it announce as a button to assistive tech and become focusable. It does not automatically activate on Space/Enter — for that, custom elements with role="button" should also handle keyboard activation.

A future iteration of <kit-card> will accept an interactive-as="button" attribute that handles all of this. For now, in production code, prefer wrapping the card content in a real <button> for accessibility.

Time to create notes. We’ll add a form with <kit-field> and use the native form submission flow.

Next: Chapter 4 — The Form →