A static deck framework. Slides in .mdx, components in .axo, output is plain HTML with
no runtime — no React, no hydration, no islands. A deck renders at a venue with no wifi,
opens by double-click off a USB stick, and exports to PDF.
npx @ambystech/axo new my-talk --starter lightning --title "AmbyKit"
cd my-talk && npm install && npm run devmy-talk/
slides/ one .mdx per slide ← what you edit
components/ your .axo components ← optional
layouts/ your .axo layouts ← optional
css/deck.css this deck's CSS
assets/
axo.config.mjs title, visibility, footer
That's all. The compiler, the 39 components, the 6 layouts, the theme, Reveal, the fonts and
Mermaid all live in @ambystech/axo. npm update @ambystech/axo upgrades every deck you
own — which is the difference between a framework and a folder you copied.
axo new <dir> scaffold a deck --starter --visibility --title --link --install --yes
axo dev compile + watch + live-reload --port 8000 --open
axo build static site in dist/ --force (refuses if confidential)
axo preview serve dist/ --port 8080
axo pdf [out] export to PDF (starts its own server, no flags)
axo check compile without writing a site
axo ls list every component and layout --verbose
axo doctor check the deck and the toolchain
axo clean remove generated filesaxo new with no flags asks — kind of deck, title, visibility — and can install for you.
It never blocks a script: with --yes, no TTY, or in CI it takes the defaults and says so.
Not published yet? Point a deck at a local checkout:
axo new my-talk --link ../axo---
layout: title
---
<Eyebrow>Ambystech · Lightning Talk</Eyebrow>
# AmbyKit
<Subtitle>Spec-Driven Development for AI coding assistants</Subtitle>
<Meta>
<Field label="Speaker">Gustavo Barrientos</Field>
</Meta>
<Notes>
- Speaker notes. Not a script.
</Notes>Components compose — you nest children, you don't pass data arrays:
<Matrix columns={['Spec-Kit', 'OpenSpec', 'AmbyKit']}>
<Row label="Install" cells={['Python', 'npm', 'npm']} />
<Row label="Everyone has this" cells={['✓', '✓', '✓']} tie />
<Row label="Only we do this" cells={['—', '—', '✓']} reveal />
<Row label="They do it better" cells={['✓', '—', '— not yet']} concede reveal />
</Matrix>Full component API: docs/components.md.
Writing .axo in VS Code, Cursor or another fork? The editor extension in extension/
gives you colour, completion and diagnostics — see docs/editor-support.md.
.axo — a frontmatter script that runs at compile time, then a template. The filename is
the component name. Drop it in components/; there is no registration step.
---
const { name, price, featured } = Axo.props;
---
<div class:list={['step', featured && 'featured']}>
<span class="n">{name}</span>
<h4>{price}</h4>
<p><slot /></p>
</div>
Axo.props · Axo.children · Axo.slots.name · <slot /> · html:raw={} · class:list={[]}.
expect · pick · cx · toChildArray are ambient — no imports, ever.
A deck component named after a built-in overrides it. Want a different Matrix? Put
Matrix.axo in your deck. You never fork the framework, and everything else keeps upgrading.
No client:*, no islands, no runtime — by design. A client:load is a compile error.
That's what keeps a deck working offline.
A footer that says CONFIDENTIAL doesn't stop anyone publishing a deck; it only documents the
leak. visibility in axo.config.mjs is the thing that does:
visibility |
axo build |
workflow | delivery |
|---|---|---|---|
public |
builds | scaffolded, deploys to Pages | public URL |
internal |
builds, warns | none | behind auth / handover |
confidential |
refuses (--force to override) |
none | PDF only |
Defaults: lightning → public · training → internal · commercial/proposal →
confidential. It fails closed: an unset visibility is treated as confidential.
A non-public deck also gets <meta name="robots" content="noindex, nofollow, noarchive">,
and no deploy workflow is written at all — there is nothing to trigger, and nothing to
accidentally deploy.
The framework is written in TypeScript and ships compiled JavaScript plus .d.ts — you
never need TypeScript to use it, and engines stays at Node >= 20.
npm run typecheck # tsc --noEmit
npm run build # esbuild -> dist/*.js + tsc -> dist/*.d.tsbuild runs on prepare, so npm install / npm link produce a working dist/ with no
extra step.
Deck authors get types too:
// axo.config.mjs
import { defineConfig } from '@ambystech/axo';
export default defineConfig({
title: 'AmbyKit',
visibility: 'public', // autocompleted; a typo is a type error
});Static HTML. MDX, Preact and esbuild are compile-time dependencies of the framework; the
browser never sees them. dist/ contains index.html, css/, assets/ and vendor/ —
2.6 MB, no CDN link anywhere, and every slide's markdown inlined so it opens over file://.
MIT — see LICENSE.