Event handlers in mark decoration attributes

Thank you for CM & its excellent documentation!

I’m attempting to use mark decorations to annotate a document. I’d like my annotation decorations to apply to arbitrary code ranges, be highlighted (accomplished with CSS) and, when clicked, to render the corresponding annotation outside the CM editor. I’ve set up my annotations as a mark decoration through a StateField for ser/de (see below). Unfortunately, possibly as expected, my onclick function reference is not directly attached to the rendered DOM element. Instead it’s stringified, so the handler does not work as I would hope.

Is this expected behavior? If so, is there a better way to achieve what I’m after? I did not feel the other decoration types matched this use case as well as mark.

const addAnnotation = StateEffect.define();

const annotationField = StateField.define({
  create() {
    return Decoration.none;
  update(annotations, tr) {
    annotations = annotations.map(tr.changes);
    tr.effects.forEach((e) => {
      if (e.is(addAnnotation)) {
        const annotationMark = Decoration.mark({
          attributes: {
            class: 'cm-underline',
            // trouble here
            onclick: function() { alert(e.value.annotationId) },
          annotationId: e.value.annotationId,
        annotations = annotations.update({
          add: [annotationMark.range(e.value.from, e.value.to)],
    return annotations;
  provide: (f) => EditorView.decorations.from(f),
  toJSON: (value, state) => {
    const annotations = [];
    value.between(1, state.doc.length, (from, to, v2) => {
        annotationId: v2.spec.annotationId,
    return annotations;
  fromJSON: (json, state) => {

Yes, this is expected behavior—mark annotations can only add raw DOM attributes, not event handlers. You can use an editor-wide click handler, use posAtCoords to figure out where the click landed, and then see if that is inside an annotation (possibly by querying the decoration set).

Thank you! I’ll give that a try. Out of curiosity, is there a reason not to allow handler functions (e.g. by directly passing attributes into the DOM element)? Seems like it would provide a better API, although perhaps my use case is rare.