diff --git a/CMake/FindNodeJS.cmake b/CMake/FindNodeJS.cmake
new file mode 100644
index 0000000000000000000000000000000000000000..7b36ca67b9ca85c622db70088b0a5e93694cc1e9
--- /dev/null
+++ b/CMake/FindNodeJS.cmake
@@ -0,0 +1,31 @@
+#[==[
+Provides the following variables:
+
+  * `NodeJS_FOUND`: Whether NodeJS was found or not.
+  * `NodeJS_INTERPRETER`: Path to the `node` interpreter.
+#]==]
+find_program (NodeJS_INTERPRETER
+  NAMES node nodejs
+  HINTS
+    "$ENV{NODE_DIR}/bin"
+  DOC
+    "Node.js interpreter")
+
+include (FindPackageHandleStandardArgs)
+
+if (NodeJS_INTERPRETER)
+  execute_process(COMMAND "${NodeJS_INTERPRETER}" --version
+      OUTPUT_VARIABLE _nodejs_version
+      RESULT_VARIABLE _nodejs_version_result)
+  if (NOT _nodejs_version_result)
+      string(REGEX MATCH "v([0-9]+)\\.([0-9]+)\\.([0-9]+)" _nodejs_version_match "${_nodejs_version}")
+      set(_nodejs_version_major ${CMAKE_MATCH_1})
+      set(_nodejs_version_minor ${CMAKE_MATCH_2})
+      set(_nodejs_version_patch ${CMAKE_MATCH_3})
+      set(_nodejs_version_string "${_nodejs_version_major}.${_nodejs_version_minor}.${_nodejs_version_patch}")
+  endif ()
+endif ()
+
+find_package_handle_standard_args (NodeJS
+  REQUIRED_VARS NodeJS_INTERPRETER
+  VERSION_VAR _nodejs_version_string)
diff --git a/CMake/vtkModuleTesting.cmake b/CMake/vtkModuleTesting.cmake
index fb44139872a01324bbd7cd2e6ffb912ff2ccbb36..644c0f0c5f1382243f1c9701e5057de237fd014f 100644
--- a/CMake/vtkModuleTesting.cmake
+++ b/CMake/vtkModuleTesting.cmake
@@ -117,6 +117,18 @@ function (vtk_module_test_executable name)
     PRIVATE
       ${optional_depends_flags})
 
+  # (vtk/vtk#19097) Although vtk_add_test_cxx skips C++ tests for wasm,
+  # some modules bypass `vtk_add_test_cxx` by directly invoking `ExternalData_add_test`
+  # for special tests, ex: ImagingCore module does it.
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    # allows the test executable to access host file system for test data.
+    # permit memory growth so that test doesn't run out of memory.
+    target_link_options("${name}"
+      PRIVATE
+        "SHELL:-s ALLOW_MEMORY_GROWTH=1"
+        "SHELL:-s MAXIMUM_MEMORY=4GB"
+        "SHELL:-s NODERAWFS=1")
+  endif ()
   vtk_module_autoinit(
     TARGETS "${name}"
     MODULES "${_vtk_build_test}"
@@ -394,14 +406,24 @@ function (vtk_add_test_cxx exename _tests)
         ${MPIEXEC_PREFLAGS})
     endif()
 
-    ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}"
-      NAME    "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}"
-      COMMAND "${_vtk_test_cxx_pre_args}" "$<TARGET_FILE:${exename}>"
-              "${test_arg}"
-              "${args}"
-              ${${_vtk_build_test}_ARGS}
-              ${${test_name}_ARGS}
-              ${_D} ${_T} ${_V})
+    # (vtk/vtk#19097) Disable C++ testing for wasm architecture until
+    # https://gitlab.kitware.com/vtk/vtk/-/issues/19097 is resolved.
+    if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+      ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}"
+        NAME    "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}"
+        COMMAND ${CMAKE_CROSSCOMPILING_EMULATOR}
+                "--eval"
+                "process.exit(125);") # all tests are skipped.
+    else ()
+      ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}"
+        NAME    "${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}"
+        COMMAND "${_vtk_test_cxx_pre_args}" "$<TARGET_FILE:${exename}>"
+                "${test_arg}"
+                "${args}"
+                ${${_vtk_build_test}_ARGS}
+                ${${test_name}_ARGS}
+                ${_D} ${_T} ${_V})
+    endif ()
     set_tests_properties("${_vtk_build_test}Cxx-${vtk_test_prefix}${test_name}"
       PROPERTIES
         LABELS "${_vtk_build_test_labels}"
@@ -418,6 +440,11 @@ function (vtk_add_test_cxx exename _tests)
           ENVIRONMENT "LD_PRELOAD=${_vtk_testing_ld_preload}")
     endif ()
 
+    # (vtk/vtk#19097) Disable C++ testing for wasm architecture until
+    # https://gitlab.kitware.com/vtk/vtk/-/issues/19097 is resolved.
+    if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+      continue ()
+    endif ()
     list(APPEND ${_tests} "${test_file}")
   endforeach ()
 
@@ -816,6 +843,92 @@ function (vtk_add_test_python)
   endforeach ()
 endfunction ()
 
