diff --git a/.circleci/config.yml b/.circleci/config.yml
index 33047b16956..1f31df70a85 100644
--- a/.circleci/config.yml
+++ b/.circleci/config.yml
@@ -122,6 +122,26 @@ jobs:
           name: Run jasmine tests (part B)
           command: .circleci/test.sh webgl-jasmine
 
+  virtual-webgl-jasmine:
+    docker:
+      # need '-browsers' version to test in real (xvfb-wrapped) browsers
+      - image: cimg/node:16.17.1-browsers
+    environment:
+      # Alaska time (arbitrary timezone to test date logic)
+      TZ: "America/Anchorage"
+    parallelism: 8
+    working_directory: ~/plotly.js
+    steps:
+      - browser-tools/install-browser-tools: &browser-versions
+          chrome-version: 110.0.5481.100
+          install-firefox: false
+          install-geckodriver: false
+      - attach_workspace:
+          at: ~/
+      - run:
+          name: Run jasmine tests (part B)
+          command: .circleci/test.sh virtual-webgl-jasmine
+
   flaky-no-gl-jasmine:
     docker:
       # need '-browsers' version to test in real (xvfb-wrapped) browsers
@@ -216,6 +236,25 @@ jobs:
           name: Test MathJax on firefox-latest
           command: .circleci/test.sh mathjax-firefox82+
 
+  make-baselines-virtual-webgl:
+    parallelism: 2
+    docker:
+      - image: circleci/python:3.8.9
+    working_directory: ~/plotly.js
+    steps:
+      - attach_workspace:
+          at: ~/
+      - run:
+          name: Install kaleido, plotly.io and required fonts
+          command: .circleci/env_image.sh
+      - run:
+          name: Create png files using virtual-webgl & WebGL v1
+          command: .circleci/test.sh make-baselines-virtual-webgl
+      - persist_to_workspace:
+          root: ~/
+          paths:
+            - plotly.js
+
   make-baselines-mathjax3:
     docker:
       - image: circleci/python:3.8.9
@@ -267,6 +306,20 @@ jobs:
           path: build
           destination: /
 
+  test-baselines-virtual-webgl:
+    docker:
+      - image: circleci/node:16.9.0
+    working_directory: ~/plotly.js
+    steps:
+      - attach_workspace:
+          at: ~/
+      - run:
+          name: Compare pixels
+          command: .circleci/test.sh test-image-virtual-webgl ; find build -maxdepth 1 -type f -delete
+      - store_artifacts:
+          path: build
+          destination: /
+
   test-baselines-mathjax3:
     docker:
       - image: circleci/node:16.9.0
@@ -441,9 +494,18 @@ workflows:
       - webgl-jasmine:
           requires:
             - install-and-cibuild
+      - virtual-webgl-jasmine:
+          requires:
+            - install-and-cibuild
       - flaky-no-gl-jasmine:
           requires:
             - install-and-cibuild
+      - make-baselines-virtual-webgl:
+          requires:
+            - install-and-cibuild
+      - test-baselines-virtual-webgl:
+          requires:
+            - make-baselines-virtual-webgl
       - make-baselines-mathjax3:
           requires:
             - install-and-cibuild
diff --git a/.circleci/test.sh b/.circleci/test.sh
index ecb9b0649c7..32532182823 100755
--- a/.circleci/test.sh
+++ b/.circleci/test.sh
@@ -53,6 +53,16 @@ case $1 in
         exit $EXIT_STATE
         ;;
 
+    virtual-webgl-jasmine)
+        SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=5 --tag=gl | circleci tests split))
+        for s in ${SHARDS[@]}; do
+            MAX_AUTO_RETRY=2
+            retry ./node_modules/karma/bin/karma start test/jasmine/karma.conf.js --virtualWebgl --tags=gl --skip-tags=noCI,noVirtualWebgl --doNotFailOnEmptyTestSuite -- "$s"
+        done
+
+        exit $EXIT_STATE
+        ;;
+
     flaky-no-gl-jasmine)
         SHARDS=($(node $ROOT/tasks/shard_jasmine_tests.js --limit=1 --tag=flaky | circleci tests split))
 
