diff --git a/packages/common/src/button.js b/packages/common/src/button.js
new file mode 100644
index 0000000000000000000000000000000000000000..a370ddb381607b16534b9598c98ad90b0e860501
--- /dev/null
+++ b/packages/common/src/button.js
@@ -0,0 +1,82 @@
+import {html, LitElement, css} from 'lit-element';
+import {ScopedElementsMixin} from '@open-wc/scoped-elements';
+import {MiniSpinner} from './mini-spinner.js';
+import * as commonStyles from '../styles.js';
+
+/**
+ * vpu-button implements a button with Bulma styles and automatic spinner and
+ * disabling if button is clicked
+ *
+ * Use the attribute "no-spinner-on-click" to disable the spinner, then you can
+ * start it with start() and stop it with stop()
+ *
+ * Type can be is-primary/is-info/is-success/is-warning/is-danger
+ */
+export class Button extends ScopedElementsMixin(LitElement) {
+    constructor() {
+        super();
+        this.value = "";
+        this.type = "";
+        this.spinner = false;
+        this.noSpinnerOnClick = false;
+        this.disabled = false;
+    }
+
+    static get scopedElements() {
+        return {
+            'vpu-mini-spinner': MiniSpinner,
+        };
+    }
+
+    connectedCallback() {
+        super.connectedCallback();
+    }
+
+    static get properties() {
+        return {
+            value: { type: String },
+            type: { type: String },
+            spinner: { type: Boolean },
+            noSpinnerOnClick: { type: Boolean, attribute: 'no-spinner-on-click' },
+            disabled: { type: Boolean, reflect: true },
+        };
+    }
+
+    clickHandler() {
+        if (!this.noSpinnerOnClick) {
+            this.start();
+        }
+    }
+
+    start() {
+        this.spinner = true;
+        this.disabled = true;
+    }
+
+    stop() {
+        this.spinner = false;
+        this.disabled = false;
+    }
+
+    isDisabled() {
+        return this.disabled;
+    }
+
+    static get styles() {
+        // language=css
+        return css`
+            ${commonStyles.getThemeCSS()}
+            ${commonStyles.getButtonCSS()}
+
+            .spinner { margin-left: 0.5em; }
+        `;
+    }
+
+    render() {
+        return html`
+            <button @click="${this.clickHandler}" class="button ${this.type}" ?disabled="${this.disabled}">
+                ${this.value} <vpu-mini-spinner class="spinner" style="display: ${this.spinner ? "inline" : "none"}"></vpu-mini-spinner>
+            </button>
+        `;
+    }
+}
diff --git a/packages/common/src/mini-spinner.js b/packages/common/src/mini-spinner.js
new file mode 100644
index 0000000000000000000000000000000000000000..67e769997b2984343db1c1ac06ad4efbe37f7c25
--- /dev/null
+++ b/packages/common/src/mini-spinner.js
@@ -0,0 +1,70 @@
+import {html, LitElement, css} from 'lit-element';
+
+export class MiniSpinner extends LitElement {
+    constructor() {
+        super();
+        this.text = "";
+    }
+
+    static get properties() {
+        return {
+            text: { type: String },
+        };
+    }
+
+    static get styles() {
+        // language=css
+        return css`
+        .outer {
+            display: inline-block;
+        }
+        .inner {
+            display: flex;
+        }
+        .ring {
+          display: inline-block;
+          position: relative;
+          width: 1em;
+          height: 1em;
+        }
+        .ring div {
+          box-sizing: border-box;
+          display: block;
+          position: absolute;
+          width: 100%;
+          height: 100%;
+          border: 0.2em solid currentColor;
+          border-radius: 50%;
+          animation: ring 1.2s cubic-bezier(0.5, 0, 0.5, 1) infinite;
+          border-color: currentColor transparent transparent transparent;
+        }
+        .ring div:nth-child(1) {
+          animation-delay: -0.45s;
+        }
+        .ring div:nth-child(2) {
+          animation-delay: -0.3s;
+        }
+        .ring div:nth-child(3) {
+          animation-delay: -0.15s;
+        }
+        @keyframes ring {
+          0% {
+            transform: rotate(0deg);
+          }
+          100% {
+            transform: rotate(360deg);
+          }
+        }
+        .text {
+            display: inline-block;
+            margin-left: 0.5em;
+            font-size: 0.7em;
+        }`;
+    } 
+
+    render() {
+        const textHtml = this.text !== "" ? html`<div class="text">${this.text}</div>` : html``;
+
+        return html`<div class="outer"><div class="inner"><div class="ring"><div></div><div></div><div></div><div></div></div>${textHtml}</div></div>`;
+    }
+}
diff --git a/packages/common/src/spinner.js b/packages/common/src/spinner.js
new file mode 100644
index 0000000000000000000000000000000000000000..88e20f5fd23f071e48192a5faee813e2e88bb78f
--- /dev/null
+++ b/packages/common/src/spinner.js
@@ -0,0 +1,118 @@
+// Note: This doesn't have a lit-element dependency on purpose, so it can be loaded faster before
+// other web components (assuming it's not bundled)
+
+export class Spinner extends HTMLElement {
+
+    constructor() {
+        super();
+        let shadowRoot = this.attachShadow({mode: 'open'});
+        shadowRoot.innerHTML = `
+<style>
+:host {
+    display: block;
+}
+#all-spinner-tuglogo {
+  width:130px;
+    height:130px;
+      position:relative;
+        background-color:transparent;
+          margin:0 auto;
+}
+
+.all-spinner-tuglogo-box {
+  width:20%;
+    height:20%;
+     background-color:#e4154b;
+        position:absolute;
+          top:50%;
+            left:50%;
+          animation-duration: 1.6s;
+            animation-direction:alternate;
+              animation-iteration-count:infinite;
+    animation-fill-mode:both;
+      animation-timing-function:ease;
+        transition: transform 0.5s, background-color 0.2s 0.5s;
+}
+
+#all-spinner-tuglogo-box-1 {
+      animation-name: box1;
+        transform:translateX(-160%) translateY(-50%);
+}
+
+#all-spinner-tuglogo-box-2 {
+      transform-origin:0 0;
+      animation-name: box2;
+transform:scale(1) translateX(-50%) translateY(-50%);
+}
+
+#all-spinner-tuglogo-box-3 {
+
+      animation-name: box3;
+        animation-delay:0.3s;
+          transform:translateX(60%) translateY(-50%); visibility:visible;
+}
+
+#all-spinner-tuglogo-box-4 {
+
+      animation-name: box4;
+        animation-delay:0.1s;
+transform:translateX(0%) translateY(-100%);  visibility:visible;
+}
+
+#all-spinner-tuglogo-box-5 {
+
+      animation-name: box5;
+        animation-delay:0.2s;
+transform:translateX(-100%) translateY(0%);  visibility:visible;
+}
+
+@keyframes box1 {
+    0% { transform:translateX(-50%) translateY(-50%); visibility:hidden; }
+    50% { transform:translateX(-50%) translateY(-50%); visibility:hidden; }
+    70% { transform:translateX(-170%) translateY(-50%); visibility:visible;}
+    80% { transform:translateX(-160%) translateY(-50%); visibility:visible;}
+  100%  { transform:translateX(-160%) translateY(-50%); visibility:visible;}
+}
+
+@keyframes box2 {
+    0% { transform:scale(0) translateX(-50%) translateY(-50%);}
+    5% { transform:scale(0) translateX(-50%) translateY(-50%);}
+    30% { transform:scale(1.2) translateX(-50%) translateY(-50%);}
+    35% { transform:scale(1) translateX(-50%) translateY(-50%);}
+  100% { transform:scale(1) translateX(-50%) translateY(-50%);}
+
+}
+
+@keyframes box3 {
+    0% { transform:translateX(-50%) translateY(-50%); visibility:hidden; }
+    50% { transform:translateX(-50%) translateY(-50%); visibility:hidden; }
+    70% { transform:translateX(70%) translateY(-50%); visibility:visible;}
+    80% { transform:translateX(60%) translateY(-50%); visibility:visible;}
+  100%  { transform:translateX(60%) translateY(-50%); visibility:visible;}
+}
+
+@keyframes box4 {
+    0%  { transform:translateX(-50%) translateY(-50%);  visibility:hidden; }
+    50% { transform:translateX(-50%) translateY(-50%);  visibility:hidden; }
+    70% { transform:translateX(10%) translateY(-110%);  visibility:visible;}
+    80% { transform:translateX(0%) translateY(-100%);  visibility:visible;}
+  100%  { transform:translateX(0%) translateY(-100%);  visibility:visible;}
+}
+@keyframes box5 {
+    0%  { transform:translateX(-50%) translateY(-50%);  visibility:hidden; }
+    50% { transform:translateX(-50%) translateY(-50%);  visibility:hidden; }
+    70% { transform:translateX(-110%) translateY(10%);  visibility:visible;}
+    80% { transform:translateX(-100%) translateY(0%);  visibility:visible;}
+  100%  { transform:translateX(-100%) translateY(0%);  visibility:visible;}
+}
+</style>
+
+<div id="all-spinner-tuglogo">
+  <div id="all-spinner-tuglogo-box-1" class="all-spinner-tuglogo-box"></div>
+  <div id="all-spinner-tuglogo-box-2" class="all-spinner-tuglogo-box"></div>
+  <div id="all-spinner-tuglogo-box-3" class="all-spinner-tuglogo-box"></div>
+  <div id="all-spinner-tuglogo-box-4" class="all-spinner-tuglogo-box"></div>
+  <div id="all-spinner-tuglogo-box-5" class="all-spinner-tuglogo-box"></div>
+</div>`;
+    }
+}
\ No newline at end of file