diff --git a/qim3d/utils/cli.py b/qim3d/utils/cli.py
index 2b6e3bd008f3896a88cbab9c1863bf7800ddb19d..db01df31c69c6779b644a69520b3c0608fcc2dce 100644
--- a/qim3d/utils/cli.py
+++ b/qim3d/utils/cli.py
@@ -1,11 +1,13 @@
 import argparse
+import qim3d
+import webbrowser
 from qim3d.gui import data_explorer, iso3d, annotation_tool, local_thickness
 
 def main():
     parser = argparse.ArgumentParser(description='Qim3d command-line interface.')
     subparsers = parser.add_subparsers(title='Subcommands', dest='subcommand')
 
-    # subcommands
+    # GUIs
     gui_parser = subparsers.add_parser('gui', help = 'Graphical User Interfaces.')
 
     gui_parser.add_argument('--data-explorer', action='store_true', help='Run data explorer.')
@@ -15,6 +17,11 @@ def main():
     gui_parser.add_argument('--host', default='0.0.0.0', help='Desired host.')
     gui_parser.add_argument('--platform', action='store_true', help='Use QIM platform address')
 
+    # K3D 
+    viz_parser = subparsers.add_parser('viz', help = 'Volumetric visualization.')
+    viz_parser.add_argument('--source', default=False, help='Path to the image file')
+    viz_parser.add_argument('--destination', default='k3d.html', help='Path to save html file.')
+    viz_parser.add_argument('--no-browser', action='store_true', help='Do not launch browser.')
 
     args = parser.parse_args()
 
@@ -47,5 +54,25 @@ def main():
             else:
                 interface = local_thickness.Interface()
                 interface.launch()
+
+
+    if args.subcommand == "viz":
+        if not args.source:
+            print ("Please specify a source file using the argument --source")
+            return
+        # Load the data
+        print (f"Loading data from {args.source}")
+        volume = qim3d.io.load(str(args.source))
+        print (f"Done, volume shape: {volume.shape}")
+
+        # Make k3d plot
+        print ("\nGenerating k3d plot...")
+        qim3d.viz.vol(volume, show=False, save=str(args.destination))
+        print (f"Done, plot available at <{args.destination}>")
+
+        if not args.no_browser:
+            print("Opening in default browser...")
+            webbrowser.open_new_tab(args.destination)
+        
 if __name__ == '__main__':
     main()
\ No newline at end of file
diff --git a/qim3d/viz/__init__.py b/qim3d/viz/__init__.py
index 98132cca4381071e1d1954f5f3f6315da78fa8a4..ef978e880a26f9be8a1616c8375bb2aa8114484d 100644
--- a/qim3d/viz/__init__.py
+++ b/qim3d/viz/__init__.py
@@ -1,2 +1,3 @@
 from .visualizations import plot_metrics
 from .img import grid_pred, grid_overview, slices
+from .k3d import vol
diff --git a/qim3d/viz/k3d.py b/qim3d/viz/k3d.py
new file mode 100644
index 0000000000000000000000000000000000000000..b87235027a19f6f4feb10d94bcc9885c64b95b4b
--- /dev/null
+++ b/qim3d/viz/k3d.py
@@ -0,0 +1,47 @@
+"""
+Volumetric visualization using K3D
+"""
+
+import k3d
+import numpy as np
+
+def vol(img, show=True, save=False):
+    """
+    Volumetric visualization of a given volume.
+
+    Args:
+        img (numpy.ndarray): The input 3D image data. It should be a 3D numpy array.
+        show (bool, optional): If True, displays the visualization. Defaults to True.
+        save (bool or str, optional): If True, saves the visualization as an HTML file. 
+            If a string is provided, it's interpreted as the file path where the HTML 
+            file will be saved. Defaults to False.
+
+    Returns:
+        k3d.plot: If show is False, returns the K3D plot object.
+
+    Examples:
+        ```python
+        import qim3d
+        vol = qim3d.examples.fly_150x256x256
+
+        # shows the volume inline
+        qim3d.viz.vol(vol) 
+
+        # saves html plot to disk
+        plot = qim3d.viz.vol(vol, show=False, save="plot.html")
+        ```
+
+    """
+    plt_volume = k3d.volume(img.astype(np.float32))
+    plot = k3d.plot()
+    plot += plt_volume
+
+    if save:
+        # Save html to disk
+        with open(str(save),'w', encoding="utf-8") as fp:
+            fp.write(plot.get_snapshot())
+
+    if show:
+        plot.display()
+    else:
+        return plot
\ No newline at end of file