@@ -82,6 +92,15 @@ case $1 in
         exit $EXIT_STATE
         ;;
 
+    make-baselines-virtual-webgl)
+        SUITE=$({\
+                  find $ROOT/test/image/mocks/gl*     -type f -printf "%f\n"; \
+                  find $ROOT/test/image/mocks/mapbox* -type f -printf "%f\n"; \
+                } | sed 's/\.json$//1' | circleci tests split)
+        python3 test/image/make_baseline.py virtual-webgl $SUITE || EXIT_STATE=$?
+        exit $EXIT_STATE
+        ;;
+
     make-baselines-mathjax3)
         python3 test/image/make_baseline.py mathjax3    legend_mathjax_title_and_items mathjax parcats_grid_subplots table_latex_multitrace_scatter table_plain_birds table_wrapped_birds ternary-mathjax || EXIT_STATE=$?
         exit $EXIT_STATE
@@ -103,6 +122,11 @@ case $1 in
         exit $EXIT_STATE
         ;;
 
+    test-image-virtual-webgl)
+        node test/image/compare_pixels_test.js virtual-webgl || { tar -cvf build/baselines.tar build/test_images/*.png ; exit 1 ; } || EXIT_STATE=$?
+        exit $EXIT_STATE
+        ;;
+
     source-syntax)
         npm run lint        || EXIT_STATE=$?
         npm run test-syntax || EXIT_STATE=$?
diff --git a/package-lock.json b/package-lock.json
index 0795b56ee95..e6dd94e1092 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -12,6 +12,7 @@
         "@plotly/d3": "3.8.1",
         "@plotly/d3-sankey": "0.7.2",
         "@plotly/d3-sankey-circular": "0.33.1",
+        "@plotly/mapbox-gl": "v1.13.4",
         "@turf/area": "^6.4.0",
         "@turf/bbox": "^6.4.0",
         "@turf/centroid": "^6.0.2",
@@ -36,7 +37,6 @@
         "has-hover": "^1.0.1",
         "has-passive-events": "^1.0.0",
         "is-mobile": "^4.0.0",
-        "mapbox-gl": "1.13.1",
         "mouse-change": "^1.4.0",
         "mouse-event-offset": "^3.0.2",
         "mouse-wheel": "^1.2.0",
@@ -114,6 +114,7 @@
         "through2": "^4.0.2",
         "transform-loader": "^0.2.4",
         "true-case-path": "^2.2.1",
+        "virtual-webgl": "^1.0.6",
         "webpack": "^5.88.2",
         "webpack-cli": "^5.1.4"
       }
@@ -2134,52 +2135,17 @@
       }
     },
     "node_modules/@mapbox/geojson-rewind": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz",
-      "integrity": "sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg==",
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
+      "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
       "dependencies": {
-        "concat-stream": "~2.0.0",
-        "minimist": "^1.2.5"
+        "get-stream": "^6.0.1",
+        "minimist": "^1.2.6"
       },
       "bin": {
         "geojson-rewind": "geojson-rewind"
       }
     },
-    "node_modules/@mapbox/geojson-rewind/node_modules/concat-stream": {
-      "version": "2.0.0",
-      "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
-      "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
-      "engines": [
-        "node >= 6.0"
-      ],
-      "dependencies": {
-        "buffer-from": "^1.0.0",
-        "inherits": "^2.0.3",
-        "readable-stream": "^3.0.2",
-        "typedarray": "^0.0.6"
-      }
-    },
-    "node_modules/@mapbox/geojson-rewind/node_modules/readable-stream": {
-      "version": "3.6.0",
-      "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-      "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
-      "dependencies": {
-        "inherits": "^2.0.3",
-        "string_decoder": "^1.1.1",
-        "util-deprecate": "^1.0.1"
-      },
-      "engines": {
-        "node": ">= 6"
-      }
-    },
-    "node_modules/@mapbox/geojson-rewind/node_modules/string_decoder": {
-      "version": "1.3.0",
-      "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-      "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-      "dependencies": {
-        "safe-buffer": "~5.2.0"
-      }
-    },
     "node_modules/@mapbox/geojson-types": {
       "version": "1.0.2",
       "resolved": "https://registry.npmjs.org/@mapbox/geojson-types/-/geojson-types-1.0.2.tgz",
@@ -2337,6 +2303,38 @@
         "elementary-circuits-directed-graph": "^1.0.4"
       }
     },
+    "node_modules/@plotly/mapbox-gl": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz",
+      "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==",
+      "dependencies": {
+        "@mapbox/geojson-rewind": "^0.5.2",
+        "@mapbox/geojson-types": "^1.0.2",
+        "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+        "@mapbox/mapbox-gl-supported": "^1.5.0",
+        "@mapbox/point-geometry": "^0.1.0",
+        "@mapbox/tiny-sdf": "^1.1.1",
+        "@mapbox/unitbezier": "^0.0.0",
+        "@mapbox/vector-tile": "^1.3.1",
+        "@mapbox/whoots-js": "^3.1.0",
+        "csscolorparser": "~1.0.3",
+        "earcut": "^2.2.2",
+        "geojson-vt": "^3.2.1",
+        "gl-matrix": "^3.2.1",
+        "grid-index": "^1.1.0",
+        "murmurhash-js": "^1.0.0",
+        "pbf": "^3.2.1",
+        "potpack": "^1.0.1",
+        "quickselect": "^2.0.0",
+        "rw": "^1.3.3",
+        "supercluster": "^7.1.0",
+        "tinyqueue": "^2.0.3",
+        "vt-pbf": "^3.1.1"
+      },
+      "engines": {
+        "node": ">=6.4.0"
+      }
+    },
     "node_modules/@plotly/point-cluster": {
       "version": "3.1.9",
       "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz",
@@ -6522,6 +6520,17 @@
       "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
       "dev": true
     },
+    "node_modules/get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==",
+      "engines": {
+        "node": ">=10"
+      },
+      "funding": {
+        "url": "https://github.com/sponsors/sindresorhus"
+      }
+    },
     "node_modules/gl-mat4": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz",
@@ -8640,6 +8649,7 @@
       "version": "1.13.1",
       "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.1.tgz",
       "integrity": "sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g==",
+      "peer": true,
       "dependencies": {
         "@mapbox/geojson-rewind": "^0.5.0",
         "@mapbox/geojson-types": "^1.0.2",
@@ -12444,6 +12454,12 @@
         "node": ">= 0.8"
       }
     },
+    "node_modules/virtual-webgl": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/virtual-webgl/-/virtual-webgl-1.0.6.tgz",
+      "integrity": "sha512-dKJgNJiiQkdWaGecvtGwL81M0gZ/3ebch4OlOrDF0L52Qo6uS/VQOW6T3ua7voJhe1E1KNIWNw4pF4rc4ZbMsQ==",
+      "dev": true
+    },
     "node_modules/vm-browserify": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
@@ -14368,43 +14384,12 @@
       }
     },
     "@mapbox/geojson-rewind": {
-      "version": "0.5.0",
-      "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.0.tgz",
-      "integrity": "sha512-73l/qJQgj/T/zO1JXVfuVvvKDgikD/7D/rHAD28S9BG1OTstgmftrmqfCx4U+zQAmtsB6HcDA3a7ymdnJZAQgg==",
+      "version": "0.5.2",
+      "resolved": "https://registry.npmjs.org/@mapbox/geojson-rewind/-/geojson-rewind-0.5.2.tgz",
+      "integrity": "sha512-tJaT+RbYGJYStt7wI3cq4Nl4SXxG8W7JDG5DMJu97V25RnbNg3QtQtf+KD+VLjNpWKYsRvXDNmNrBgEETr1ifA==",
       "requires": {
-        "concat-stream": "~2.0.0",
-        "minimist": "^1.2.5"
-      },
-      "dependencies": {
-        "concat-stream": {
-          "version": "2.0.0",
-          "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-2.0.0.tgz",
-          "integrity": "sha512-MWufYdFw53ccGjCA+Ol7XJYpAlW6/prSMzuPOTRnJGcGzuhLn4Scrz7qf6o8bROZ514ltazcIFJZevcfbo0x7A==",
-          "requires": {
-            "buffer-from": "^1.0.0",
-            "inherits": "^2.0.3",
-            "readable-stream": "^3.0.2",
-            "typedarray": "^0.0.6"
-          }
-        },
-        "readable-stream": {
-          "version": "3.6.0",
-          "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.0.tgz",
-          "integrity": "sha512-BViHy7LKeTz4oNnkcLJ+lVSL6vpiFeX6/d3oSH8zCW7UxP2onchk+vTGB143xuFjHS3deTgkKoXXymXqymiIdA==",
-          "requires": {
-            "inherits": "^2.0.3",
-            "string_decoder": "^1.1.1",
-            "util-deprecate": "^1.0.1"
-          }
-        },
-        "string_decoder": {
-          "version": "1.3.0",
-          "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz",
-          "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==",
-          "requires": {
-            "safe-buffer": "~5.2.0"
-          }
-        }
+        "get-stream": "^6.0.1",
+        "minimist": "^1.2.6"
       }
     },
     "@mapbox/geojson-types": {
@@ -14537,6 +14522,35 @@
         "elementary-circuits-directed-graph": "^1.0.4"
       }
     },
+    "@plotly/mapbox-gl": {
+      "version": "1.13.4",
+      "resolved": "https://registry.npmjs.org/@plotly/mapbox-gl/-/mapbox-gl-1.13.4.tgz",
+      "integrity": "sha512-sR3/Pe5LqT/fhYgp4rT4aSFf1rTsxMbGiH6Hojc7PH36ny5Bn17iVFUjpzycafETURuFbLZUfjODO8LvSI+5zQ==",
+      "requires": {
+        "@mapbox/geojson-rewind": "^0.5.2",
+        "@mapbox/geojson-types": "^1.0.2",
+        "@mapbox/jsonlint-lines-primitives": "^2.0.2",
+        "@mapbox/mapbox-gl-supported": "^1.5.0",
+        "@mapbox/point-geometry": "^0.1.0",
+        "@mapbox/tiny-sdf": "^1.1.1",
+        "@mapbox/unitbezier": "^0.0.0",
+        "@mapbox/vector-tile": "^1.3.1",
+        "@mapbox/whoots-js": "^3.1.0",
+        "csscolorparser": "~1.0.3",
+        "earcut": "^2.2.2",
+        "geojson-vt": "^3.2.1",
+        "gl-matrix": "^3.2.1",
+        "grid-index": "^1.1.0",
+        "murmurhash-js": "^1.0.0",
+        "pbf": "^3.2.1",
+        "potpack": "^1.0.1",
+        "quickselect": "^2.0.0",
+        "rw": "^1.3.3",
+        "supercluster": "^7.1.0",
+        "tinyqueue": "^2.0.3",
+        "vt-pbf": "^3.1.1"
+      }
+    },
     "@plotly/point-cluster": {
       "version": "3.1.9",
       "resolved": "https://registry.npmjs.org/@plotly/point-cluster/-/point-cluster-3.1.9.tgz",
@@ -17998,6 +18012,11 @@
       "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==",
       "dev": true
     },
+    "get-stream": {
+      "version": "6.0.1",
+      "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz",
+      "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg=="
+    },
     "gl-mat4": {
       "version": "1.2.0",
       "resolved": "https://registry.npmjs.org/gl-mat4/-/gl-mat4-1.2.0.tgz",
@@ -19634,6 +19653,7 @@
       "version": "1.13.1",
       "resolved": "https://registry.npmjs.org/mapbox-gl/-/mapbox-gl-1.13.1.tgz",
       "integrity": "sha512-GSyubcoSF5MyaP8z+DasLu5v7KmDK2pp4S5+VQ5WdVQUOaAqQY4jwl4JpcdNho3uWm2bIKs7x1l7q3ynGmW60g==",
+      "peer": true,
       "requires": {
         "@mapbox/geojson-rewind": "^0.5.0",
         "@mapbox/geojson-types": "^1.0.2",
@@ -22597,6 +22617,12 @@
       "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==",
       "dev": true
     },
+    "virtual-webgl": {
+      "version": "1.0.6",
+      "resolved": "https://registry.npmjs.org/virtual-webgl/-/virtual-webgl-1.0.6.tgz",
+      "integrity": "sha512-dKJgNJiiQkdWaGecvtGwL81M0gZ/3ebch4OlOrDF0L52Qo6uS/VQOW6T3ua7voJhe1E1KNIWNw4pF4rc4ZbMsQ==",
+      "dev": true
+    },
     "vm-browserify": {
       "version": "1.1.2",
       "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz",
diff --git a/package.json b/package.json
index 02c19a6d1c5..161abfbf83a 100644
--- a/package.json
+++ b/package.json
@@ -73,6 +73,7 @@
     "@plotly/d3": "3.8.1",
     "@plotly/d3-sankey": "0.7.2",
     "@plotly/d3-sankey-circular": "0.33.1",
+    "@plotly/mapbox-gl": "v1.13.4",
     "@turf/area": "^6.4.0",
     "@turf/bbox": "^6.4.0",
     "@turf/centroid": "^6.0.2",
@@ -97,7 +98,6 @@
     "has-hover": "^1.0.1",
     "has-passive-events": "^1.0.0",
     "is-mobile": "^4.0.0",
-    "mapbox-gl": "1.13.1",
     "mouse-change": "^1.4.0",
     "mouse-event-offset": "^3.0.2",
     "mouse-wheel": "^1.2.0",
@@ -175,6 +175,7 @@
     "through2": "^4.0.2",
     "transform-loader": "^0.2.4",
     "true-case-path": "^2.2.1",
+    "virtual-webgl": "^1.0.6",
     "webpack": "^5.88.2",
     "webpack-cli": "^5.1.4"
   }
diff --git a/src/plots/mapbox/constants.js b/src/plots/mapbox/constants.js
index 53804475757..86e955cb4cd 100644
--- a/src/plots/mapbox/constants.js
+++ b/src/plots/mapbox/constants.js
@@ -2,7 +2,7 @@
 
 var sortObjectKeys = require('../../lib/sort_object_keys');
 
-var requiredVersion = '1.13.1';
+var requiredVersion = '1.13.4';
 
 var OSM = '© <a target="_blank" href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors';
 var carto = [
@@ -183,7 +183,7 @@ module.exports = {
 
     wrongVersionErrorMsg: [
         'Your custom plotly.js bundle is not using the correct mapbox-gl version',
-        'Please install mapbox-gl@' + requiredVersion + '.'
+        'Please install @plotly/mapbox-gl@' + requiredVersion + '.'
     ].join('\n'),
 
     noAccessTokenErrorMsg: [
diff --git a/src/plots/mapbox/index.js b/src/plots/mapbox/index.js
index 15430dfaac9..1dfb85b75fa 100644
--- a/src/plots/mapbox/index.js
+++ b/src/plots/mapbox/index.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var mapboxgl = require('mapbox-gl/dist/mapbox-gl-unminified');
+var mapboxgl = require('@plotly/mapbox-gl/dist/mapbox-gl-unminified');
 
 var Lib = require('../../lib');
 var strTranslate = Lib.strTranslate;
diff --git a/src/plots/mapbox/mapbox.js b/src/plots/mapbox/mapbox.js
index f1e3bb7106f..fb85de019f2 100644
--- a/src/plots/mapbox/mapbox.js
+++ b/src/plots/mapbox/mapbox.js
@@ -1,6 +1,6 @@
 'use strict';
 
-var mapboxgl = require('mapbox-gl/dist/mapbox-gl-unminified');
+var mapboxgl = require('@plotly/mapbox-gl/dist/mapbox-gl-unminified');
 
 var Lib = require('../../lib');
 var geoUtils = require('../../lib/geo_location_utils');
diff --git a/src/traces/scattergl/plot.js b/src/traces/scattergl/plot.js
index e06679b8f44..214d62ab656 100644
--- a/src/traces/scattergl/plot.js
+++ b/src/traces/scattergl/plot.js
@@ -60,6 +60,14 @@ var exports = module.exports = function plot(gd, subplot, cdata) {
     linkTraces(gd, subplot, cdata);
 
     if(scene.dirty) {
+        if(
+            (scene.line2d || scene.error2d) &&
+            !(scene.scatter2d || scene.fill2d || scene.glText)
+        ) {
+            // Fixes shared WebGL context drawing lines only case
+            regl.clear({});
+        }
+
         // make sure scenes are created
         if(scene.error2d === true) {
             scene.error2d = createError(regl);
diff --git a/test/image/compare_pixels_test.js b/test/image/compare_pixels_test.js
index 9640a73b004..5d63cf676a0 100644
--- a/test/image/compare_pixels_test.js
+++ b/test/image/compare_pixels_test.js
@@ -42,9 +42,13 @@ if(argv._.length === 0) {
 // Build list of mocks to compare
 var allMockList = [];
 var mathjax3;
+var virtualWebgl = false;
 argv._.forEach(function(pattern) {
     if(pattern === 'mathjax3') {
         mathjax3 = true;
+    } else if(pattern === 'virtual-webgl') {
+        virtualWebgl = true;
+        allMockList = getMockList('');
     } else {
         var mockList = getMockList(pattern);
 
@@ -66,6 +70,12 @@ allMockList = allMockList.filter(function(a) {
     );
 });
 
+if(virtualWebgl) {
+    allMockList = allMockList.filter(function(a) {
+        return a.slice(0, 2) === 'gl' || a.slice(0, 6) === 'mapbox';
+    });
+}
+
 if(mathjax3) {
     allMockList = [
         'legend_mathjax_title_and_items',
@@ -91,6 +101,7 @@ var fail = function(mockName) {
         failed.push(mockName);
     }
 };
+
 for(var i = 0; i < allMockList.length; i++) {
     var mockName = allMockList[i];
 
@@ -151,15 +162,28 @@ for(var i = 0; i < allMockList.length; i++) {
 
     var shouldBePixelPerfect = !(isMapbox || isOtherFlaky);
 
+    var threshold = shouldBePixelPerfect ? 0 : [
+        // more flaky
+        'mapbox_angles',
+        'mapbox_layers',
+        'mapbox_custom-style',
+        'mapbox_geojson-attributes'
+    ].indexOf(mockName) !== -1 ? 1 : 0.15;
+
+    if(virtualWebgl) {
+        threshold = Math.max(0.4, threshold);
+        if([
+            'gl3d_ibm-plot',
+            'gl3d_isosurface_2surfaces-checker_spaceframe',
+            'gl3d_opacity-scaling-spikes',
+            'gl3d_cone-wind',
+            'gl3d_isosurface_math',
+            'gl3d_scatter3d-blank-text'
+        ].indexOf(mockName) !== -1) threshold = 0.7;
+    }
+
     var numDiffPixels = pixelmatch(img0.data, img1.data, diff.data, width, height, {
-        threshold: shouldBePixelPerfect ? 0 :
-            [
-                // more flaky
-                'mapbox_angles',
-                'mapbox_layers',
-                'mapbox_custom-style',
-                'mapbox_geojson-attributes'
-            ].indexOf(mockName) !== -1 ? 1 : 0.15
+        threshold: threshold
     });
 
     if(numDiffPixels) {
diff --git a/test/image/make_baseline.py b/test/image/make_baseline.py
index 3d12090832b..13cdc2b120b 100644
--- a/test/image/make_baseline.py
+++ b/test/image/make_baseline.py
@@ -10,6 +10,11 @@
     args = sys.argv
 
 root = os.getcwd()
+
+virtual_webgl = os.path.join(root, 'node_modules', 'virtual-webgl', 'src', 'virtual-webgl.js')
+plotlyjs = os.path.join(root, 'build', 'plotly.js')
+plotlyjs_with_virtual_webgl = os.path.join(root, 'build', 'plotly_with_virtual-webgl.js')
+
 dirIn = os.path.join(root, 'test', 'image', 'mocks')
 dirOut = os.path.join(root, 'build', 'test_images')
 
@@ -33,8 +38,21 @@
     mathjax_version = 3
     print('Kaleido using MathJax v3')
 
+virtual_webgl_version = 0 # i.e. virtual-webgl is not used
+if 'virtual-webgl' in sys.argv or 'virtual-webgl=' in sys.argv :
+    virtual_webgl_version = 1
+    print('using virtual-webgl for WebGL v1')
+
+    with open(plotlyjs_with_virtual_webgl, 'w') as fileOut:
+        for filename in [virtual_webgl, plotlyjs]:
+            with open(filename, 'r') as fileIn:
+                for line in fileIn:
+                    fileOut.write(line)
+
+    plotlyjs = plotlyjs_with_virtual_webgl
+
+pio.kaleido.scope.plotlyjs = plotlyjs
 pio.templates.default = 'none'
-pio.kaleido.scope.plotlyjs = os.path.join(root, 'build', 'plotly.js')
 
 _credentials = open(os.path.join(root, 'build', 'credentials.json'), 'r')
 pio.kaleido.scope.mapbox_access_token = json.load(_credentials)['MAPBOX_ACCESS_TOKEN']
diff --git a/test/jasmine/karma.conf.js b/test/jasmine/karma.conf.js
index 0b4af2fec7e..7ed9a6d0f43 100644
--- a/test/jasmine/karma.conf.js
+++ b/test/jasmine/karma.conf.js
@@ -127,6 +127,7 @@ var pathToUnpolyfill = path.join(__dirname, 'assets', 'unpolyfill.js');
 var pathToSaneTopojsonDist = path.join(__dirname, '..', '..', 'node_modules', 'sane-topojson', 'dist');
 var pathToMathJax2 = path.join(__dirname, '..', '..', 'node_modules', 'mathjax-v2');
 var pathToMathJax3 = path.join(__dirname, '..', '..', 'node_modules', 'mathjax-v3');
+var pathToVirtualWebgl = path.join(__dirname, '..', '..', 'node_modules', 'virtual-webgl', 'src', 'virtual-webgl.js');
 
 var reporters = [];
 if(argv['report-progress'] || argv['report-spec'] || argv['report-dots']) {
@@ -351,6 +352,13 @@ if(isBundleTest) {
     func.defaultConfig.files.push(pathToJQuery);
 }
 
+if(argv.virtualWebgl) {
+    // add virtual-webgl to the top
+    func.defaultConfig.files = [
+        pathToVirtualWebgl
+    ].concat(func.defaultConfig.files);
+}
+
 // lastly, load test file glob
 func.defaultConfig.files.push(testFileGlob);
 
diff --git a/test/jasmine/tests/gl3d_plot_interact_test.js b/test/jasmine/tests/gl3d_plot_interact_test.js
index c8570f2dde8..67e7805cc39 100644
--- a/test/jasmine/tests/gl3d_plot_interact_test.js
+++ b/test/jasmine/tests/gl3d_plot_interact_test.js
@@ -2156,7 +2156,7 @@ describe('Test removal of gl contexts', function() {
         .then(done, done.fail);
     });
 
-    it('@gl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
+    it('@gl @noVirtualWebgl should fire *plotly_webglcontextlost* when on webgl context lost', function(done) {
         var _mock = Lib.extendDeep({}, require('../../image/mocks/gl3d_marker-arrays.json'));
 
         Plotly.newPlot(gd, _mock).then(function() {
diff --git a/test/jasmine/tests/splom_test.js b/test/jasmine/tests/splom_test.js
index 18b3d4116a1..b85c14882bd 100644
--- a/test/jasmine/tests/splom_test.js
+++ b/test/jasmine/tests/splom_test.js
@@ -1727,7 +1727,7 @@ describe('Test splom select:', function() {
         .then(done, done.fail);
     });
 
-    it('@gl should behave correctly during select->dblclick->pan scenarios', function(done) {
+    it('@noCI @gl should behave correctly during select->dblclick->pan scenarios', function(done) {
         var fig = Lib.extendDeep({}, require('../../image/mocks/splom_0.json'));
         fig.layout = {
             width: 400,