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

@thm-health/vue-profile-image-cropper

v1.0.0

Published

Unstyled Vue 3 profile image cropper

Readme

Vue 3 Profile Image Cropper

Minimal unstyled, accessible profile-image cropper for Vue.js 3 with zero-runtime dependencies and small in size (< 12kB).

Install

npm install @thm-health/vue-profile-image-cropper

Peer dependency: vue@^3.4.

import { ProfileImageCropper, type CropResult } from '@thm-health/vue-profile-image-cropper';

Usage

Mount only when a File is available. Own zoom bounds and controls in the parent. Use v-model:zoom with optional minZoom / maxZoom / zoomStep for wheel and +/− zoom. The classes in this example are Tailwind CSS.

<script setup lang="ts">
import { ref } from 'vue';
import { ProfileImageCropper, type CropResult } from '@thm-health/vue-profile-image-cropper';

const MIN_ZOOM = 1;
const MAX_ZOOM = 3;
const cropperRef = ref<InstanceType<typeof ProfileImageCropper> | null>(null);
const selectedImage = ref<File | null>(null);
const zoom = ref(MIN_ZOOM);

function onFileChange(event: Event) {
  const input = event.target as HTMLInputElement;
  selectedImage.value = input.files?.[0] ?? null;
  zoom.value = MIN_ZOOM;
  input.value = '';
}

async function onCropClick() {
  const result = await cropperRef.value?.cropImage();
  if (result) {
    // result.blob
  }
}

function onLoading(loading: boolean) {
  // disable controls while loading
}
</script>

<template>
  <input type="file" accept="image/*" @change="onFileChange" />

  <ProfileImageCropper
    v-if="selectedImage"
    ref="cropperRef"
    v-model:zoom="zoom"
    :min-zoom="MIN_ZOOM"
    :max-zoom="MAX_ZOOM"
    :image="selectedImage"
    aria-label="Image crop area. Drag to reposition, scroll or press +/− to zoom, arrow keys to move the image."
    class="h-[200px] w-full max-h-[200px] max-w-md rounded-xl border bg-slate-100"
    mask-class="rounded-full shadow-[0_0_0_9999px_rgb(17_24_39_/_0.55)]"
    ring-class="rounded-full border-2 border-white"
    @loading="onLoading"
  />

  <input
    type="range"
    :min="MIN_ZOOM"
    :max="MAX_ZOOM"
    step="0.1"
    v-model.number="zoom"
    :disabled="!selectedImage"
  />

  <button type="button" :disabled="!selectedImage" @click="onCropClick">Crop</button>
</template>

Props

| Name | Type | Default | Required | Description | | -------------- | ------------------------ | -------------- | -------- | ------------------------------------------------------------------------------------------ | | image | File\|HTMLImageElement | — | yes | Source image. Fixed for the component lifetime — remount (e.g. :key) to load a new file. | | zoom | number | — | yes | Zoom multiplier via v-model:zoom (typically ≥ 1). | | minZoom | number | 1 | no | Lower bound for wheel / + − zoom. | | maxZoom | number | Infinity | no | Upper bound for wheel / + − zoom. | | zoomStep | number | 0.1 | no | Zoom delta per wheel tick or + / − key. | | outputSize | number | 512 | no | Edge length in pixels of the exported square image. | | mimeType | string | 'image/jpeg' | no | MIME type used for canvas export (image/jpeg, image/png, image/webp). | | quality | number | 0.92 | no | Encoder quality for JPEG/WebP (0–1). Ignored for PNG. | | keyboardStep | number | 8 | no | Arrow-key move distance in CSS pixels. | | disabled | boolean | false | no | Disables pointer, wheel, and keyboard interactions on the viewport. | | maskClass | string | — | no | Class(es) on the crop-square mask. | | ringClass | string | — | no | Class(es) on the crop-square outline. |

Events

