Skip to content

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:

gjs
<template>
  <h3>Hello, static fence!</h3>
</template>

Template-only component

The simplest live demo — a <template> tag with no class:

gjs
<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:

gjs
<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:

gjs
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:

gjs
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

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.

gts
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.

gjs
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.

gts
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.

gjs
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.

gts
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>
}