Examples
Interactive demos showing the different ways to embed Ember components in VitePress.
Static code fence
A regular code fence without live — syntax-highlighted only, not rendered:
<template>
<h3>Hello, static fence!</h3>
</template>Template-only component
The simplest live demo — a <template> tag with no class:
<template>
<p style='color: tomato; font-weight: bold;'>
Hello from a live Ember template!
</p>
</template>Live + Preview
Adding preview shows both the rendered output and the source code:
<template>
<p style='color: seagreen'>
This renders with a preview panel and a code block.
</p>
</template>Toggle button
A class-based component with @tracked state and the on modifier:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
export default class Toggle extends Component {
@tracked isOn = false;
toggle = () => {
this.isOn = !this.isOn;
};
<template>
<button
type='button'
{{on 'click' this.toggle}}
style='padding: 10px 20px; border-radius: 20px; border: none; cursor: pointer; font-weight: bold; font-size: 14px; transition: all 0.2s;
background: {{if this.isOn "#2ecc71" "#ccc"}};
color: {{if this.isOn "white" "#666"}};'
>
{{if this.isOn 'ON ✓' 'OFF'}}
</button>
</template>
}Step counter (with preview)
A more complex component showing input binding, multiple actions, and preview mode:
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
export default class StepCounter extends Component {
@tracked step = 1;
@tracked value = 0;
changeStep = (e) => {
this.step = Number(e.target.value) || 1;
};
add = () => {
this.value += this.step;
};
reset = () => {
this.value = 0;
};
<template>
<div
style='font-family: system-ui; display: flex; gap: 8px; align-items: center; flex-wrap: wrap;'
>
<label style='font-size: 13px;'>
Step:
<input
type='number'
value={{this.step}}
{{on 'input' this.changeStep}}
min='1'
style='width: 50px; padding: 4px; border: 1px solid #ccc; border-radius: 4px;'
/>
</label>
<button
type='button'
{{on 'click' this.add}}
style='padding: 6px 14px; border-radius: 4px; background: #3498db; color: white; border: none; cursor: pointer;'
>Add +{{this.step}}</button>
<button
type='button'
{{on 'click' this.reset}}
style='padding: 6px 14px; border-radius: 4px; background: #e74c3c; color: white; border: none; cursor: pointer;'
>Reset</button>
<span
style='font-size: 20px; font-weight: bold; min-width: 60px; text-align: center;'
>
{{this.value}}
</span>
</div>
</template>
}File-based demos
Components loaded from .gjs / .gts files in the demos/ directory:
button.gjs
<template>
<button
type="button"
style="padding: 8px 16px; border-radius: 6px; background: var(--vp-c-brand-1, #e04e39); color: white; border: none; cursor: pointer; font-family: var(--vp-font-family-base, system-ui); font-size: 14px; font-weight: 600; transition: opacity 0.2s;"
>
Ember Button
</button>
</template>counter.gts
An interactive counter with TypeScript, @tracked state, and arrow function methods.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
interface CounterSignature {
Element: HTMLDivElement;
}
export default class Counter extends Component<CounterSignature> {
@tracked count = 0;
increment = () => {
this.count += 1;
};
decrement = () => {
this.count -= 1;
};
<template>
<div
style="display: inline-flex; align-items: center; gap: 0; font-family: var(--vp-font-family-base, system-ui); border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden;"
>
<button
type="button"
{{on "click" this.decrement}}
style="padding: 8px 16px; border: none; border-right: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft); color: var(--vp-c-text-1); cursor: pointer; font-size: 18px; line-height: 1; transition: background 0.2s;"
>−</button>
<span
style="min-width: 48px; padding: 8px 4px; text-align: center; font-size: 20px; font-weight: 600; color: var(--vp-c-text-1); background: var(--vp-c-bg); font-variant-numeric: tabular-nums;"
>
{{this.count}}
</span>
<button
type="button"
{{on "click" this.increment}}
style="padding: 8px 16px; border: none; border-left: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft); color: var(--vp-c-text-1); cursor: pointer; font-size: 18px; line-height: 1; transition: background 0.2s;"
>+</button>
</div>
</template>
}todo-list.gjs
A todo list demonstrating component composition and array state management.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
const gt = (a, b) => a > b;
const TodoItem = <template>
<li
style="display: flex; align-items: center; gap: 10px; padding: 10px 12px; border-bottom: 1px solid var(--vp-c-divider);"
>
<input
type="checkbox"
checked={{@todo.done}}
{{on "change" @onToggle}}
style="width: 18px; height: 18px; cursor: pointer; accent-color: var(--vp-c-brand-1, #e04e39);"
/>
<span
style="flex: 1; text-decoration: {{if
@todo.done
'line-through'
'none'
}}; color: {{if
@todo.done
'var(--vp-c-text-3)'
'var(--vp-c-text-1)'
}}; transition: color 0.2s;"
>
{{@todo.text}}
</span>
<button
type="button"
{{on "click" @onRemove}}
style="background: none; border: none; color: var(--vp-c-danger-1, #e04e39); cursor: pointer; font-size: 16px; padding: 4px; line-height: 1; opacity: 0.7; transition: opacity 0.2s;"
>✕</button>
</li>
</template>;
export default class TodoList extends Component {
@tracked todos = [
{ id: 1, text: 'Learn Ember with Vite', done: true },
{ id: 2, text: 'Build a VitePress plugin', done: true },
{ id: 3, text: 'Create interactive demos', done: false },
];
@tracked nextId = 4;
@tracked newText = '';
updateText = (event) => {
this.newText = event.target.value;
};
addTodo = (event) => {
event.preventDefault();
const text = this.newText.trim();
if (!text) return;
this.todos = [...this.todos, { id: this.nextId, text, done: false }];
this.nextId += 1;
this.newText = '';
};
toggleTodo = (todo) => {
this.todos = this.todos.map((t) =>
t.id === todo.id ? { ...t, done: !t.done } : t,
);
};
removeTodo = (todo) => {
this.todos = this.todos.filter((t) => t.id !== todo.id);
};
get remaining() {
return this.todos.filter((t) => !t.done).length;
}
<template>
<div
style="font-family: var(--vp-font-family-base, system-ui); max-width: 420px; border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden;"
>
<div
style="padding: 14px 16px; border-bottom: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);"
>
<h4 style="margin: 0; font-size: 16px; color: var(--vp-c-text-1);">📝
Todo List</h4>
</div>
<div style="padding: 12px 16px;">
<form
{{on "submit" this.addTodo}}
style="display: flex; gap: 8px; margin-bottom: 4px;"
>
<input
type="text"
value={{this.newText}}
{{on "input" this.updateText}}
placeholder="What needs to be done?"
style="flex: 1; padding: 8px 12px; border: 1px solid var(--vp-c-divider); border-radius: 6px; background: var(--vp-c-bg); color: var(--vp-c-text-1); font-size: 14px; outline: none;"
/>
<button
type="submit"
style="padding: 8px 16px; border-radius: 6px; background: var(--vp-c-brand-1, #e04e39); color: white; border: none; cursor: pointer; font-weight: 600; font-size: 14px;"
>Add</button>
</form>
</div>
<ul style="list-style: none; padding: 0; margin: 0;">
{{#each this.todos as |todo|}}
<TodoItem
@todo={{todo}}
@onToggle={{fn this.toggleTodo todo}}
@onRemove={{fn this.removeTodo todo}}
/>
{{/each}}
</ul>
<div
style="padding: 10px 16px; border-top: 1px solid var(--vp-c-divider); background: var(--vp-c-bg-soft);"
>
<p style="margin: 0; font-size: 13px; color: var(--vp-c-text-3);">
{{this.remaining}}
item{{if (gt this.remaining 1) "s" ""}}
remaining
</p>
</div>
</div>
</template>
}tab-panel.gts
A tab panel written in GTS with TypeScript interfaces.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
const eq = (a: unknown, b: unknown) => a === b;
interface TabPanelSignature {
Element: HTMLDivElement;
}
type Tab = 'about' | 'features' | 'setup';
const TAB_CONTENT: Record<Tab, string> = {
about:
'This plugin lets you embed live Ember components directly inside VitePress documentation. Components are compiled on-the-fly using content-tag and babel-plugin-ember-template-compilation.',
features:
'• Inline GJS/GTS code fences with live rendering\n• File-based component demos\n• Full Glimmer component support with tracked state\n• TypeScript support via .gts files\n• Hot module replacement',
setup:
'1. Install vite-plugin-ember\n2. Add it to your VitePress config\n3. Use ```gjs live fences or <CodePreview> components\n4. Write standard Ember/Glimmer components',
};
export default class TabPanel extends Component<TabPanelSignature> {
@tracked activeTab: Tab = 'about';
selectTab = (tab: Tab) => {
this.activeTab = tab;
};
get content(): string {
return TAB_CONTENT[this.activeTab];
}
get tabs(): Tab[] {
return ['about', 'features', 'setup'];
}
<template>
<div
style="font-family: var(--vp-font-family-base, system-ui); max-width: 500px; border: 1px solid var(--vp-c-divider); border-radius: 8px; overflow: hidden;"
>
<nav
style="display: flex; background: var(--vp-c-bg-soft); border-bottom: 1px solid var(--vp-c-divider);"
>
{{#each this.tabs as |tab|}}
<button
type="button"
{{on "click" (fn this.selectTab tab)}}
style="flex: 1; padding: 10px; border: none; cursor: pointer; font-weight: 600; text-transform: capitalize; transition: color 0.2s, background 0.2s;
background: {{if
(eq tab this.activeTab)
'var(--vp-c-bg)'
'transparent'
}};
color: {{if
(eq tab this.activeTab)
'var(--vp-c-brand-1, #e04e39)'
'var(--vp-c-text-2)'
}};
border-bottom: {{if
(eq tab this.activeTab)
'2px solid var(--vp-c-brand-1, #e04e39)'
'2px solid transparent'
}};"
>
{{tab}}
</button>
{{/each}}
</nav>
<div
style="padding: 16px; min-height: 100px; white-space: pre-line; line-height: 1.6; color: var(--vp-c-text-1);"
>
{{this.content}}
</div>
</div>
</template>
}color-picker.gjs
A color picker showing child component extraction and dynamic inline styles.
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';
import { fn } from '@ember/helper';
const eq = (a, b) => a === b;
const COLORS = [
'#e04e39',
'#3498db',
'#2ecc71',
'#f39c12',
'#9b59b6',
'#1abc9c',
];
const ColorSwatch = <template>
<button
type="button"
{{on "click" @onSelect}}
style="width: 32px; height: 32px; border-radius: 50%;
border: {{if
@isSelected
'3px solid var(--vp-c-text-1)'
'2px solid transparent'
}};
background: {{@color}}; cursor: pointer;
transition: transform 0.15s;
transform: {{if @isSelected 'scale(1.2)' 'scale(1)'}};"
></button>
</template>;
export default class ColorPicker extends Component {
@tracked selectedColor = COLORS[0];
@tracked message = 'Pick a color!';
selectColor = (color) => {
this.selectedColor = color;
this.message = `Selected: ${color}`;
};
<template>
<div
style="font-family: var(--vp-font-family-base, system-ui); max-width: 300px;"
>
<h4 style="margin: 0 0 12px; color: var(--vp-c-text-1);">🎨 Color Picker</h4>
<div
style="display: flex; gap: 8px; margin-bottom: 16px; flex-wrap: wrap;"
>
{{#each COLORS as |color|}}
<ColorSwatch
@color={{color}}
@isSelected={{eq color this.selectedColor}}
@onSelect={{fn this.selectColor color}}
/>
{{/each}}
</div>
<div
style="padding: 20px; border-radius: 8px; text-align: center;
background: {{this.selectedColor}}; color: white;
font-weight: bold; transition: background 0.3s;"
>
{{this.message}}
</div>
</div>
</template>
}Service injection
Components can use Ember's @service decorator to inject services registered via setupEmber.
service-demo.gts
A component that receives a greeting message from an injected GreetingService.
import Component from '@glimmer/component';
import { service } from '@ember/service';
import type GreetingService from './greeting-service';
export default class ServiceDemo extends Component {
@service declare greeting: GreetingService;
<template>
<div
style="display: flex; align-items: center; gap: 10px; padding: 16px; font-family: var(--vp-font-family-base, system-ui); background: var(--vp-c-bg-soft); border-radius: 8px;"
>
<span style="font-size: 24px;">🏷️</span>
<div>
<p
style="margin: 0; font-size: 16px; font-weight: 600; color: var(--vp-c-text-1);"
>
{{this.greeting.message}}
</p>
<p style="margin: 4px 0 0; font-size: 13px; color: var(--vp-c-text-2);">
This message was injected via
<code>@service</code>.
</p>
</div>
</div>
</template>
}