npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

@explita/tiptap-extension-image

v0.1.0

Published

A framework-agnostic TipTap v3 image extension with upload placeholders, drag-and-drop uploads, resizing, alignment controls, and external image sources.

Readme

@explita/tiptap-extension-image

A rich TipTap image extension with upload placeholders, drag-and-drop, resizable handles, alignment toolbar, and external image support.

Features

  • Two visual modes: Upload placeholder (dashed drop zone) and rendered <img> element
  • Click to upload: Opens a native file picker filtered to PNG, JPEG, GIF, and WebP
  • Drag-and-drop: Drag an image file directly onto the placeholder; visual feedback on hover
  • Clipboard paste support: Paste images directly from the clipboard and upload through the configured onUpload callback.
  • External image support: External images bypass upload processing and are stored directly as their source URL.
  • Alignment toolbar: Floating bar with Inline / Left / Center / Right buttons on hover
  • Resize handles: Optional drag-to-resize via ResizableNodeView with configurable directions, min dimensions, and aspect-ratio lock
  • Error states: Inline error messages for invalid file types, failed uploads, or empty results
  • Multi-image upload: Upload callback can return multiple images — all are inserted into the document
  • Framework-agnostic: No React, Vue, or Tailwind dependencies — plain DOM node view
  • No external CSS required: All styles are inline; customize via HTMLAttributes or [data-image-node] CSS selectors

Installation

npm install @explita/tiptap-extension-image
# or
pnpm add @explita/tiptap-extension-image

This package has a peer dependency on @tiptap/core and @tiptap/pm (v3).

This extension is designed as a replacement for @tiptap/extension-image. Do not install both extensions in the same editor instance.

Usage

Basic setup

import { Editor } from "@tiptap/core";
import { Image } from "@explita/tiptap-extension-image";

const editor = new Editor({
  extensions: [
    Image.configure({
      onUpload: async (files) => {
        // Upload the files to your server/CDN and return image metadata
        const formData = new FormData();
        for (const f of files) formData.append("files", f);
        const res = await fetch("/api/upload", {
          method: "POST",
          body: formData,
        });
        const data = await res.json();
        return data.map((item) => ({
          src: item.url,
          alt: item.alt,
          title: item.title,
        }));
      },
    }),
  ],
});

Upload errors

Throw an error from onUpload to display the error state:

onUpload: async (files) => {
  const result = await upload(files[0]);

  if (!result.success) {
    throw new Error("Upload failed");
  }

  return [{ src: result.url }];
};

Commands

// Insert an upload placeholder
editor.chain().setImageUpload().run();

// Insert an external image from a URL
editor
  .chain()
  .setExternalImage({
    src: "https://example.com/image.jpg",
    alt: "Alt text",
    title: "Title",
  })
  .run();

// Change alignment of the selected image
editor.chain().setImageAlignment("left").run();
editor.chain().setImageAlignment("center").run();
editor.chain().setImageAlignment("right").run();
editor.chain().setImageAlignment(null).run(); // inline / reset

With external picker (vault, gallery, etc.)

Image.configure({
  onPick: async () => {
    // Open your custom picker UI
    const images = await myPicker.open();
    return images; // Array<{ src, alt?, title? }> or null if cancelled
  },
  onUpload: async (files) => {
    // Fallback upload
    const url = await uploadFile(files[0]);
    return [{ src: url }];
  },
});

With resize handles

Image.configure({
  resize: {
    enabled: true,
    directions: ["se", "e"], // "se" (corner) | "e" (right edge) | "ne" | "nw" | "sw" | "w" | "n" | "s"
    minWidth: 50,
    minHeight: 50,
    preserveAspectRatio: true,
  },
});

Configuration

