diff --git a/javascript/src/annotations.ts b/javascript/src/annotations.ts
new file mode 100644
index 0000000..4eef28d
--- /dev/null
+++ b/javascript/src/annotations.ts
@@ -0,0 +1,71 @@
+/* -----------------------------------------------------------------------------
+| Copyright (c) Jupyter Development Team.
+| Distributed under the terms of the Modified BSD License.
+|----------------------------------------------------------------------------*/
+
+import type { JSONValue } from '@lumino/coreutils';
+import type { ISignal } from '@lumino/signaling';
+
+/**
+ * Generic annotation change
+ */
+export type AnnotationsChange<T> = {
+  oldValue?: T;
+  newValue?: T;
+};
+
+/**
+ * Annotation interface.
+ */
+export interface IAnnotation {
+  sender: string;
+  pos: JSONValue;
+  content: JSONValue;
+}
+
+/**
+ * Annotations interface.
+ * This interface must be implemented by the shared documents that want
+ * to include annotations.
+ */
+export interface IAnnotations<T extends IAnnotation> {
+  /**
+   * The annotation changed signal.
+   */
+  readonly annotationChanged: ISignal<this, AnnotationsChange<T>>;
+
+  /**
+   * Return an iterator that yields every annotation key.
+   */
+  readonly annotations: Array<string>;
+
+  /**
+   * Get the value for an annotation
+   *
+   * @param key Key to get
+   */
+  getAnnotation(key: string): T | undefined;
+
+  /**
+   * Set the value of an annotation
+   *
+   * @param key Key to set
+   * @param value New value
+   */
+  setAnnotation(key: string, value: T): void;
+
+  /**
+   * Update the value of an existing annotation
+   *
+   * @param key Key to update
+   * @param value New value
+   */
+  updateAnnotation(key: string, value: T): void;
+
+  /**
+   * Delete an annotation
+   *
+   * @param key Key to delete
+   */
+  deleteAnnotation(key: string): void;
+}
diff --git a/javascript/src/api.ts b/javascript/src/api.ts
index 6daad0f..d8fe747 100644
--- a/javascript/src/api.ts
+++ b/javascript/src/api.ts
@@ -23,6 +23,8 @@ import type {
 import type { IObservableDisposable } from '@lumino/disposable';
 import type { ISignal } from '@lumino/signaling';
 
+import type { IAnnotation, IAnnotations } from './annotations.js';
+
 /**
  * Changes on Sequence-like data are expressed as Quill-inspired deltas.
  *
@@ -95,6 +97,11 @@ export interface ISharedDocument extends ISharedBase {
    */
   readonly state: JSONObject;
 
+  /**
+   * The changed signal.
+   */
+  readonly changed: ISignal<this, DocumentChange>;
+
   /**
    * Get the value for a state attribute
    *
@@ -109,11 +116,6 @@ export interface ISharedDocument extends ISharedBase {
    * @param value New attribute value
    */
   setState(key: string, value: JSONValue): void;
-
-  /**
-   * The changed signal.
-   */
-  readonly changed: ISignal<this, DocumentChange>;
 }
 
 /**
@@ -399,8 +401,10 @@ export namespace SharedCell {
  * Implements an API for nbformat.IBaseCell.
  */
 export interface ISharedBaseCell<