| Name | Type | Description | | ---------- | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | loading | boolean | Fired when decode/load state changes (true while loading, then false). | | error | Error | Fired when loading fails | | position | CropSquarePosition | Relative position of the crop square on the image (x/y 0–100, or null if that axis cannot pan, as it covers the whole image width or height). x=0: Crop square is at the left edge of the image, x=100: Crop square is at the right edge; y=0: Crop square is at the top of the image, y=100: Crop square is at the bottom of the image. Can be used to announce the position for assistive technologies. |

Exposed methods

| Name | Description | | -------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | cropImage() => Promise<CropResult \| null> | Exports the current crop; emits error on failure. Returns null on failure. CropResult: { blob: Blob } — use URL.createObjectURL(result.blob) for previews. | | cropper | (Internal) ImageCropper instance (zoom, pan, viewport, and source geometry). Does not follow Semantic Versioning |

Styling

The cropper has no size of its own. Set a height and width with class, or it is not visible. mask-class dims the area outside the crop square, and ring-class draws its edge. Without those classes, the crop area is visually unrecognizable.

Example

<ProfileImageCropper
  class="profile-image-cropper"
  mask-class="profile-image-cropper__mask"
  ring-class="profile-image-cropper__ring"
/>
<style>
  .profile-image-cropper {
    height: 200px;
    width: 100%;
    max-height: 200px;
    border: 1px solid;
  }

  .profile-image-cropper__mask {
    box-shadow: 0 0 0 9999px rgb(0 0 0 / 0.55);
  }

  .profile-image-cropper__ring {
    border: 2px solid white;
  }
</style>

Example with Tailwind CSS

<ProfileImageCropper
  class="h-[200px] w-full max-h-[200px] border"
  mask-class="shadow-[0_0_0_9999px_rgb(0_0_0_/_0.55)]"
  ring-class="border-2 border-white"
/>

Circle

cropImage() always exports a square image. To show the crop square as a circle, set border-radius: 50% on the mask and ring (Tailwind: rounded-full). That CSS changes only the overlay.

<ProfileImageCropper
  class="h-[200px] w-full max-h-[200px] border"
  mask-class="rounded-full shadow-[0_0_0_9999px_rgb(0_0_0_/_0.55)]"
  ring-class="rounded-full border-2 border-white"
/>

Accessibility

The viewport has role="application" and is in the tab order unless disabled is true. With focus on the viewport, + and − change zoom by zoomStep, and the arrow keys moves the image under that square by keyboardStep pixels.

Set aria-label so screen reader users know how to control the cropper.

<ProfileImageCropper
  aria-label="Image crop area. Drag to reposition, scroll or press +/− to zoom, arrow keys to move the image."
/>

You can also announce the crop position and zoom in a live region. position is a CropSquarePosition: x and y are 0–100, or null when that axis cannot move.

<script setup lang="ts">
import { computed, ref } from 'vue';
import { type CropSquarePosition } from '@thm-health/vue-profile-image-cropper';

const zoom = ref(1);
const position = ref<CropSquarePosition>({ x: null, y: null });

const ariaPosition = computed(() => {
  const zoomText = `Zoom ${Math.round(zoom.value * 100)} percent.`;
  if (position.value.x === null && position.value.y === null) {
    return `Crop fills the image. ${zoomText} Zoom in to reposition.`;
  }
  const x =
    position.value.x != null
      ? `Horizontal position ${Math.round(position.value.x)} percent.`
      : 'Horizontal position fixed.';
  const y =
    position.value.y != null
      ? `Vertical position ${Math.round(position.value.y)} percent.`
      : 'Vertical position fixed.';
  return `${x} ${y} ${zoomText}`;
});
</script>

<template>
  <ProfileImageCropper v-model:zoom="zoom" @position="position = $event" />
  <span aria-live="polite" aria-atomic="true" class="sr-only">{{ ariaPosition }}</span>
</template>

Development

pnpm install
pnpm dev         # demo app
pnpm build       # library bundle + types → dist/
pnpm build:demo  # demo production build
pnpm lint
pnpm format
pnpm test        # Vitest unit tests
pnpm test:e2e    # Cypress against the demo (upload, zoom, pan, crop, debug)
pnpm check       # typecheck + lint + format + unit + lib build

License

MIT