diff --git a/src/components/SpeckleViewer.vue b/src/components/SpeckleViewer.vue
index eefceae..0bdf538 100644
--- a/src/components/SpeckleViewer.vue
+++ b/src/components/SpeckleViewer.vue
@@ -6,38 +6,13 @@
diff --git a/src/composables/viewer.ts b/src/composables/viewer.ts
new file mode 100644
index 0000000..59252fe
--- /dev/null
+++ b/src/composables/viewer.ts
@@ -0,0 +1,51 @@
+import { ref } from 'vue'
+import {
+ CameraController,
+ DefaultViewerParams,
+ SelectionExtension,
+ SpeckleLoader,
+ UrlHelper,
+ Viewer,
+} from '@speckle/viewer'
+
+export default function useViewer() {
+ const viewer = ref(null)
+
+ /**
+ * Initialize the viewer
+ * @param element - HTMLElement to initialize the viewer on
+ */
+ async function init(element: HTMLElement) {
+ // Set the default viewer parameters
+ const params = DefaultViewerParams
+ params.showStats = false
+ params.verbose = true
+
+ // Create the viewer instance on the element
+ viewer.value = new Viewer(element, params)
+
+ // Add the stock camera controller and selection extensions
+ viewer.value.createExtension(CameraController)
+ viewer.value.createExtension(SelectionExtension)
+ }
+
+ /**
+ * Load a model from a Speckle URL
+ * @param url - The URL of the Speckle model
+ */
+ const loadModelFromUrl = async (url: string) => {
+ if (!viewer.value) return
+
+ const urls = await UrlHelper.getResourceUrls(url)
+ urls.forEach(async (url) => {
+ const loader = new SpeckleLoader(viewer.value.getWorldTree(), url, '')
+ await viewer.value.loadObject(loader, true)
+ })
+ }
+
+ return {
+ init,
+ viewer,
+ loadModelFromUrl,
+ }
+}