---
title: "Rich text — Angular"
description: "Text boxes can mix styles, like a bold word or a red sentence. How they're stored, how your toolbar formats them, and how to show your own fonts."
framework: "Angular"
source: "https://www.embedpdf.com/docs/headless/angular/concepts/rich-text"
---

# Rich text

A text box (a free text annotation) can mix styles: a bold word, a red sentence, a bigger heading.
Double-click the box below, select a word, and use the buttons, or press `Ctrl`/`Cmd` with `B`, `I`
or `U` while you type:

**`rich-text.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  untracked,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { withInteraction } from '@embedpdf/angular/interaction';
import { EpdfAnnotation, EpdfAnnotationLayer, withAnnotation } from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const notice = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/notice.pdf');
  return { kind: 'bytes', id: 'notice', bytes: new Uint8Array(await response.arrayBuffer()) };
};

type Format = 'bold' | 'italic' | 'underline';
const FORMATS: readonly Format[] = ['bold', 'italic', 'underline'];

// The same calls style the selected words while typing, and the whole box otherwise.
@Component({
  selector: 'demo-rich-text-toolbar',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (format of formats; track format) {
        <button
          type="button"
          [class]="'button ' + format"
          [attr.aria-pressed]="isOn(format)"
          [disabled]="!hasText()"
          (click)="annotation.text.toggleFormat(format)"
        >
          {{ format[0]!.toUpperCase() }}
        </button>
      }
      <button
        type="button"
        class="button"
        [disabled]="!hasText()"
        (click)="annotation.selection.update({ fontColor: '#c00000' })"
      >
        Red
      </button>
      <button
        type="button"
        class="button"
        [disabled]="!hasText()"
        (click)="annotation.selection.update({ fontSize: large() ? 16 : 24 })"
      >
        {{ large() ? '16 pt' : '24 pt' }}
      </button>
      <p class="hint">Ctrl or Cmd with B, I or U works while you type</p>
    </div>
  `,
})
export class RichTextToolbar {
  protected readonly annotation = inject(EpdfAnnotation);
  protected readonly formats = FORMATS;
  private readonly panel = this.annotation.selection.properties;
  protected readonly hasText = computed(() =>
    this.panel().properties.some((property) => property.control === 'textFormat'),
  );
  protected readonly large = computed(() => this.panel().values.fontSize === 24);

  // On when every selected word has it; `mixed` lists what the words disagree on.
  protected isOn(format: Format) {
    const { values, mixed } = this.panel();
    return values[format] === true && !mixed.includes(format);
  }
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
    RichTextToolbar,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: notice }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './rich-text.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-rich-text-toolbar />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
          <epdf-annotation-layer />
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly stage = viewChild(EpdfStage);
  private done = false;

  constructor() {
    // On load: the document's rich text box, selected.
    effect(() => {
      const stage = this.stage();
      if (!stage || this.annotation.status() !== 'ready' || this.done) return;
      this.done = true;
      untracked(() => {
        const richText = this.annotation.list().find((each) => each.nm === 'rich-text');
        if (!richText) return;
        this.annotation.selection.set([richText.ref]);
        stage.reveal(richText.page, { rect: richText.rect });
      });
    });
  }
}
```

**`rich-text.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

.hint {
  margin: 0;
  font-size: 13px;
  color: var(--muted);
}

/* Each format button shows its own format */
.bold {
  font-weight: 800;
}

.italic {
  font-style: italic;
}