+#[==[.rst:
+JavaScript tests
+^^^^^^^^^^^^^^^^^^^^^^^^^
+
+.. cmake:command:: vtk_add_test_module_javascript_node
+
+  This function declares JavaScript tests run with NodeJS.
+  Test files are required to use the `mjs` extension.
+  Additional arguments to `node` can be passed via `_vtk_node_args` variable.
+
+  .. code-block:: cmake
+
+    vtk_add_test_module_javascript_node(<VARNAME> <ARG>...)
+#]==]
+
+#[==[.rst:
+The ``_vtk_testing_nodejs_exe`` variable must point to the path of a `node` interpreter.
+#]==]
+
+#[==[.rst
+Options:
+
+- ``NO_DATA``
+- ``NO_OUTPUT``
+
+Each argument should be either an option, a test specification, or it is passed
+as flags to all tests declared in the group. The list of tests is set in the
+``<VARNAME>`` variable in the calling scope.
+
+Options:
+
+- ``NO_DATA``: The test does not need to know the test input data directory. If
+  it does, it is passed on the command line via the ``-D`` flag.
+- ``NO_OUTPUT``: The test does not need to write out any data to the
+  filesystem. If it does, a directory which may be written to is passed via
+  the ``-T`` flag.
+
+Additional flags may be passed to tests using the ``${_vtk_build_test}_ARGS``
+variable or the ``<NAME>_ARGS`` variable.
+#]==]
+function (vtk_add_test_module_javascript_node)
+  if (NOT _vtk_testing_nodejs_exe)
+    message(FATAL_ERROR "The \"_vtk_testing_nodejs_exe\" variable must point to a nodejs executable!")
+  endif ()
+  set(mjs_options
+    NO_DATA
+    NO_OUTPUT)
+  _vtk_test_parse_args("${mjs_options}" "mjs" ${ARGN})
+  _vtk_test_set_options("${mjs_options}" "" ${options})
+
+  set(_vtk_fail_regex
+    # vtkLogger
+    "(\n|^)ERROR: "
+    "ERR\\|"
+    # vtkDebugLeaks
+    "instance(s)? still around")
+
+  foreach (name IN LISTS names)
+    _vtk_test_set_options("${mjs_options}" "local_" ${_${name}_options})
+    _vtk_test_parse_name("${name}" "mjs")
+    set(_D "")
+    if (NOT local_NO_DATA)
+      set(_D -D "${_vtk_build_TEST_OUTPUT_DATA_DIRECTORY}")
+    endif ()
+
+    set(_T "")
+    if (NOT local_NO_OUTPUT)
+      set(_T -T "${_vtk_build_TEST_OUTPUT_DIRECTORY}")
+    endif ()
+    ExternalData_add_test("${_vtk_build_TEST_DATA_TARGET}"
+      NAME    "${_vtk_build_test}JavaScript-${test_name}"
+      WORKING_DIRECTORY "${CMAKE_CURRENT_SOURCE_DIR}"
+      COMMAND ${_vtk_testing_nodejs_exe}
+              "${_vtk_node_args}"
+              "${test_file}"
+              ${${_vtk_build_test}_ARGS}
+              ${${test_name}_ARGS}
+              ${_D} ${_T})
+    set_tests_properties("${_vtk_build_test}JavaScript-${test_name}"
+      PROPERTIES
+        LABELS "${_vtk_build_test_labels}"
+        FAIL_REGULAR_EXPRESSION "${_vtk_fail_regex}"
+        SKIP_RETURN_CODE 125)
+  endforeach ()
+endfunction ()
+
 #[==[.rst:
 MPI tests
 """""""""
diff --git a/CMakeLists.txt b/CMakeLists.txt
index 7d9e21b5702ec7fb0d6823160dfb6235aef0d4cd..a1860a9be21399bde50979e8da2f4be180df9d57 100644
--- a/CMakeLists.txt
+++ b/CMakeLists.txt
@@ -275,6 +275,15 @@ set("_vtk_module_reason_VTK::Java"
 if (VTK_WRAP_SERIALIZATION)
   list(APPEND vtk_requested_modules
     VTK::SerializationManager)
+  if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+    list(APPEND vtk_requested_modules
+      VTK::WebAssembly)
+  else ()
+    list(APPEND vtk_rejected_modules
+      VTK::WebAssembly)
+  endif ()
+  set("_vtk_module_reason_VTK::WebAssembly"
+  "via `VTK_WRAP_SERIALIZATION` AND `CMAKE_SYSTEM_NAME` STREQUAL Emscripten")
 else ()
   list(APPEND vtk_rejected_modules
     VTK::SerializationManager)
diff --git a/Filters/CellGrid/Testing/Cxx/CMakeLists.txt b/Filters/CellGrid/Testing/Cxx/CMakeLists.txt
index affd849747f2ed51f41fb7a1942f223b042c215b..1b0ec9c715e85cd088c26573326a685402deb46a 100644
--- a/Filters/CellGrid/Testing/Cxx/CMakeLists.txt
+++ b/Filters/CellGrid/Testing/Cxx/CMakeLists.txt
@@ -4,4 +4,3 @@ vtk_add_test_cxx(vtkFiltersCellGridCxxTests tests
   TestDGCells.cxx,NO_VALID
 )
 vtk_test_cxx_executable(vtkFiltersCellGridCxxTests tests)
-set_target_properties(vtkFiltersCellGridCxxTests PROPERTIES LINKER_LANGUAGE CXX)
diff --git a/IO/Import/Testing/Cxx/CMakeLists.txt b/IO/Import/Testing/Cxx/CMakeLists.txt
index acac5170a0ce6d6d6111ed1ab0884f25fd9102ae..17f53d2916b76a6584b4118fd4792160a72b33a3 100644
--- a/IO/Import/Testing/Cxx/CMakeLists.txt
+++ b/IO/Import/Testing/Cxx/CMakeLists.txt
@@ -68,8 +68,11 @@ vtk_add_test_cxx(vtkIOImportCxxTests tests
   TestOBJImporter-Malformed,TestOBJImporter.cxx,NO_VALID
     DATA{../Data/Input/malformed.obj}
 )
-set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES PASS_REGULAR_EXPRESSION "Unexpected point indice value")
-set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES FAIL_REGULAR_EXPRESSION "")
+# (vtk/vtk#19097) This test is skipped on wasm architecture.
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten") 
+  set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES PASS_REGULAR_EXPRESSION "Unexpected point indice value")
+  set_tests_properties(VTK::IOImportCxx-TestOBJImporter-Malformed PROPERTIES FAIL_REGULAR_EXPRESSION "")
+endif ()  
 
 vtk_add_test_cxx(vtkIOImportCxxTests tests
   TestVRMLImporter.cxx,NO_VALID
diff --git a/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512 b/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512
new file mode 100644
index 0000000000000000000000000000000000000000..12a0fef282c167d19d62be3846c4e75368302d7c
--- /dev/null
+++ b/Testing/Data/WasmSceneManager/scalar-bar-widget.blobs.json.sha512
@@ -0,0 +1 @@
+87ac7e8caa53b1d049be54d5eb4496be6e45ab5af428af53d8844ead7a49edaa5b0926897c0290a3a0014583add415adf12211bd473f1d72d1feeeacd08abf84
diff --git a/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512 b/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512
new file mode 100644
index 0000000000000000000000000000000000000000..a5c40fefdd3b8db9226297c217e40f3ed50ebbd6
--- /dev/null
+++ b/Testing/Data/WasmSceneManager/scalar-bar-widget.states.json.sha512
@@ -0,0 +1 @@
+adfab1e7584c77e82cdf91a5b18a65e087414634976f45a673988eb0a71f44674cbafbe4d86192093d66ae3e8f2aa742cf1a8e0b159d6bbd89ec0b5933ef8470
diff --git a/Testing/Data/WasmSceneManager/simple.blobs.json.sha512 b/Testing/Data/WasmSceneManager/simple.blobs.json.sha512
new file mode 100644
index 0000000000000000000000000000000000000000..c291f03d6ae1971cd1a4a598f624104956863048
--- /dev/null
+++ b/Testing/Data/WasmSceneManager/simple.blobs.json.sha512
@@ -0,0 +1 @@
+be824ebdcd14cf7a4d89673000988ec04e1cce448aba7a3ab33712aae3fd685e537a52ea724f75c6518c5291d143bc5c79e86cdf93b627d6a80b7cea4c9e2917
diff --git a/Web/WebAssembly/CMakeLists.txt b/Web/WebAssembly/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..7f33f84bf3a32c35c95d11c36302f41cf1a46fc4
--- /dev/null
+++ b/Web/WebAssembly/CMakeLists.txt
@@ -0,0 +1,116 @@
+if (NOT CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  message(FATAL_ERROR
+    "The VTK::WebAssembly module requires Emscripten compiler.")
+endif ()
+
+set(classes
+  vtkWasmSceneManager)
+
+vtk_module_add_module(VTK::WebAssembly
+  CLASSES ${classes})
+
+vtk_add_test_mangling(VTK::WebAssembly)
+
+configure_file("Packaging/Wheel/__init__.py.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/Wheel/vtk_wasm/__init__.py")
+configure_file("Packaging/Wheel/MANIFEST.in.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/Wheel/MANIFEST.in")
+configure_file("Packaging/Wheel/README.md.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/Wheel/README.md")
+configure_file("Packaging/Wheel/setup.cfg.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/Wheel/setup.cfg")
+configure_file("Packaging/Wheel/setup.py.in"
+  "${CMAKE_CURRENT_BINARY_DIR}/Wheel/setup.py")
+
+# -----------------------------------------------------------------------------
+# Emscripten compile+link options
+# -----------------------------------------------------------------------------
+set(emscripten_link_options)
+list(APPEND emscripten_link_options
+  "-lembind"
+  "--extern-post-js=${CMAKE_CURRENT_SOURCE_DIR}/post.js"
+  # "--embind-emit-tsd=vtkWasmSceneManager.ts"
+  #"--memoryprofiler"
+  #"--cpuprofiler"
+  "-sALLOW_MEMORY_GROWTH=1"
+  "-sALLOW_TABLE_GROWTH=1"
+  "-sEXPORT_NAME=vtkWasmSceneManager"
+  "-sENVIRONMENT=node,web"
+  "-sEXPORTED_RUNTIME_METHODS=['addFunction','UTF8ToString']"
+  # "-sEXCEPTION_DEBUG=1" # prints stack trace for uncaught C++ exceptions from VTK (very rare, but PITA to figure out)
+  # "-sGL_DEBUG=1"
+  # "-sGL_ASSERTIONS=1"
+  # "-sTRACE_WEBGL_CALLS=1"
+  "-sMAXIMUM_MEMORY=4GB")
+if (CMAKE_SIZEOF_VOID_P EQUAL "8")
+  list(APPEND emscripten_link_options
+  "-sMAXIMUM_MEMORY=16GB"
+  "-sWASM_BIGINT=1")
+endif ()
+# -----------------------------------------------------------------------------
+# Optimizations
+# -----------------------------------------------------------------------------
+set(emscripten_optimizations)
+set(emscripten_debug_options)
+set(vtk_scene_manager_wasm_optimize "BEST")
+set(vtk_scene_manager_wasm_optimize_NO_OPTIMIZATION "-O0")
+set(vtk_scene_manager_wasm_optimize_LITTLE "-O1")
+set(vtk_scene_manager_wasm_optimize_MORE "-O2")
+set(vtk_scene_manager_wasm_optimize_BEST "-O3")
+set(vtk_scene_manager_wasm_optimize_SMALLEST "-Os")
+set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE "-Oz")
+set(vtk_scene_manager_wasm_optimize_SMALLEST_WITH_CLOSURE_link "--closure=1")
+
+if (DEFINED "vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}")
+  list(APPEND emscripten_optimizations
+    ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}})
+  list(APPEND emscripten_link_options
+    ${vtk_scene_manager_wasm_optimize_${vtk_scene_manager_wasm_optimize}_link})
+else ()
+  message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_optimize=${vtk_scene_manager_wasm_optimize}")
+endif ()
+
+set(vtk_scene_manager_wasm_debuginfo "NONE")
+set(vtk_scene_manager_wasm_debuginfo_NONE "-g0")
+set(vtk_scene_manager_wasm_debuginfo_READABLE_JS "-g1")
+set(vtk_scene_manager_wasm_debuginfo_PROFILE "-g2")
+set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE "-g3")
+set(vtk_scene_manager_wasm_debuginfo_DEBUG_NATIVE_link "-sASSERTIONS=1")
+if (DEFINED "vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}")
+  list(APPEND emscripten_debug_options
+    ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}})
+  list(APPEND emscripten_link_options
+    ${vtk_scene_manager_wasm_debuginfo_${vtk_scene_manager_wasm_debuginfo}_link})
+else ()
+  message (FATAL_ERROR "Unrecognized value for vtk_scene_manager_wasm_debuginfo=${vtk_scene_manager_wasm_debuginfo}")
+endif ()
+
+vtk_module_add_executable(WasmSceneManager
+  BASENAME vtkWasmSceneManager
+  vtkWasmSceneManagerEmBinding.cxx)
+target_link_libraries(WasmSceneManager
+  PRIVATE
+    VTK::WebAssembly
+    VTK::RenderingOpenGL2
+    VTK::RenderingUI)
+add_executable("VTK::WasmSceneManager" ALIAS
+  WasmSceneManager)
+target_compile_options(WasmSceneManager
+  PRIVATE
+    ${emscripten_compile_options}
+    ${emscripten_optimizations}
+    ${emscripten_debug_options})
+target_link_options(WasmSceneManager
+  PRIVATE
+    ${emscripten_link_options}
+    ${emscripten_optimizations}
+    ${emscripten_debug_options})
+set_target_properties(WasmSceneManager
+  PROPERTIES
+  SUFFIX ".mjs")
+# [cmake/cmake#20745](https://gitlab.kitware.com/cmake/cmake/-/issues/20745)
+# CMake doesn't install multiple files associated with an executable target.
+get_target_property(_vtk_scene_manager_version_suffix WebAssembly VERSION)
+install(FILES
+  "$<TARGET_FILE_DIR:WasmSceneManager>/vtkWasmSceneManager-${_vtk_scene_manager_version_suffix}.wasm"
+  DESTINATION ${CMAKE_INSTALL_BINDIR})
diff --git a/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in b/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in
new file mode 100644
index 0000000000000000000000000000000000000000..7ad8585a49c324dfefc81a2666783b7d7d75ef36
--- /dev/null
+++ b/Web/WebAssembly/Packaging/Wheel/MANIFEST.in.in
@@ -0,0 +1 @@
+graft vtk_wasm
diff --git a/Web/WebAssembly/Packaging/Wheel/README.md.in b/Web/WebAssembly/Packaging/Wheel/README.md.in
new file mode 100644
index 0000000000000000000000000000000000000000..0f177166f7afadb686094f2db55038a39692b9a0
--- /dev/null
+++ b/Web/WebAssembly/Packaging/Wheel/README.md.in
@@ -0,0 +1,3 @@
+# vtk-wasm
+
+VTK WebAssembly binaries with glue javacript libraries.
diff --git a/Web/WebAssembly/Packaging/Wheel/__init__.py.in b/Web/WebAssembly/Packaging/Wheel/__init__.py.in
new file mode 100644
index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391
diff --git a/Web/WebAssembly/Packaging/Wheel/setup.cfg.in b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in
new file mode 100644
index 0000000000000000000000000000000000000000..33744c4d6b7d2671c5778df021e363e7ce576654
--- /dev/null
+++ b/Web/WebAssembly/Packaging/Wheel/setup.cfg.in
@@ -0,0 +1,30 @@
+[metadata]
+name = vtk-wasm
+version = @VTK_VERSION@
+description = VTK WebAssembly binaries with glue javacript libraries
+long_description = file: README.md
+long_description_content_type = text/x-md
+author = Kitware Inc.
+license = BSD License
+classifiers =
+    Environment :: Web Environment
+    License :: OSI Approved :: BSD License
+    Natural Language :: English
+    Operating System :: OS Independent
+    Programming Language :: JavaScript
+    Programming Language :: C++
+    Topic :: Software Development :: Libraries :: Application Frameworks
+    Topic :: Software Development :: Libraries :: Python Modules
+keywords =
+    WebAssembly
+    Web
+    Visualization
+
+[options]
+packages = find:
+include_package_data = True
+install_requires =
+    vtk==@VTK_VERSION@
+
+[semantic_release]
+version_pattern = setup.cfg:version = (\d+\.\d+\.\d+)
diff --git a/Web/WebAssembly/Packaging/Wheel/setup.py.in b/Web/WebAssembly/Packaging/Wheel/setup.py.in
new file mode 100644
index 0000000000000000000000000000000000000000..606849326a4002007fd42060b51e69a19c18675c
--- /dev/null
+++ b/Web/WebAssembly/Packaging/Wheel/setup.py.in
@@ -0,0 +1,3 @@
+from setuptools import setup
+
+setup()
diff --git a/Web/WebAssembly/Testing/CMakeLists.txt b/Web/WebAssembly/Testing/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..a82329dba97b9e1271a0441a5cf0e4614c49a824
--- /dev/null
+++ b/Web/WebAssembly/Testing/CMakeLists.txt
@@ -0,0 +1,8 @@
+vtk_module_test_data(
+  Data/WasmSceneManager/scalar-bar-widget.blobs.json
+  Data/WasmSceneManager/scalar-bar-widget.states.json
+  Data/WasmSceneManager/simple.blobs.json)
+
+if (CMAKE_SYSTEM_NAME STREQUAL "Emscripten")
+  add_subdirectory(JavaScript)
+endif ()
diff --git a/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt
new file mode 100644
index 0000000000000000000000000000000000000000..9e45a96cd06c06b9e79404a709870a535f57843d
--- /dev/null
+++ b/Web/WebAssembly/Testing/JavaScript/CMakeLists.txt
@@ -0,0 +1,10 @@
+set(vtk_nodejs_min_version "18.8.0")
+find_package(NodeJS "${vtk_nodejs_min_version}" REQUIRED)
+set(_vtk_testing_nodejs_exe "${NodeJS_INTERPRETER}")
+
+set(_vtk_node_args
+  --import "${PROJECT_BINARY_DIR}/bin/vtkWasmSceneManager-${VTK_MAJOR_VERSION}.${VTK_MINOR_VERSION}.mjs")
+vtk_add_test_module_javascript_node(
+  testInitialize.mjs,NO_DATA
+  testBlobs.mjs,
+  testStates.mjs)
diff --git a/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..a1ddaf42829bbe606bea4d5eecc42223750feb67
--- /dev/null
+++ b/Web/WebAssembly/Testing/JavaScript/testBlobs.mjs
@@ -0,0 +1,52 @@
+import { readFile } from "fs/promises";
+import path from "path";
+
+async function testBlobs() {
+  const dataDirectoryIndex = process.argv.indexOf("-D") + 1;
+  if (dataDirectoryIndex <= 0) {
+    throw new Error("Please provide path to a blobs file using -D");
+  }
+  const dataDirectory = process.argv[dataDirectoryIndex];
+  const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "simple.blobs.json")));
+  const manager = await globalThis.createVTKWasmSceneManager({})
+  if (!manager.initialize()) {
+    throw new Error("Failed to initialize scene manager");
+  }
+
+  for (let hash in blobs) {
+    if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) {
+      throw new Error(`Failed to register blob with hash=${hash}`);
+    }
+  }
+  for (let hash in blobs) {
+    const blob = manager.getBlob(hash);
+    if (!(blob instanceof Uint8Array)) {
+      throw new Error(`getBlob did not return a Uint8Array for hash=${hash}`);
+    }
+    if (blob.toString() !== blobs[hash].bytes.toString()) {
+      throw new Error(`blob for hash=${hash} does not match registered blob.`);
+    }
+  }
+}
+
+const tests = [
+  {
+    description: "Register blobs with hashes",
+    test: testBlobs,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..acdae02800277c7c5dc445214149b34638c2a478
--- /dev/null
+++ b/Web/WebAssembly/Testing/JavaScript/testInitialize.mjs
@@ -0,0 +1,27 @@
+async function testInitialize() {
+  const manager = await globalThis.createVTKWasmSceneManager({});
+  if (!manager.initialize()) {
+    throw new Error();
+  }
+}
+const tests = [
+  {
+    description: "Initialize VTK scene manager",
+    test: testInitialize,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/Testing/JavaScript/testStates.mjs b/Web/WebAssembly/Testing/JavaScript/testStates.mjs
new file mode 100644
index 0000000000000000000000000000000000000000..b9219d88408bf29ec71165dd20b83bf090a960d6
--- /dev/null
+++ b/Web/WebAssembly/Testing/JavaScript/testStates.mjs
@@ -0,0 +1,60 @@
+import { readFile } from "fs/promises";
+import path from "path";
+
+const object_ids = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29, 31, 32]
+const exepected_dependencies = [1, 2, 3, 41, 5, 42, 44, 4, 6, 33, 35, 38, 40, 43, 11, 45, 46, 47, 48, 49, 50, 51, 7, 34, 36, 37, 39, 12, 8, 9, 10, 13, 14, 15, 16, 19, 21, 24, 27, 30, 17, 18, 20, 22, 23, 25, 26, 28, 29]
+
+async function testStates() {
+  const dataDirectoryIndex = process.argv.indexOf("-D") + 1;
+  if (dataDirectoryIndex <= 0) {
+    throw new Error("Please provide path to a blobs file using -D");
+  }
+  const dataDirectory = process.argv[dataDirectoryIndex];
+  const blobs = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.blobs.json")));
+  const states = JSON.parse(await readFile(path.join(dataDirectory, "Data", "WasmSceneManager", "scalar-bar-widget.states.json")));
+  const manager = await globalThis.createVTKWasmSceneManager({});
+  if (!manager.initialize()) {
+    throw new Error("Failed to initialize scene manager");
+  }
+  for (let i = 0; i < object_ids.length; ++i) {
+    const object_id = object_ids[i];
+    if (!manager.registerState(JSON.stringify(states[object_id]))) {
+      throw new Error(`Failed to register state at object_id=${object_id}`);
+    }
+  }
+  for (let hash in blobs) {
+    if (!manager.registerBlob(hash, new Uint8Array(blobs[hash].bytes))) {
+      throw new Error(`Failed to register blob with hash=${hash}`);
+    }
+  }
+  manager.updateObjectsFromStates();
+  const activeIds = manager.getAllDependencies(0);
+  if (!(activeIds instanceof Uint32Array)) {
+    throw new Error("getAllDependencies did not return a Uint32Array");
+  }
+  if (activeIds.toString() != exepected_dependencies.toString()) {
+    throw new Error(`${activeIds} != ${exepected_dependencies}`);
+  }
+}
+
+const tests = [
+  {
+    description: "Register states",
+    test: testStates,
+  },
+];
+
+let exitCode = 0;
+for (let test of tests) {
+  try {
+    await test.test();
+    console.log("✓", test.description);
+    exitCode |= 0;
+  }
+  catch (error) {
+    console.log("x", test.description);
+    console.log(error);
+    exitCode |= 1;
+  }
+}
+process.exit(exitCode);
diff --git a/Web/WebAssembly/post.js b/Web/WebAssembly/post.js
new file mode 100644
index 0000000000000000000000000000000000000000..c13443372cb05e1d4c482fde45968ba43346a852
--- /dev/null
+++ b/Web/WebAssembly/post.js
@@ -0,0 +1 @@
+globalThis.createVTKWasmSceneManager = vtkWasmSceneManager;
diff --git a/Web/WebAssembly/vtk.module b/Web/WebAssembly/vtk.module
new file mode 100644
index 0000000000000000000000000000000000000000..ff7a6e6dfcf9700bba7f0b7e06f6586014c7b229
--- /dev/null
+++ b/Web/WebAssembly/vtk.module
@@ -0,0 +1,12 @@
+NAME
+  VTK::WebAssembly
+LIBRARY_NAME
+  vtkWebAssembly
+SPDX_LICENSE_IDENTIFIER
+  BSD-3-Clause
+SPDX_COPYRIGHT_TEXT
+  Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+DEPENDS
+  VTK::SerializationManager
+PRIVATE_DEPENDS
+  VTK::RenderingCore
diff --git a/Web/WebAssembly/vtkWasmSceneManager.cxx b/Web/WebAssembly/vtkWasmSceneManager.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..b7d0c5ccc416cb90698c4b282b841c64bc8da884
--- /dev/null
+++ b/Web/WebAssembly/vtkWasmSceneManager.cxx
@@ -0,0 +1,130 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include "vtkWasmSceneManager.h"
+
+#include "vtkCallbackCommand.h"
+#include "vtkCommand.h"
+#include "vtkObjectFactory.h"
+
+#include "vtkRenderWindow.h"
+#include "vtkRenderWindowInteractor.h"
+
+VTK_ABI_NAMESPACE_BEGIN
+//-------------------------------------------------------------------------------
+vtkStandardNewMacro(vtkWasmSceneManager);
+
+//-------------------------------------------------------------------------------
+vtkWasmSceneManager::vtkWasmSceneManager() = default;
+
+//-------------------------------------------------------------------------------
+vtkWasmSceneManager::~vtkWasmSceneManager() = default;
+
+//-------------------------------------------------------------------------------
+void vtkWasmSceneManager::PrintSelf(ostream& os, vtkIndent indent)
+{
+  this->vtkObjectManager::PrintSelf(os, indent);
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::SetSize(vtkTypeUInt32 identifier, int width, int height)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    if (auto iren = renderWindow->GetInteractor())
+    {
+      iren->UpdateSize(width, height);
+      return true;
+    }
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::Render(vtkTypeUInt32 identifier)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    renderWindow->Render();
+    return true;
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::StartEventLoop(vtkTypeUInt32 identifier)
+{
+  vtkRenderWindowInteractor::InteractorManagesTheEventLoop = false;
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    auto interactor = renderWindow->GetInteractor();
+    std::cout << "Started event loop id=" << identifier
+              << ", interactor=" << interactor->GetObjectDescription() << '\n';
+    interactor->Start();
+    return true;
+  }
+  return false;
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::StopEventLoop(vtkTypeUInt32 identifier)
+{
+  auto object = this->GetObjectAtId(identifier);
+  if (auto renderWindow = vtkRenderWindow::SafeDownCast(object))
+  {
+    auto interactor = renderWindow->GetInteractor();
+    std::cout << "Stopping event loop id=" << identifier
+              << ", interactor=" << interactor->GetObjectDescription() << '\n';
+    interactor->TerminateApp();
+    return true;
+  }
+  return false;
+}
+
+namespace
+{
+struct CallbackBridge
+{
+  vtkWasmSceneManager::ObserverCallbackF f;
+  vtkTypeUInt32 SenderId;
+};
+}
+
+//-------------------------------------------------------------------------------
+unsigned long vtkWasmSceneManager::AddObserver(
+  vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback)
+{
+  auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier));
+  if (object == nullptr)
+  {
+    return 0;
+  }
+  vtkNew<vtkCallbackCommand> callbackCmd;
+  callbackCmd->SetClientData(new CallbackBridge{ callback, identifier });
+  callbackCmd->SetClientDataDeleteCallback([](void* clientData) {
+    auto* bridge = reinterpret_cast<CallbackBridge*>(clientData);
+    delete bridge;
+  });
+  callbackCmd->SetCallback([](vtkObject*, unsigned long eid, void* clientData, void*) {
+    auto* bridge = reinterpret_cast<CallbackBridge*>(clientData);
+    bridge->f(bridge->SenderId, vtkCommand::GetStringFromEventId(eid));
+  });
+  return object->AddObserver(eventName.c_str(), callbackCmd);
+}
+
+//-------------------------------------------------------------------------------
+bool vtkWasmSceneManager::RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag)
+{
+
+  auto object = vtkObject::SafeDownCast(this->GetObjectAtId(identifier));
+  if (object == nullptr)
+  {
+    return false;
+  }
+  object->RemoveObserver(tag);
+  return true;
+}
+
+VTK_ABI_NAMESPACE_END
diff --git a/Web/WebAssembly/vtkWasmSceneManager.h b/Web/WebAssembly/vtkWasmSceneManager.h
new file mode 100644
index 0000000000000000000000000000000000000000..493fa82a25b4f3922cb0f7810fd468bf59e7f076
--- /dev/null
+++ b/Web/WebAssembly/vtkWasmSceneManager.h
@@ -0,0 +1,94 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+/**
+ * @class   vtkWasmSceneManager
+ * @brief   vtkWasmSceneManager provides additional functionality that relates to a vtkRenderWindow
+ *          and user interaction.
+ *
+ * `vtkWasmSceneManager` is a javascript wrapper of `vtkSceneManager` for managing VTK
+ * objects, specifically designed for webassembly (wasm). It extends
+ * functionality of `vtkObjectManager` for managing objects such as `vtkRenderWindow`,
+ * `vtkRenderWindowInteractor` and enables event-observers in webassembly
+ * visualization applications.
+ *
+ * @sa vtkObjectManager
+ */
+#ifndef vtkWasmSceneManager_h
+#define vtkWasmSceneManager_h
+
+#include "vtkObjectManager.h"
+
+#include "vtkSerializationManagerModule.h" // for export macro
+
+VTK_ABI_NAMESPACE_BEGIN
+
+class VTKSERIALIZATIONMANAGER_EXPORT vtkWasmSceneManager : public vtkObjectManager
+{
+public:
+  static vtkWasmSceneManager* New();
+  vtkTypeMacro(vtkWasmSceneManager, vtkObjectManager);
+  void PrintSelf(ostream& os, vtkIndent indent) override;
+
+  /**
+   * Set the size of the `vtkRenderWindow` object at `identifier` to
+   * the supplied dimesions.
+   *
+   * Returns `true` if the obejct at `identifier` is a `vtkRenderWindow`
+   * with a `vtkRenderWindowInteractor` attached to it,
+   * `false` otherwise.
+   */
+  bool SetSize(vtkTypeUInt32 identifier, int width, int height);
+
+  /**
+   * Render the `vtkRenderWindow` object at `identifier`.
+   *
+   * Returns `true` if the obejct at `identifier` is a `vtkRenderWindow`
+   * `false` otherwise.
+   */
+  bool Render(vtkTypeUInt32 identifier);
+
+  /**
+   * Start event loop of the `vtkRenderWindowInteractor` object at `identifier`.
+   *
+   * Returns `true` if the obejct at `identifier` is a `vtkRenderWindowInteractor`
+   * `false` otherwise.
+   */
+  bool StartEventLoop(vtkTypeUInt32 identifier);
+
+  /**
+   * Stop event loop of the `vtkRenderWindowInteractor` object at `identifier`.
+   *
+   * Returns `true` if the obejct at `identifier` is a `vtkRenderWindowInteractor`
+   * `false` otherwise.
+   */
+  bool StopEventLoop(vtkTypeUInt32 identifier);
+
+  typedef void (*ObserverCallbackF)(vtkTypeUInt32, const char*);
+
+  /**
+   * Observes `eventName` event emitted by an object registered at `identifier`
+   * and invokes `callback` with the `identifier` and `eventName` for every such emission.
+   *
+   * Returns the tag of an observer for `eventName`. You can use the tag in `RemoveObserver`
+   * to stop observing `eventName` event from the object at `identifier`
+   */
+  unsigned long AddObserver(
+    vtkTypeUInt32 identifier, std::string eventName, ObserverCallbackF callback);
+
+  /**
+   * Stop observing the object at `identifier`.
+   * Returns `true` if an object exists at `identifier`,
+   * `false` otherwise.
+   */
+  bool RemoveObserver(vtkTypeUInt32 identifier, unsigned long tag);
+
+protected:
+  vtkWasmSceneManager();
+  ~vtkWasmSceneManager() override;
+
+private:
+  vtkWasmSceneManager(const vtkWasmSceneManager&) = delete;
+  void operator=(const vtkWasmSceneManager&) = delete;
+};
+VTK_ABI_NAMESPACE_END
+#endif
diff --git a/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx
new file mode 100644
index 0000000000000000000000000000000000000000..5c19ef16237a866aa7ec01a892b0f79fe15819a0
--- /dev/null
+++ b/Web/WebAssembly/vtkWasmSceneManagerEmBinding.cxx
@@ -0,0 +1,250 @@
+// SPDX-FileCopyrightText: Copyright (c) Ken Martin, Will Schroeder, Bill Lorensen
+// SPDX-License-Identifier: BSD-3-Clause
+#include <emscripten.h>
+#include <emscripten/bind.h>
+
+#include "vtkDataArrayRange.h"
+#include "vtkOpenGLPolyDataMapper.h"
+#include "vtkTypeUInt8Array.h"
+#include "vtkWasmSceneManager.h"
+
+namespace
+{
+
+#define CHECK_INIT                                                                                 \
+  do                                                                                               \
+  {                                                                                                \
+    if (Manager == nullptr)                                                                        \
+    {                                                                                              \
+      std::cerr << "Manager is null. Did you call forget to call initialize()?\n";                 \
+    }                                                                                              \
+  } while (0)
+
+vtkWasmSceneManager* Manager = nullptr;
+
+using namespace emscripten;
+
+thread_local const val Uint8Array = val::global("Uint8Array");
+thread_local const val Uint32Array = val::global("Uint32Array");
+thread_local const val JSON = val::global("JSON");
+
+//-------------------------------------------------------------------------------
+bool initialize()
+{
+  Manager = vtkWasmSceneManager::New();
+  bool result = Manager->Initialize();
+  // Remove the default vtkOpenGLPolyDataMapper as it is not used with wasm build.
+  Manager->GetSerializer()->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper));
+  Manager->GetDeserializer()->UnRegisterHandler(typeid(vtkOpenGLPolyDataMapper));
+  Manager->GetDeserializer()->UnRegisterConstructor("vtkOpenGLPolyDataMapper");
+  return result;
+}
+
+//-------------------------------------------------------------------------------
+void finalize()
+{
+  CHECK_INIT;
+  Manager->UnRegister(nullptr);
+}
+
+//-------------------------------------------------------------------------------
+bool registerState(const std::string& state)
+{
+  CHECK_INIT;
+  return Manager->RegisterState(state);
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterState(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterState(identifier);
+}
+
+//-------------------------------------------------------------------------------
+val getState(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return JSON.call<val>("parse", Manager->GetState(identifier));
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterObject(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterObject(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool registerBlob(const std::string& hash, val jsArray)
+{
+  CHECK_INIT;
+  if (jsArray.instanceof (val::global("Uint8Array")))
+  {
+    const vtkIdType l = jsArray["length"].as<vtkIdType>();
+    auto blob = vtk::TakeSmartPointer(vtkTypeUInt8Array::New());
+    blob->SetNumberOfValues(l);
+    auto blobRange = vtk::DataArrayValueRange(blob);
+    val memoryView{ typed_memory_view(static_cast<std::size_t>(l), blobRange.data()) };
+    memoryView.call<void>("set", jsArray);
+    return Manager->RegisterBlob(hash, blob);
+  }
+  else
+  {
+    std::cerr << "Invalid type! Expects instanceof blob == Uint8Array" << std::endl;
+    return false;
+  }
+}
+
+//-------------------------------------------------------------------------------
+bool unRegisterBlob(const std::string& hash)
+{
+  CHECK_INIT;
+  return Manager->UnRegisterBlob(hash);
+}
+
+//-------------------------------------------------------------------------------
+val getBlob(const std::string& hash)
+{
+  CHECK_INIT;
+  const auto blob = Manager->GetBlob(hash);
+  val jsBlob = Uint8Array.new_(typed_memory_view(blob->GetNumberOfValues(), blob->GetPointer(0)));
+  return jsBlob;
+}
+
+//-------------------------------------------------------------------------------
+void pruneUnusedBlobs()
+{
+  CHECK_INIT;
+  Manager->PruneUnusedBlobs();
+}
+
+//-------------------------------------------------------------------------------
+void clear()
+{
+  CHECK_INIT;
+  Manager->Clear();
+}
+
+//-------------------------------------------------------------------------------
+val getAllDependencies(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  const auto ids = Manager->GetAllDependencies(identifier);
+  val jsIds = Uint32Array.new_(typed_memory_view(ids.size(), ids.data()));
+  return jsIds;
+}
+//-------------------------------------------------------------------------------
+std::size_t getTotalBlobMemoryUsage()
+{
+  CHECK_INIT;
+  return ::Manager->GetTotalBlobMemoryUsage();
+}
+
+//-------------------------------------------------------------------------------
+std::size_t getTotalVTKDataObjectMemoryUsage()
+{
+  CHECK_INIT;
+  return ::Manager->GetTotalVTKDataObjectMemoryUsage();
+}
+
+//-------------------------------------------------------------------------------
+void updateObjectsFromStates()
+{
+  CHECK_INIT;
+  Manager->UpdateObjectsFromStates();
+}
+
+//-------------------------------------------------------------------------------
+void updateStatesFromObjects()
+{
+  CHECK_INIT;
+  Manager->UpdateStatesFromObjects();
+}
+
+//-------------------------------------------------------------------------------
+bool setSize(vtkTypeUInt32 identifier, int width, int height)
+{
+  CHECK_INIT;
+  return Manager->SetSize(identifier, width, height);
+}
+
+//-------------------------------------------------------------------------------
+bool render(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->Render(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool startEventLoop(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->StartEventLoop(identifier);
+}
+
+//-------------------------------------------------------------------------------
+bool stopEventLoop(vtkTypeUInt32 identifier)
+{
+  CHECK_INIT;
+  return Manager->StopEventLoop(identifier);
+}
+
+//-------------------------------------------------------------------------------
+unsigned long addObserver(vtkTypeUInt32 identifier, std::string eventName, val jsFunc)
+{
+  CHECK_INIT;
+  int fp = val::module_property("addFunction")(jsFunc, std::string("vii")).as<int>();
+  auto callback = reinterpret_cast<vtkWasmSceneManager::ObserverCallbackF>(fp);
+  return Manager->AddObserver(identifier, eventName, callback);
+}
+
+//-------------------------------------------------------------------------------
+bool removeObserver(vtkTypeUInt32 identifier, unsigned long tag)
+{
+  CHECK_INIT;
+  return Manager->RemoveObserver(identifier, tag);
+}
+
+} // namespace
+
+EMSCRIPTEN_BINDINGS(vtkWasmSceneManager)
+{
+  function("initialize", ::initialize);
+  function("finalize", ::finalize);
+
+  function("registerState", ::registerState);
+  function("unRegisterState", ::unRegisterState);
+  function("getState", ::getState);
+
+  function("unRegisterObject", ::unRegisterObject);
+
+  function("registerBlob", ::registerBlob);
+  function("unRegisterBlob", ::unRegisterBlob);
+  function("getBlob", ::getBlob);
+  function("pruneUnusedBlobs", ::pruneUnusedBlobs);
+
+  function("clear", ::clear);
+
+  function("getAllDependencies", ::getAllDependencies);
+
+  function("getTotalBlobMemoryUsage", ::getTotalBlobMemoryUsage);
+  function("getTotalVTKDataObjectMemoryUsage", ::getTotalVTKDataObjectMemoryUsage);
+
+  function("updateObjectsFromStates", ::updateObjectsFromStates);
+  function("updateStatesFromObjects", ::updateStatesFromObjects);
+
+  function("setSize", ::setSize);
+  function("render", ::render);
+
+  function("startEventLoop", ::startEventLoop);
+  function("stopEventLoop", ::stopEventLoop);
+
+  function("addObserver", ::addObserver);
+  function("removeObserver", ::removeObserver);
+}
+
+int main()
+{
+  return 0;
+}