Skip to content
ogygia playground

Guides

Tabs & code

Grouped tabs, synced package managers, copy buttons, and code in every language.

On this page

Documentation lives and dies by its code samples. ogygia gives you tabbed panels for showing the same thing three ways, build-time highlighting for a dozen languages, and a copy button on every block. This page shows all of it.

Tabs

A <TabGroup> wraps two or more <Tab> panels. Each Tab has a label, and the content between the tags is ordinary Markdown — including fenced code, as long as you leave blank lines around the fences.

npm install ogygia
pnpm add ogygia
yarn add ogygia
bun add ogygia

Author it like this:

<TabGroup group="install">
<Tab label="npm">

```bash
npm install ogygia
```

</Tab>
<Tab label="pnpm">

```bash
pnpm add ogygia
```

</Tab>
</TabGroup>

Synced tab groups

Give two <TabGroup> the same group and they move together. Pick pnpm once and every package-manager block on the page follows — the choice persists across reloads, so a returning reader never re-picks their tool.

npm run dev
pnpm dev
yarn dev
bun dev

Both <TabGroup> on this page share group="install". Try switching one — the other switches with it.

Markdown-native tabs

You rarely need the raw components. Two ::: blocks cover most cases, and the tab components are injected for you — no <script> import.

::: code-group puts the label in each fence, VitePress-style:

npm create ogygia@latest
pnpm create ogygia
bun create ogygia

::: tabs takes any Markdown, one tab per == Label:

Install with Homebrew: brew install node.

Use your distro’s package manager, or nvm.

Grab the installer from nodejs.org, or use winget.

Copy buttons

Every fenced block gets a copy button in its top corner. It copies the raw source, not the highlighted HTML, so what lands on the clipboard is exactly what you paste into a terminal or editor. Nothing to configure; it is on by default.

Code in every language

Highlighting is powered by Shiki at build time, so any language VS Code knows, ogygia renders. Here is a tour.

Shell

pnpm add ogygia
pnpm dev --host --port 4321

TypeScript

import { content, markdown } from 'ogygia/content';
import { z } from 'zod';

export const guides = content({
	source: markdown('guides/**/+doc.svx'),
	schema: z.object({
		title: z.string(),
		summary: z.string().optional()
	})
});

const page = await guides.get('guides/tabs-and-code');
console.log(page?.data.title);

Svelte

<script lang="ts">
	import Counter from '$lib/playground/demos/Counter.svelte' with { wake: 'visible' };

	let { title } = $props();
	let open = $state(false);
</script>

<h2>{title}</h2>
<button onclick={() => (open = !open)}>Toggle</button>
{#if open}
	<Counter />
{/if}

JSON

{
	"name": "my-docs",
	"type": "module",
	"scripts": {
		"dev": "vite dev",
		"build": "vite build"
	},
	"dependencies": {
		"ogygia": "^0.5.0"
	}
}

CSS

:root {
	--og-accent: #0d9488;
	--og-bg: #ffffff;
	--og-text: #1c1c21;
}

.doc-demo {
	padding: 1.5rem;
	border: 1px solid var(--og-line);
	border-radius: 12px;
}

HTML

<article class="doc">
	<h1>Tabs &amp; code</h1>
	<p>Static HTML, rendered on the server.</p>
	<div data-island="Counter"><!-- hydrates on visible --></div>
</article>

A longer sample

Real routes are longer than a snippet. Highlighting scales fine — here is a full catch-all page component:

<script lang="ts">
	import { Doc, SideNav, OnThisPage } from 'ogygia/content';
	import { page } from '$app/state';

	let { data } = $props();
	const { doc, nav } = $derived(data);

	const crumbs = $derived(
		page.url.pathname.split('/').filter(Boolean)
	);
</script>

<div class="layout">
	<aside>
		<SideNav tree={nav} current={page.url.pathname} />
	</aside>

	<article>
		<nav class="breadcrumbs" aria-label="Breadcrumb">
			{#each crumbs as crumb, i}
				<span>{crumb}</span>
				{#if i < crumbs.length - 1}<span aria-hidden="true">/</span>{/if}
			{/each}
		</nav>

		<h1>{doc.title}</h1>
		{#if doc.summary}<p class="summary">{doc.summary}</p>{/if}

		<doc.Component />
	</article>

	<OnThisPage headings={doc.headings} />
</div>

Tip. Keep samples runnable. A reader who copies a block should get something that works, not a fragment that needs three imports you did not show.

For the full stress test with tabs, tables, and live demos all at once, see the kitchen sink.