.underline {
  text-decoration: underline;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

## How it's stored

A text box has a **body style**, which is how its text looks by default, and **paragraphs** of
**runs**. A run is a piece of text with the styles it changes, and nothing else: a bold word in a
regular box is one run with `{ weight: 700 }`.

```ts
annotation.richText;
// {
//   body: { family: 'Helvetica', size: 12, color: '#000000' },
//   paragraphs: [{ runs: [{ text: 'Paid ' }, { text: 'in full', style: { weight: 700 } }] }],
// }
```

The engine writes it the way Acrobat does, so the formatting is the same there. The engine docs
have the full model: [Free text](https://www.embedpdf.com/docs/engine/annotations/free-text#rich-text).

## Format from your toolbar

The same calls work while someone types in a box and when boxes are only selected:

```ts
await annotation.selection.update({ bold: true });
await annotation.selection.update({ fontSize: 24, fontColor: '#c00000' });
await annotation.text.toggleFormat('italic'); // italic on, or off if it already is
```

Where a change lands depends on what's selected:

- **Words selected while typing**: the change styles those words.
- **Otherwise**: it styles the whole text of every selected box.

A property that isn't about text, such as opacity or the border, always applies to the whole box.

To show the state in your toolbar, [`annotation.selection.properties()`](../annotations/selecting#a-style-panel)
reports it the same way. With words selected, `values.bold` is true when every selected word is
bold, and `mixed` lists what the words disagree on. With a caret or a selected box, it reports the
box's own style. Text boxes also give you `textFormat` properties, so a style panel shows the bold,
italic and underline buttons without extra code.

> A word with its own style keeps it when the box changes: turn the whole box red, and the one blue
> word stays blue. To make a word follow the box again, write the paragraphs without that style.

## When it's saved

Typing is written after a short pause, at every format change, and when editing ends, so the engine
never gets half a word. `documents.download()` waits for the last write, so the file you download has
everything the user typed.

## Show your own fonts

The engine puts a font into the PDF, and the browser draws the text while people type, in the same
font: the fonts plugin (`withFonts()`, from the `fonts` entry) hands the browser the file the
engine writes with. Register the font with the engine, in your `engine` function,
which runs only in the browser:

**`web-font.ts`**

```typescript
import { localEngine } from '@embedpdf/engine';

/** `provideEmbedPdf({ engine }, …)`: runs in the browser, before the first document opens. */
export const engine = async () => {
  const pdfEngine = localEngine();
  const data = await fetch('/fonts/brand-sans.ttf').then((response) => response.arrayBuffer());

  await pdfEngine.fonts.register({ key: 'brand-sans', data });
  return pdfEngine;
};
```

The key is the font's name: `fontFamily: 'brand-sans'` on a text box. The standard PDF fonts need
nothing: they show in the engine's own faces. Without the fonts plugin, a box shows in its standard
font, or in Helvetica. More in [Fonts](https://www.embedpdf.com/docs/engine/documents/fonts).

## Draw your own text box

The annotation layer's text box already edits rich text. If you [draw text boxes
yourself](../annotations/custom-rendering), `[epdfRichTextEditor]` makes your element the editor:
it draws the runs, turns typing back into runs, keeps the caret when the style changes, and handles
pasting, input methods and the format shortcuts:

**`branded-text-box.ts`**

```typescript
import { Component } from '@angular/core';
import {
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  EpdfRichTextEditor,
  type Annotation,
} from '@embedpdf/angular/annotation';

@Component({
  selector: 'app-annotations',
  imports: [EpdfAnnotationLayer, EpdfAnnotationTemplate, EpdfRichTextEditor],
  template: `
    <epdf-annotation-layer>
      <ng-template [epdfAnnotation]="isTextBox" let-annotation>
        <!-- The box fills its frame (width and height 100% in your CSS) and draws it; the
             editor inside fills the box. -->
        <div class="text-box" [class.editing]="editor.editing()">
          <div [epdfRichTextEditor]="annotation" #editor="epdfRichTextEditor"></div>
        </div>
      </ng-template>
    </epdf-annotation-layer>
  `,
})
export class Annotations {
  protected readonly isTextBox = (annotation: Annotation) => annotation.subtype === 'free-text';
}
```

**`branded-text-box.ts`**

```typescript
import {
  ChangeDetectionStrategy,
  Component,
  computed,
  effect,
  inject,
  untracked,
  viewChild,
  ViewEncapsulation,
} from '@angular/core';
import { EpdfDocumentGate, provideEmbedPdf } from '@embedpdf/angular/runtime';
import type { OpenInput } from '@embedpdf/angular/runtime';
import { EpdfPageTemplate, EpdfStage, withStage } from '@embedpdf/angular/stage';
import { EpdfRenderLayer, withRender } from '@embedpdf/angular/render';
import { EpdfSelectionLayer, withSelection } from '@embedpdf/angular/selection';
import { withInteraction } from '@embedpdf/angular/interaction';
import {
  EpdfAnnotation,
  EpdfAnnotationLayer,
  EpdfAnnotationTemplate,
  EpdfRichTextEditor,
  withAnnotation,
  type Annotation,
} from '@embedpdf/angular/annotation';
import { localEngine } from '@embedpdf/engine';

const notice = async (): Promise<OpenInput> => {
  const response = await fetch('/pdfs/notice.pdf');
  return { kind: 'bytes', id: 'notice', bytes: new Uint8Array(await response.arrayBuffer()) };
};

const FORMATS = ['bold', 'italic', 'underline'] as const;

// The formatting calls work on your element as they do on the built-in one.
@Component({
  selector: 'demo-format-buttons',
  changeDetection: ChangeDetectionStrategy.OnPush,
  template: `
    <div class="toolbar">
      @for (format of formats; track format) {
        <button
          type="button"
          [class]="'button ' + format"
          [disabled]="!hasText()"
          (click)="annotation.text.toggleFormat(format)"
        >
          {{ format[0]!.toUpperCase() }}
        </button>
      }
    </div>
  `,
})
export class FormatButtons {
  protected readonly annotation = inject(EpdfAnnotation);
  protected readonly formats = FORMATS;
  protected readonly hasText = computed(() =>
    this.annotation.selected().some((selected) => selected.subtype === 'free-text'),
  );
}

@Component({
  selector: 'demo-root',
  imports: [
    EpdfDocumentGate,
    EpdfStage,
    EpdfPageTemplate,
    EpdfRenderLayer,
    EpdfSelectionLayer,
    EpdfAnnotationLayer,
    EpdfAnnotationTemplate,
    EpdfRichTextEditor,
    FormatButtons,
  ],
  providers: [
    provideEmbedPdf(
      { engine: () => localEngine(), initialDocuments: [{ source: notice }] },
      withStage(),
      withRender(),
      withInteraction(),
      withSelection(),
      withAnnotation(),
    ),
  ],
  changeDetection: ChangeDetectionStrategy.OnPush,
  styleUrl: './branded-text-box.css',
  encapsulation: ViewEncapsulation.None,
  template: `
    <ng-container *epdfDocumentGate="let document; fallback: loading">
      <demo-format-buttons />
      <epdf-stage class="stage">
        <ng-template epdfPage>
          <epdf-render-layer />
          <epdf-selection-layer />
          <epdf-annotation-layer>
            <!-- Your own text box, in your app's look: it fills its frame, which the layer
                 places and turns like the text box. The element inside is the editor and
                 fills the box, so the box draws the fill, border and padding. -->
            <ng-template [epdfAnnotation]="isTextBox" let-annotation>
              <div class="text-box" [class.editing]="editor.editing()">
                <div [epdfRichTextEditor]="annotation" #editor="epdfRichTextEditor"></div>
              </div>
            </ng-template>
          </epdf-annotation-layer>
        </ng-template>
      </epdf-stage>
    </ng-container>

    <ng-template #loading><p class="loading">Loading…</p></ng-template>
  `,
})
export class App {
  private readonly annotation = inject(EpdfAnnotation);
  private readonly stage = viewChild(EpdfStage);
  private done = false;

  // Kept in a field, the same function on every page: the layer registers the look once.
  protected readonly isTextBox = (annotation: Annotation) => annotation.subtype === 'free-text';

  constructor() {
    // On load: the document's text box, selected, drawn by your component.
    effect(() => {
      const stage = this.stage();
      if (!stage || this.annotation.status() !== 'ready' || this.done) return;
      this.done = true;
      untracked(() => {
        const textBox = this.annotation.list().find((each) => each.nm === 'text-box');
        if (!textBox) return;
        this.annotation.selection.set([textBox.ref]);
        stage.reveal(textBox.page, { rect: textBox.rect });
      });
    });
  }
}
```

**`branded-text-box.css`**

```css
/* Colors */
:root {
  --ink: #1a2748;
  --text: #3b4b7a;
  --muted: #5a6b92;
  --line: #e6eaf2;
  --line-strong: #dce8fc;
  --surface: #ffffff;
  --tint: #f3f7fe;
  --canvas: #f7f9fc;
  --accent: #054fb3;
  --focus: #7db6ff;
  --shadow: rgb(7 32 76 / 0.04);
  --brand: #7c3aed;
  --brand-tint: #f5f0ff;
}

/* The controls, above the pages */
.toolbar {
  display: flex;
  flex-wrap: wrap;
  align-items: center;
  gap: 8px;
  margin-bottom: 14px;
}

.button {
  display: inline-flex;
  align-items: center;
  justify-content: center;
  gap: 6px;
  font: inherit;
  font-size: 13px;
  font-weight: 550;
  line-height: 1.1;
  color: var(--ink);
  background: var(--surface);
  border: 1px solid var(--line);
  border-radius: 9px;
  padding: 8px 12px;
  cursor: pointer;
  white-space: nowrap;
  box-shadow: 0 1px 2px var(--shadow);
  transition:
    background-color 0.15s ease,
    border-color 0.15s ease;
}

.button:hover {
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:active {
  transform: translateY(1px);
}

.button:focus-visible {
  outline: 2px solid var(--focus);
  outline-offset: 2px;
}

.button[aria-pressed='true'] {
  color: var(--accent);
  background: var(--tint);
  border-color: var(--line-strong);
}

.button:disabled {
  opacity: 0.45;
  cursor: not-allowed;
  transform: none;
}

/* Each format button shows its own format */
.bold {
  font-weight: 800;
}

.italic {
  font-style: italic;
}

.underline {
  text-decoration: underline;
}

/* Your own text box */
.text-box {
  box-sizing: border-box;
  width: 100%;
  height: 100%;
  padding: 6px 10px;
  overflow: hidden;
  background: var(--brand-tint);
  border-left: 4px solid var(--brand);
  border-radius: 4px 10px 10px 4px;
  box-shadow: 0 6px 16px -10px rgb(76 29 149 / 0.45);
}

.text-box.editing {
  outline: 2px solid var(--brand);
  outline-offset: 2px;
}

/* The pages */
.stage {
  height: 420px;
  border: 1px solid var(--line);
  border-radius: 14px;
  background: var(--canvas);
  box-shadow:
    0 1px 2px var(--shadow),
    0 18px 40px -28px rgb(7 32 76 / 0.28);
}

.loading {
  margin: 0;
  padding: 24px 0;
  text-align: center;
  font-size: 13px;
  color: var(--muted);
}
```

> **Try it:** double-click the box and type. The buttons format the selected words, as on the
> built-in box.

The directive gives your element the box's body style: font, size, color and alignment. Its
`editing()` is true while someone types in the box. It also makes the element fill its parent: the
text is laid out at 16 px or more and drawn back at the box's size, so iOS doesn't zoom the page in
when the box takes focus. Draw the box's fill, border and padding on the parent, as above; anything
on the element itself would scale with the text. The formatting calls above work on your element as
they do on the built-in one.
