diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 4dc076a955650839013721b4fa118ae0df6cfcbd..38fb0f23de9d116a8323d9da9041d086fbd66dd7 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -15,6 +15,7 @@ stages:
     - build
     - test
     - test-ext
+    - package
     - release-prep
     - upload
 
@@ -376,6 +377,37 @@ webassembly:test:
     needs:
         - webassembly:build
 
+## Webassembly package
+webassembly-wheel:package:
+    extends:
+        - .webassembly
+        - .cmake_package_webassembly_wheel_linux
+        - .linux_packager_tags
+        - .cmake_webassembly_wheel_artifacts
+        - .run_automatically
+    dependencies:
+        - webassembly:build
+    needs:
+        - webassembly:build
+
+webassembly-wheel:pypi:upload:
+    extends:
+        - .pypi_webassembly_upload
+        - .tag_only
+    dependencies:
+        - webassembly-wheel:package
+    needs:
+        - webassembly-wheel:package
+
+webassembly-wheel:vtk:upload:
+    extends:
+        - .pypi_webassembly_vtk_upload
+        - .weekly_upload_only
+    dependencies:
+        - webassembly-wheel:package
+    needs:
+        - webassembly-wheel:package
+
 ## Python wheel builds
 
 wheel-linux-python36-x86_64:build:
diff --git a/.gitlab/artifacts.yml b/.gitlab/artifacts.yml
index a4d7e2ba793aeb7629eb64a32210b3210678177b..882688a7aa81447fc9c96f141c7f24911d12dcc9 100644
--- a/.gitlab/artifacts.yml
+++ b/.gitlab/artifacts.yml
@@ -74,6 +74,9 @@
             # SPDX files
             - build/**/*.spdx
 
+            # WebAssembly
+            - build/Web/WebAssembly/
+
 .cmake_junit_artifacts:
     extends: .cmake_annotations_artifacts
     artifacts:
@@ -136,6 +139,14 @@
             - build/dist/*.whl
             - build/*-wheel-sdk-*.tar.xz
 
+.cmake_webassembly_wheel_artifacts:
+    artifacts:
+        expire_in: 1d
+        when: always
+        paths:
+            - build/Web/WebAssembly/Wheel/dist/*.whl
+            - build/Web/WebAssembly/Wheel/dist/*.tar.gz
+
 .cmake_release_artifacts:
     extends: .cmake_annotations_artifacts
     artifacts:
diff --git a/.gitlab/os-linux.yml b/.gitlab/os-linux.yml
index 34697fad00d7edc8a688630ed36cf6afc76ec2aa..9d0efd4928d9426a551c21c6346acef13c719dbb 100644
--- a/.gitlab/os-linux.yml
+++ b/.gitlab/os-linux.yml
@@ -479,6 +479,13 @@
         - x11
         - vtk
 
+.linux_packager_tags:
+    tags:
+        - build
+        - docker
+        - linux-x86_64
+        - vtk
+
 ## Linux-specific scripts
 
 .before_script_linux: &before_script_linux
@@ -692,6 +699,22 @@
 
     interruptible: true
 
+.cmake_package_webassembly_wheel_linux:
+    stage: package
+
+    script:
+        - apt update
+        - apt install -y python3.10-venv
+        - find build/bin/ -regex '.*\.\(js\|mjs\|wasm\)$' -exec cp -v {} build/Web/WebAssembly/Wheel/vtk_wasm/ \;
+        - cd build/Web/WebAssembly/Wheel
+        - python -m venv venv
+        - . venv/bin/activate
+        - python -m pip install --upgrade --no-cache-dir pip setuptools
+        - python -m pip install build
+        - python -m build .
+
+    interruptible: true
+
 .cmake_test_webassembly_linux:
     stage: test
     extends: .warning_policy
diff --git a/.gitlab/upload.yml b/.gitlab/upload.yml
index cf521ecfa00c51a6774dce56f8ebb7427963f326..7752d16d2a1b4d99b200225ce507f0a63910a60a 100644
--- a/.gitlab/upload.yml
+++ b/.gitlab/upload.yml
@@ -70,3 +70,33 @@
         - ls dist
         - dnf install -y --setopt=install_weak_deps=False twine
         - twine upload -u gitlab-ci-token -p $CI_JOB_TOKEN --repository-url https://gitlab.kitware.com/api/v4/projects/$CI_PROJECT_ID/packages/pypi dist/*
+
+.pypi_webassembly_upload:
+    image: "fedora:34"
+    stage: upload
+    tags:
+        - docker
+        - linux-x86_64
+        - build
+    environment:
+        name: pypi-upload
+
+    script:
+        - cd build/Web/WebAssembly/Wheel
+        - ls dist
+        - dnf install -y --setopt=install_weak_deps=False twine
+        - twine upload -u __token__ -p $PYPI_UPLOAD_TOKEN dist/* # can the same token be used?
+
+.pypi_webassembly_vtk_upload:
+    image: "fedora:34"
+    stage: upload
+    tags:
+        - docker
+        - linux-x86_64
+        - build
+
+    script:
+        - cd build/Web/WebAssembly/Wheel
+        - ls dist
+        - dnf install -y --setopt=install_weak_deps=False twine
+        - twine upload -u gitlab-ci-token -p $CI_JOB_TOKEN --repository-url https://gitlab.kitware.com/api/v4/projects/$CI_PROJECT_ID/packages/pypi dist/*
diff --git a/Web/WebAssembly/Packaging/Wheel/setup.cfg.in b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in
index 33744c4d6b7d2671c5778df021e363e7ce576654..63da35c0a04d468fd6803d21d721b73a49ee3281 100644
--- a/Web/WebAssembly/Packaging/Wheel/setup.cfg.in
+++ b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in
@@ -24,7 +24,7 @@ keywords =
 packages = find:
 include_package_data = True
 install_requires =
-    vtk==@VTK_VERSION@
+    vtk==@VTK_MAJOR_VERSION@.@VTK_MINOR_VERSION@.*
 
 [semantic_release]
 version_pattern = setup.cfg:version = (\d+\.\d+\.\d+)