Skip to content

AntikytheraMenu ​

The Antikythera library provides a graphical custom element named <antikythera-menu>. It renders a compact article sticker and expands into a fullscreen reading companion. Entry metadata loads first; expansion requests body-free menu data, while editorial, bibliography, and annotations load independently when needed. Matching .annotation elements are then connected to floating cards. Text-scan annotations may also be inserted automatically from the Sanity scan phrases.

In v2 the fullscreen view was reworked into snap-scrolled pages containing the entry details (abstract, editorial, non-glossary annotations, bibliography, credits), a PDF preview card with PDF and Markdown download buttons for the current entry, a Related Articles section, and an About page sourced from the journal document. Related article cards can show PDF previews and downloads on large screens, but never Markdown export buttons. In v2.1 these pages are content-driven: empty entry and related pages are omitted, so About becomes the first and only snap page when it is the only available content.

INFO

Please contact Antikythera to get the correct slug for your article and its corresponding annotation ids. Alternatively annotation ids can also be queried by using the Antikythera object directly which is explained in the next page.

Props ​

The <antikythera-menu /> element can be customized via several props (attributes):

NameTypeRequiredUsageDefault
entryStringfalseEntry slug; required for article-specific metadata, content, and annotations.undefined
themeStringfalseDefine menu theme. Options include dark and light.dark
environmentStringfalseDefine the environment. Options include production and staging.production
api-urlStringfalseRuntime API host override (valid http/https origin).baked-in host
api-background-colorStringfalseOverride the API background color (CMS value takes effect otherwise).undefined
api-foreground-colorStringfalseOverride the API foreground color (CMS value takes effect otherwise).undefined

INFO

The element also observes a few attributes (activeannotation, inactiveannotation, forceopen) that are managed internally by the Antikythera class for annotation coordination. You normally don't need to set these yourself.

Emits ​

The <antikythera-menu /> element emits a viewChange event whenever it changes view state. The states are:

ValueState
0Minimal View (compact sticker; on desktop, at most one floating annotation card)
1Summary View (sticker with title, authors, DOI, release date, and the More Info CTA; desktop annotation cards when enabled)
2Fullscreen View (expanded overlay with entry details, downloads, Related Articles, and About pages; Return to Article collapses the view)

INFO

The viewChange value can be accessed via:

<antikythera-menu @viewChange="<YOURFUNCTION>" />

const <YOURFUNCTION> = (value) => {
	console.log(value)
}

Table of contents ​

In 2.1.0, the sticker can show a compact current-chapter card that expands into a scrollable list of chapters with inline subheadings. The list follows reading position, stays within the available viewport height, and shows edge fades when more content is available. Desktop readers can preview chapters on hover; mobile readers tap to expand. Mobile navigation folds to the destination before scrolling, and opening an annotation collapses the expanded list.

Enable API ToC on the Sanity entry to opt in explicitly. When unset, it follows Use Pagination?; unpaginated articles default to off. The component renders it only when the API returns entry.apiToc === true and usable headings/navigation exist.

Host markup and chapter metadata ​

The menu reads headings with IDs inside .article-body: h1 establishes chapters, and h2/h3 establish subheadings. It also supports .block-heading[id] wrappers containing an h1. Headings inside asides, navigation, the footer, or [data-tertiary-aside] are excluded. Set data-toc-label on a heading to keep its ToC label stable when visible text is animated or replaced.