| Option | Type | Default | Description | | ---------------- | ------------------------------------------------------ | ------- | ----------------------------------------------------- | | inline | boolean | true | Render inline (<span>) or as block (<div>) | | onUpload | ((files: File[]) => Promise\<ImageAsset[]\>) \| null | null | Callback to upload files; returns image assets | | onPick | (() => Promise\<ImageAsset[] \| null\>) \| null | null | Callback to pick from an external source | | HTMLAttributes | Record\<string, any\> | {} | Extra HTML attributes merged onto the <img> element | | resize | ResizeConfig \| false | false | Configures drag-resize handles (see below) | | pasteImages | boolean | false | Enables clipboard image uploads |

ImageAsset

interface ImageAsset {
  id?: string;
  src: string;
  alt?: string;
  title?: string;
  width?: number;
  height?: number;
  mimeType?: string;
  size?: number;
}

ResizeConfig

interface ResizeConfig {
  enabled: boolean;
  directions?: ResizableNodeViewDirection[]; // e.g. ["se", "e"]
  minWidth?: number;
  minHeight?: number;
  preserveAspectRatio?: boolean;
}

Commands API

The extension extends TipTap's Commands interface with three commands:

| Command | Signature | Description | | ------------------- | ----------------------------------------------------------------------- | ------------------------------------- | | setImageUpload | () => ReturnType | Inserts a new upload placeholder node | | setExternalImage | (params: { src: string; alt?: string; title?: string }) => ReturnType | Inserts an image from a URL | | setImageAlignment | (align: "left" \| "center" \| "right" \| null) => ReturnType | Sets alignment on the selected image |

HTML Parsing / Rendering

  • Parses from: <span[data-image-node]>, <div[data-image-node]>, <img[data-image-node]>
  • Renders as: <img> with data-image-node (image mode), or <span>/<div> with data-image-node (placeholder mode)

Styling

All visual styling is applied via inline CSS. You can override it by targeting the [data-image-node] attribute:

[data-image-node] {
  border-radius: 8px;
}

/* Placeholder-specific */
[data-image-node] {
  /* The placeholder is a <span> or <div> with this attribute */
}

/*
 * Dark mode support.
 *
 * The extension already references CSS variables — with light-mode
 * fallbacks — in all its inline styles.  To switch colours for dark
 * mode you only need to define the variables under whatever class or
 * attribute your app uses (e.g. `.dark`, `[data-theme="dark"]`,
 * `.theme-dark`, `body.dark`).  The new values cascade automatically;
 * no `!important` overrides are needed.
 *
 * Full list of available variables with their default (light) values:
 */
.dark {
  /* ── Placeholder ── */
  --image-ph-bg: #1f2937;
  --image-ph-border: #4b5563;
  --image-ph-icon: #9ca3af;
  --image-ph-label: #d1d5db;
  --image-ph-hint: #6b7280;

  /* ── Placeholder hover ── */
  --image-ph-hover-bg: #374151;
  --image-ph-hover-border: #6b7280;
  --image-ph-hover-icon: #e5e7eb;

  /* ── Drag-over / active ── */
  --image-ph-active-bg: #1e3a5f;
  --image-ph-active-border: #60a5fa;
  --image-ph-active-label: #60a5fa;

  /* ── Upload in progress ── */
  --image-ph-progress-bg: #1e3a5f;
  --image-ph-progress-border: #60a5fa;
  --image-ph-progress-icon: #60a5fa;
  --image-ph-progress-label: #60a5fa;

  /* ── Error state ── */
  --image-ph-error-bg: #3b1a1a;
  --image-ph-error-border: #f87171;
  --image-ph-error-text: #fca5a5;

  /* ── Close button ── */
  --image-close-bg: #4b5563;
  --image-close-color: #d1d5db;
  --image-close-hover-bg: #6b7280;
  --image-close-hover-color: #f9fafb;

  /* ── Alignment toolbar ── */
  --image-toolbar-bg: rgba(45, 45, 45, 0.92);
  --image-toolbar-btn: rgba(255, 255, 255, 0.6);
  --image-toolbar-btn-hover-bg: rgba(255, 255, 255, 0.15);
}

Or use the HTMLAttributes option to pass additional attributes:

Image.configure({
  HTMLAttributes: {
    class: "my-custom-image",
    style: "border-radius: 8px;",
  },
});

License

MIT © Explita