diff --git a/package-lock.json b/package-lock.json
index c347cf5fd8a28..6f82f32040b25 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -22,7 +22,7 @@
         "chartjs-adapter-dayjs-4": "1.0.4",
         "chartjs-plugin-zoom": "2.2.0",
         "clippie": "4.1.5",
-        "cropperjs": "1.6.2",
+        "cropperjs": "2.0.0",
         "css-loader": "7.1.2",
         "dayjs": "1.11.13",
         "dropzone": "6.0.0-beta.2",
@@ -433,6 +433,126 @@
         "node": ">=14.0.0"
       }
     },
+    "node_modules/@cropper/element": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element/-/element-2.0.0.tgz",
+      "integrity": "sha512-lsthn0nQq73GExUE7Mg/ss6Q3RXADGDv055hxoLFwvl/wGHgy6ZkYlfLZ/VmgBHC6jDK5IgPBFnqrPqlXWSGBA==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-canvas": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-canvas/-/element-canvas-2.0.0.tgz",
+      "integrity": "sha512-GPtGJgSm92crJhhhwUsaMw3rz2KfJWWSz7kRAlufFEV/EHTP5+6r6/Z1BCGRna830i+Avqbm435XLOtA7PVJwA==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-crosshair": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-crosshair/-/element-crosshair-2.0.0.tgz",
+      "integrity": "sha512-KfPfyrdeFvUC31Ws7ATtcalWWSaMtrC6bMoCipZhqbUOE7wZoL4ecDSL6BUOZxPa74awZUqfzirCDjHvheBfyw==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-grid": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-grid/-/element-grid-2.0.0.tgz",
+      "integrity": "sha512-i78SQ0IJTLFveKX6P7svkfMYVdgHrQ8ZmmEw8keFy9n1ZVbK+SK0UHK5FNMRNI/gtVhKJOGEnK/zeyjUdj4Iyw==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-handle": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-handle/-/element-handle-2.0.0.tgz",
+      "integrity": "sha512-ZJvW+0MkK9E8xYymGdoruaQn2kwjSHFpNSWinjyq6csuVQiCPxlX5ovAEDldmZ9MWePPtWEi3vLKQOo2Yb0T8g==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-image": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-image/-/element-image-2.0.0.tgz",
+      "integrity": "sha512-9BxiTS/aHRmrjopaFQb9mQQXmx4ruhYHGkDZMVz24AXpMFjUY6OpqrWse/WjzD9tfhMFvEdu17b3VAekcAgpeg==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/element-canvas": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-selection": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-selection/-/element-selection-2.0.0.tgz",
+      "integrity": "sha512-ensNnbIfJsJ8bhbJTH/RXtk2URFvTOO4TvfRk461n2FPEC588D7rwBmUJxQg74IiTi4y1JbCI+6j+4LyzYBLCQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/element-canvas": "^2.0.0",
+        "@cropper/element-image": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-shade": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-shade/-/element-shade-2.0.0.tgz",
+      "integrity": "sha512-jv/2bbNZnhU4W+T4G0c8ADocLIZvQFTXgCf2RFDNhI5UVxurzWBnDdb8Mx8LnVplnkTqO+xUmHZYve0CwgWo+Q==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/element-canvas": "^2.0.0",
+        "@cropper/element-selection": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/element-viewer": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/element-viewer/-/element-viewer-2.0.0.tgz",
+      "integrity": "sha512-zY+3VRN5TvpM8twlphYtXw0tzJL2VgzeK7ufhL1BixVqOdRxwP13TprYIhqwGt9EW/SyJZUiaIu396T89kRX8A==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/element-canvas": "^2.0.0",
+        "@cropper/element-image": "^2.0.0",
+        "@cropper/element-selection": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/elements": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/elements/-/elements-2.0.0.tgz",
+      "integrity": "sha512-PQkPo1nUjxLFUQuHYu+6atfHxpX9B41Xribao6wpvmvmNIFML6LQdNqqWYb6LyM7ujsu71CZdBiMT5oetjJVoQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/element": "^2.0.0",
+        "@cropper/element-canvas": "^2.0.0",
+        "@cropper/element-crosshair": "^2.0.0",
+        "@cropper/element-grid": "^2.0.0",
+        "@cropper/element-handle": "^2.0.0",
+        "@cropper/element-image": "^2.0.0",
+        "@cropper/element-selection": "^2.0.0",
+        "@cropper/element-shade": "^2.0.0",
+        "@cropper/element-viewer": "^2.0.0"
+      }
+    },
+    "node_modules/@cropper/utils": {
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/@cropper/utils/-/utils-2.0.0.tgz",
+      "integrity": "sha512-cprLYr+7kK3faGgoOsTW9gIn5sefDr2KwOmgyjzIXk+8PLpW8FgFKEg5FoWfRD5zMAmkCBuX6rGKDK3VdUEGrg==",
+      "license": "MIT"
+    },
     "node_modules/@csstools/css-parser-algorithms": {
       "version": "3.0.4",
       "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.4.tgz",
@@ -5298,10 +5418,14 @@
       }
     },
     "node_modules/cropperjs": {
-      "version": "1.6.2",
-      "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-1.6.2.tgz",
-      "integrity": "sha512-nhymn9GdnV3CqiEHJVai54TULFAE3VshJTXSqSJKa8yXAKyBKDWdhHarnlIPrshJ0WMFTGuFvG02YjLXfPiuOA==",
-      "license": "MIT"
+      "version": "2.0.0",
+      "resolved": "https://registry.npmjs.org/cropperjs/-/cropperjs-2.0.0.tgz",
+      "integrity": "sha512-TO2j0Qre01kPHbow4FuTrbdEB4jTmGRySxW49jyEIqlJZuEBfrvCTT0vC3eRB2WBXudDfKi1Onako6DKWKxeAQ==",
+      "license": "MIT",
+      "dependencies": {
+        "@cropper/elements": "^2.0.0",
+        "@cropper/utils": "^2.0.0"
+      }
     },
     "node_modules/cross-spawn": {
       "version": "7.0.6",
diff --git a/package.json b/package.json
index ef1a132994b17..3d197c9113fcc 100644
--- a/package.json
+++ b/package.json
@@ -21,7 +21,7 @@
     "chartjs-adapter-dayjs-4": "1.0.4",
     "chartjs-plugin-zoom": "2.2.0",
     "clippie": "4.1.5",
-    "cropperjs": "1.6.2",
+    "cropperjs": "2.0.0",
     "css-loader": "7.1.2",
     "dayjs": "1.11.13",
     "dropzone": "6.0.0-beta.2",
diff --git a/templates/shared/avatar_upload_crop.tmpl b/templates/shared/avatar_upload_crop.tmpl
index 2c4166fa9c93c..7f2138f60b9d2 100644
--- a/templates/shared/avatar_upload_crop.tmpl
+++ b/templates/shared/avatar_upload_crop.tmpl
@@ -4,5 +4,5 @@
 {{- /* the cropper-panel must be next sibling of the input "avatar" */ -}}
 <div class="cropper-panel tw-hidden">
 	<div class="tw-my-2">{{ctx.Locale.Tr "settings.cropper_prompt"}}</div>
-	<div class="cropper-wrapper"><img class="cropper-source" src alt></div>
+	<div class="cropper-wrapper"></div>
 </div>
diff --git a/templates/shared/user/profile_big_avatar.tmpl b/templates/shared/user/profile_big_avatar.tmpl
index 91f04e0b53dc6..d94ba0499e0b4 100644
--- a/templates/shared/user/profile_big_avatar.tmpl
+++ b/templates/shared/user/profile_big_avatar.tmpl
@@ -1,7 +1,7 @@
 <div id="profile-avatar-card" class="ui card">
 	<div id="profile-avatar" class="content tw-flex">
 	{{if eq .SignedUserID .ContextUser.ID}}
-		<a class="image" href="{{AppSubUrl}}/user/settings" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
+		<a class="image" href="{{AppSubUrl}}/user/settings#update-avatar" data-tooltip-content="{{ctx.Locale.Tr "user.change_avatar"}}">
 			{{/* the size doesn't take affect (and no need to take affect), image size(width) should be controlled by the parent container since this is not a flex layout*/}}
 			{{ctx.AvatarUtils.Avatar .ContextUser 256}}
 		</a>
diff --git a/templates/user/settings/profile.tmpl b/templates/user/settings/profile.tmpl
index 03c3c18f28c1c..abf9a3f16add9 100644
--- a/templates/user/settings/profile.tmpl
+++ b/templates/user/settings/profile.tmpl
@@ -97,7 +97,7 @@
 			</form>
 		</div>
 
-		<h4 class="ui top attached header">
+		<h4 class="ui top attached header" id="update-avatar">
 			{{ctx.Locale.Tr "settings.avatar"}}
 		</h4>
 		<div class="ui attached segment">
diff --git a/web_src/css/features/cropper.css b/web_src/css/features/cropper.css
index f7f8168006056..c9281af7d25bd 100644
--- a/web_src/css/features/cropper.css
+++ b/web_src/css/features/cropper.css
@@ -1,6 +1,8 @@
-@import "cropperjs/dist/cropper.css";
-
 .avatar-file-with-cropper + .cropper-panel .cropper-wrapper {
-  max-width: 400px;
+  width: 400px;
   max-height: 400px;
 }
+
+cropper-canvas {
+  height: 400px;
+}
diff --git a/web_src/js/features/comp/Cropper.ts b/web_src/js/features/comp/Cropper.ts
index aaa169115238d..2b4afe21d6165 100644
--- a/web_src/js/features/comp/Cropper.ts
+++ b/web_src/js/features/comp/Cropper.ts
@@ -1,47 +1,67 @@
-import {showElem, type DOMEvent} from '../../utils/dom.ts';
+import {createElementFromHTML, hideElem, showElem, type DOMEvent} from '../../utils/dom.ts';
+import {debounce} from 'perfect-debounce';
+import type {CropperCanvas, CropperSelection} from 'cropperjs';
 
 type CropperOpts = {
   container: HTMLElement,
-  imageSource: HTMLImageElement,
+  wrapper: HTMLDivElement,
   fileInput: HTMLInputElement,
 }
 
-async function initCompCropper({container, fileInput, imageSource}: CropperOpts) {
-  const {default: Cropper} = await import(/* webpackChunkName: "cropperjs" */'cropperjs');
-  let currentFileName = '';
-  let currentFileLastModified = 0;
-  const cropper = new Cropper(imageSource, {
-    aspectRatio: 1,
-    viewMode: 2,
-    autoCrop: false,
-    crop() {
-      const canvas = cropper.getCroppedCanvas();
+async function initCompCropper({container, fileInput, wrapper}: CropperOpts) {
+  await import(/* webpackChunkName: "cropperjs" */'cropperjs');
+
+  fileInput.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => {
+    if (!e.target.files?.length) {
+      wrapper.replaceChildren();
+      hideElem(container);
+      return;
+    }
+
+    const [file] = e.target.files;
+    const objectUrl = URL.createObjectURL(file);
+    const cropperCanvas = createElementFromHTML<CropperCanvas>(`
+      <cropper-canvas theme-color="var(--color-primary)">
+        <cropper-image src="${objectUrl}" scalable skewable translatable></cropper-image>
+        <cropper-shade hidden></cropper-shade>
+        <cropper-handle action="select" plain></cropper-handle>
+        <cropper-selection aspect-ratio="1" movable resizable>
+          <cropper-handle action="move" theme-color="transparent"></cropper-handle>
+          <cropper-handle action="n-resize"></cropper-handle>
+          <cropper-handle action="e-resize"></cropper-handle>
+          <cropper-handle action="s-resize"></cropper-handle>
+          <cropper-handle action="w-resize"></cropper-handle>
+          <cropper-handle action="ne-resize"></cropper-handle>
+          <cropper-handle action="nw-resize"></cropper-handle>
+          <cropper-handle action="se-resize"></cropper-handle>
+          <cropper-handle action="sw-resize"></cropper-handle>
+        </cropper-selection>
+      </cropper-canvas>
+    `);
+    cropperCanvas.querySelector<CropperSelection>('cropper-selection').addEventListener('change', debounce(async (e) => {
+      const selection = e.target as CropperSelection;
+      if (!selection.width || !selection.height) return;
+      const canvas = await selection.$toCanvas();
+
       canvas.toBlob((blob) => {
-        const croppedFileName = currentFileName.replace(/\.[^.]{3,4}$/, '.png');
-        const croppedFile = new File([blob], croppedFileName, {type: 'image/png', lastModified: currentFileLastModified});
         const dataTransfer = new DataTransfer();
-        dataTransfer.items.add(croppedFile);
+        dataTransfer.items.add(new File(
+          [blob],
+          file.name.replace(/\.[^.]{3,4}$/, '.png'),
+          {type: 'image/png', lastModified: file.lastModified},
+        ));
         fileInput.files = dataTransfer.files;
       });
-    },
-  });
+    }, 200));
 
-  fileInput.addEventListener('input', (e: DOMEvent<Event, HTMLInputElement>) => {
-    const files = e.target.files;
-    if (files?.length > 0) {
-      currentFileName = files[0].name;
-      currentFileLastModified = files[0].lastModified;
-      const fileURL = URL.createObjectURL(files[0]);
-      imageSource.src = fileURL;
-      cropper.replace(fileURL);
-      showElem(container);
-    }
+    wrapper.replaceChildren(cropperCanvas);
+    showElem(container);
   });
 }
 
 export async function initAvatarUploaderWithCropper(fileInput: HTMLInputElement) {
   const panel = fileInput.nextElementSibling as HTMLElement;
   if (!panel?.matches('.cropper-panel')) throw new Error('Missing cropper panel for avatar uploader');
-  const imageSource = panel.querySelector<HTMLImageElement>('.cropper-source');
-  await initCompCropper({container: panel, fileInput, imageSource});
+  const wrapper = panel.querySelector<HTMLImageElement>('.cropper-wrapper');
+  await initCompCropper({container: panel, fileInput, wrapper});
 }