Paginated hosts expose ordered .chapter-navigation__tile[href] links inside .chapter-navigation, marking the current tile with aria-current="page". Link text (or the last span's text) supplies its chapter title. An optional data-toc-headings JSON attribute supplies off-page subheadings without fetching each chapter's HTML:

html
<a class="chapter-navigation__tile" href="/article/chapter-two"
   data-toc-headings='{"version":1,"headings":[{"id":"background","level":2,"title":"Background"}]}'>
  Chapter Two
</a>

IDs must be unique within that index and contain no whitespace or #; levels are 2 or 3 and titles must be nonempty. An empty headings array means the chapter has no subheadings. Missing or invalid metadata falls back to chapter discovery via HTML. Metadata changes are observed even outside the article root; failed fetches retry when readers explicitly reopen the list.

Router and anchor integration ​

For subheading navigation, the menu dispatches a bubbling, composed, cancelable antikythera:anchor-navigation event with detail: { href }. A router integration can call preventDefault() and own navigation to that URL, including its fragment. Unhandled requests retain the library's standalone navigation behavior; chapter links can delegate to the host's live chapter-navigation tiles.

A host that needs to wait for mobile menu folding can dispatch antikythera:prepare-anchor on the menu element with detail: { href, waitUntil }. The callback receives a promise resolving to the settled ToC card's viewport-bottom coordinate on mobile, or 0 on desktop. Await it before applying the host's anchor offset. These DOM events are separate from viewChange.

Initial fragment loads preserve the article summary until reader input, so browser anchor placement alone does not fold it. A saved ToC navigation destination takes precedence and restores the compact sticker; its navigation lock expires so subsequent reader scrolling can update the menu normally.

Article announcements ​

The first announcement selected on an entry appears below its sticker on viewports at least 640px wide. It is visible only within the first screen of the first chapter and while the fullscreen menu is closed. Scrolling one screen vertically or horizontally hides it; returning to the first screen restores it. Scrolling inside an announcement card does not dismiss it. The ToC is hidden while the announcement occupies the slot.

Cards support an image, subtitle, rich-text body, optional title/label, and a button. Background and text colors come from Sanity; without a text-color override, the foreground is chosen automatically. Hide Title and Label? leaves the subtitle visible. A valid enabled button also makes the image a link. Missing announcement data clears any previously displayed article announcement.

Expanded content loading ​

The menu requests includeContent: false, deferSections: true, keeping article body, editorial, bibliography, and annotations out of the initial expanded payload. Section-presence flags preserve the layout while editorial/bibliography prefetch on hover or focus or load on opening, with independent loading/retry controls. Annotations use their own endpoint and do not block menu readiness. Loaded sections survive collapse/reopen. See the API loading contract.

Download sections are labeled Resources and Related Resources; the current entry's Markdown action is Download Markdown. Related entry downloads remain PDF-only.

Annotations ​

On desktop, annotation cards are gated behind an "Annotations by …" attribution toggle rendered next to the sticker. Clicking an in-article annotation reveals and enables the toggle automatically, so readers who interact with annotations always see them. Floating (viewport-triggered) and inline (expanded view) annotations share the same presentation.

Annotations whose Sanity Type is Glossary Term are press-only: scrolling a highlighted glossary phrase does not add it to the automatic annotation stack. Pressing the phrase opens a single dismissible card below the sticker, including a desktop Close control.

The annotation rich-text editor supports Divider, regular URL marks, and typed Annotation Link references. Pressing an annotation link replaces the source card in its current slot. The card replacement transition handles the replacement in place, and linked annotations are fetched by their Sanity document reference when they were not part of the entry's initial annotation list, so links can continue recursively.

The annotation lifecycle is designed for client-side pagination. A document observer queues rescans after page swaps, adopts existing highlight elements without wrapping their text again, and rebinds interaction after DOM moves. Multiple connected highlights may share one annotation ID; a card remains active while any matching highlight is visible. Glossary cards never auto-open or auto-dismiss on scroll, while regular cards keep scroll entry/exit behavior with a short dismissal grace window.

Custom fonts ​

Uploaded Sanity fonts arrive on the full entry payload as fonts and fontFaceCss. When the menu fetches that response, the library installs or updates one document-level stylesheet. This is required because @font-face declarations inside a custom element's shadow root do not register fonts for the document. The host article template can then apply entry-wide and block-level font-family slugs from the API payload.

Debug shortcuts ​

While the menu is mounted in the Vite development environment, two keyboard shortcuts help with visual debugging. They are disabled in production builds:

KeyAction
iInvert the theme colors
gToggle a 12-column layout grid overlay

Shortcuts are ignored while typing in inputs, textareas, or editable elements.

Setting up ​

WARNING

Some frameworks (e.g. Nuxt) require additional setup to work with custom html elements. Please explicitly add 'antikythera-menu' to the list of allowed custom elements in the config file. If you experience issues in other frameworks please consult how custom html elements should be handled for the specific framework used.

e.g. nuxt.config.ts

Details
javascript
export default defineNuxtConfig({
	vue: { compilerOptions: { isCustomElement: (tag: any) => tag === 'antikythera-menu' } }
})

Vue/Nuxt

Details
vue
<template>
	<main>
		<nav>
			<antikythera-menu entry="example-entry" />
		</nav>
		<article>
			<p>
				Lorem ipsum dolor sit amet consectetur. Mi quis ut frla tincidunt rhoncus molestie.
				Lorem ipsum dolor sit amet consectetur. Mi quis ut fringilla proin varius nibh elit.
				<span id="example-annotation" class="annotation">Velit</span> lorem nulla tincidunt
				rhoncus placerat sed gravida neque. Arcu id vel eget nec est nec fermentum luctus.
				<span id="second-annotation" class="annotation">aenean</span> Ipsum tellus Ipsum
				tellus Ipsum tellus
			</p>
		</article>
	</main>
</template>

<script setup>
import { onMounted, ref } from 'vue'
import { AntikytheraMenu, Antikythera } from '@antikythera/antikythera'
import '@antikythera/antikythera/fonts'

const antikythera = ref(null)

onMounted(async () => {
	antikythera.value = new Antikythera({ entry: 'example-entry' })
})
</script>

React/Nextjs

Details
jsx
import { useEffect } from 'react'
import { AntikytheraMenu, Antikythera } from '@antikythera/antikythera'
import '@antikythera/antikythera/fonts'

export default function Home() {
	let antikythera

	useEffect(() => {
		if (!antikythera) {
			antikythera = new Antikythera({ entry: 'example-entry' })
		}
	}, [])

	return (
		<div>
			<main>
				<nav>
					<antikythera-menu entry="example-entry" />
				</nav>
				<article>
					<p>
						Lorem ipsum dolor sit amet consectetur. Mi quis ut frla tincidunt rhoncus
						molestie. Lorem ipsum dolor sit amet consectetur. Mi quis ut fringilla proin
						varius nibh elit.
						<span id="example-annotation" class="annotation">
							Velit
						</span>{' '}
						lorem nulla tincidunt rhoncus placerat sed gravida neque. Arcu id vel eget
						nec est nec fermentum luctus.
						<span id="second-annotation" class="annotation">
							aenean
						</span>{' '}
						Ipsum tellus Ipsum tellus Ipsum tellus
					</p>
				</article>
			</main>
		</div>
	)
}