-  Metadata extends nbformat.IBaseCellMetadata = nbformat.IBaseCellMetadata
-> extends ISharedText {
+  Metadata extends nbformat.IBaseCellMetadata = nbformat.IBaseCellMetadata,
+  Annotation extends IAnnotation = IAnnotation
+> extends ISharedText,
+    IAnnotations<Annotation> {
   /**
    * The type of the cell.
    */
diff --git a/javascript/src/index.ts b/javascript/src/index.ts
index 68d6b03..5e1e3f3 100644
--- a/javascript/src/index.ts
+++ b/javascript/src/index.ts
@@ -10,6 +10,8 @@
 export * from './api.js';
 export * from './utils.js';
 
+export * from './annotations.js';
+
 export * from './ytext.js';
 export * from './ydocument.js';
 export * from './yfile.js';
diff --git a/javascript/src/ycell.ts b/javascript/src/ycell.ts
index 969100a..7bfb50a 100644
--- a/javascript/src/ycell.ts
+++ b/javascript/src/ycell.ts
@@ -6,8 +6,10 @@
 import type * as nbformat from '@jupyterlab/nbformat';
 import { JSONExt, JSONObject, PartialJSONValue, UUID } from '@lumino/coreutils';
 import { ISignal, Signal } from '@lumino/signaling';
+
 import { Awareness } from 'y-protocols/awareness';
 import * as Y from 'yjs';
+
 import type {
   CellChange,
   IMapChange,
@@ -18,6 +20,8 @@ import type {
   ISharedRawCell,
   SharedCell
 } from './api.js';
+import type { AnnotationsChange, IAnnotation } from './annotations.js';
+
 import { IYText } from './ytext.js';
 import { YNotebook } from './ynotebook.js';
 
@@ -128,7 +132,7 @@ export const createStandaloneCell = (cell: SharedCell.Cell): YCellType =>
   createCell(cell);
 
 export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
-  implements ISharedBaseCell<Metadata>, IYText
+  implements ISharedBaseCell<Metadata, IAnnotation>, IYText
 {
   /**
    * Create a new YCell that works standalone. It cannot be
@@ -227,6 +231,21 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
     return this._isDisposed;
   }
 
+  /**
+   * The annotation changed signal.
+   */
+  get annotationChanged(): ISignal<this, AnnotationsChange<IAnnotation>> {
+    return this._annotationChanged;
+  }
+
+  /**
+   * Return an iterator that yields every annotation key.
+   */
+  get annotations(): Array<string> {
+    const annotation = this._ymetadata.get('annotations');
+    return Object.keys(annotation);
+  }
+
   /**
    * Whether the cell is standalone or not.
    *
@@ -370,6 +389,66 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
     Signal.clearData(this);
   }
 
+  /**
+   * Get the value for an annotation
+   *
+   * @param key Key to get
+   */
+  getAnnotation(key: string): IAnnotation | undefined {
+    const annotations = this._ymetadata.get('annotations');
+    if (annotations && key in annotations) {
+      return JSONExt.deepCopy(annotations[key]);
+    } else {
+      return undefined;
+    }
+  }
+
+  /**
+   * Set the value of an annotation
+   *
+   * @param key Key to set
+   * @param value New value
+   */
+  setAnnotation(key: string, value: IAnnotation): void {
+    const clone = JSONExt.deepCopy(value as any);
+    const annotations = this._ymetadata.get('annotations') ?? {};
+    annotations[key] = clone;
+    this._ymetadata.set('annotations', annotations);
+  }
+
+  /**
+   * Update the value of an existing annotation
+   *
+   * @param key Key to update
+   * @param value New value
+   */
+  updateAnnotation(key: string, value: Partial<IAnnotation>): void {
+    const annotations = this._ymetadata.get('annotations');
+    if (!annotations || !(key in annotations)) {
+      return;
+    }
+
+    const annotation = annotations[key];
+    const clone = JSONExt.deepCopy(value as any);
+    for (const [key, value] of Object.entries(clone)) {
+      annotation[key] = value;
+    }
+    this._ymetadata.set('annotations', { ...annotations, key: annotation });
+  }
+
+  /**
+   * Delete an annotation
+   *
+   * @param key Key to delete
+   */
+  deleteAnnotation(key: string): void {
+    const annotations = this._ymetadata.get('annotations');
+    if (annotations && key in annotations) {
+      delete annotations[key];
+      this._ymetadata.set('annotations', annotations);
+    }
+  }
+
   /**
    * Get cell id.
    *
@@ -661,18 +740,24 @@ export class YBaseCell<Metadata extends nbformat.IBaseCellMetadata>
   };
 
   protected _metadataChanged = new Signal<this, IMapChange>(this);
+
   /**
    * The notebook that this cell belongs to.
    */
   protected _notebook: YNotebook | null = null;
+
   private _awareness: Awareness | null;
-  private _changed = new Signal<this, CellChange>(this);
-  private _disposed = new Signal<this, void>(this);
   private _isDisposed = false;
   private _prevSourceLength: number;
   private _undoManager: Y.UndoManager | null = null;
   private _ymetadata: Y.Map<any>;
   private _ysource: Y.Text;
+
+  private _disposed = new Signal<this, void>(this);
+  private _changed = new Signal<this, CellChange>(this);
+  private _annotationChanged = new Signal<this, AnnotationsChange<IAnnotation>>(
+    this
+  );
 }
 
 /**