Skip to content

Writing Components

There are two ways to include live Ember components in your documentation: inline code fences and file-based demos.

Known Limitations

By default, components are rendered without an Ember application container. Initializers and routing are not available. Service injection (@service) works when you call setupEmber with a services option in your theme setup.

Inline code fences

Write Ember components directly in your markdown using fenced code blocks. Add the live flag to make them interactive.

Template-only (GJS)

The simplest demo — just a <template> tag:

md
```gjs live
<template>
  <p>Hello from Ember!</p>
</template>
```

This renders the template inline in your page:

gjs
<template>
  <p style='color: tomato; font-weight: bold;'>Hello from Ember!</p>
</template>

Class-based components

Import from @glimmer/component, @glimmer/tracking, and @ember/modifier to build stateful components:

md
```gjs live
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

export default class Counter extends Component {
  @tracked count = 0;

  increment = () => {
    this.count++;
  };

  <template>
    <button type='button' {{on 'click' this.increment}}>
      Clicked:
      {{this.count}}
    </button>
  </template>
}
```
gjs
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';
import { on } from '@ember/modifier';

export default class Counter extends Component {
  @tracked count = 0;

  increment = () => {
    this.count++;
  };

  <template>
    <button
      type='button'
      {{on 'click' this.increment}}
      style='padding: 8px 16px; border-radius: 6px; background: #3498db; color: white; border: none; cursor: pointer; font-size: 14px;'
    >
      Clicked:
      {{this.count}}
    </button>
  </template>
}

TypeScript (GTS)

Use gts instead of gjs for TypeScript components:

md
```gts live
import Component from '@glimmer/component';
import { tracked } from '@glimmer/tracking';

interface Signature {
  Args: { label?: string };
}

export default class Greeter extends Component<Signature> {
  @tracked name = 'world';

  <template>
    <p>Hello, {{this.name}}!</p>
  </template>
}
```

The plugin automatically strips type annotations via @babel/plugin-transform-typescript.

Code fence flags

SyntaxBehavior
```gjsStatic, syntax-highlighted code only
```gjs liveLive rendered component
```gjs live previewLive component with source code displayed below
```gjs live preview collapsibleLive component with collapsible source code
```gts liveLive TypeScript component
```gts live previewLive TypeScript component with source code
```gts live preview collapsibleLive TypeScript component with collapsible source code

Preview mode

Adding preview shows both the rendered output and the source code:

gjs
<template>
  <p style='color: seagreen; font-weight: bold;'>
    This component renders above its own source code.
  </p>
</template>

File-based demos

For larger components, keep them in separate .gjs / .gts files and reference them with the <CodePreview> component directly:

md
<CodePreview src="/demos/counter.gts" />

Place demo files in a demos/ directory (or anywhere under docs/). The path is relative to your VitePress root.

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

TIP

File-based demos are useful when a component is too large for a code fence, or when you want to share the same component across multiple pages.

Available imports

The plugin resolves these package namespaces automatically — no extra dependencies needed:

PackageCommon imports
@glimmer/componentComponent base class
@glimmer/trackingtracked, cached
@ember/modifieron modifier
@ember/helperfn, concat, get, hash
@ember/serviceservice decorator
@ember/ownergetOwner, setOwner
@ember/rendererrenderComponent (used by the Vue wrapper)

Any @ember/* or @glimmer/* import is resolved from ember-source's ESM packages automatically.

Custom packages

You can use any npm package installed in your project — just import it in your component code and Vite will resolve it from node_modules:

md
```gjs live
import { TrackedArray } from 'tracked-built-ins';
import Component from '@glimmer/component';
import { on } from '@ember/modifier';

export default class List extends Component {
  items = new TrackedArray(['hello', 'world']);

  add = () => {
    this.items.push('item ' + this.items.length);
  };

  <template>
    <ul>{{#each this.items as |item|}}<li>{{item}}</li>{{/each}}</ul>
    <button type='button' {{on 'click' this.add}}>Add</button>
  </template>
}
```

For packages that need special resolution (e.g., mapping a bare specifier to a local file), use the resolve option.

For addons that ship custom Babel plugins, use the babelPlugins option.

Styling components

Inline styles work as expected inside <template> tags. You can also use standard CSS approaches:

gjs
<template>
  <div class='demo-card'>
    <h4>Styled card</h4>
    <p>This uses inline styles for simplicity.</p>
  </div>
  <style>
    .demo-card {
      padding: 16px;
      background: var(--vp-c-bg-soft);
      border-radius: 8px;
      border: 1px solid var(--vp-c-divider);
    }
    .demo-card h4 {
      margin: 0 0 8px;
    }
    .demo-card p {
      margin: 0;
      color: var(--vp-c-text-2);
    }
  </style>
</template>

WARNING

<style> blocks inside templates are injected globally. Use unique class names to avoid conflicts between demos.