diff --git a/src/client/app.vue b/src/client/app.vue
index c2e60c9bbe..615f6b9cd3 100644
--- a/src/client/app.vue
+++ b/src/client/app.vue
@@ -293,7 +293,7 @@ export default Vue.extend({
 		const ro = new ResizeObserver((entries, observer) => {
 			adjustTitlePosition();
 		});
-		
+
 		ro.observe(this.$refs.contents);
 
 		window.addEventListener('resize', adjustTitlePosition);
@@ -556,6 +556,7 @@ export default Vue.extend({
 				'calendar',
 				'rss',
 				'trends',
+				'clock'
 			];
 
 			this.$root.menu({
diff --git a/src/client/components/analog-clock.vue b/src/client/components/analog-clock.vue
new file mode 100644
index 0000000000..a107362240
--- /dev/null
+++ b/src/client/components/analog-clock.vue
@@ -0,0 +1,143 @@
+<template>
+<svg class="mk-analog-clock" viewBox="0 0 10 10" preserveAspectRatio="none">
+	<circle v-for="angle, i in graduations"
+					:cx="5 + (Math.sin(angle) * (5 - graduationsPadding))"
+					:cy="5 - (Math.cos(angle) * (5 - graduationsPadding))"
+					:r="i % 5 == 0 ? 0.125 : 0.05"
+					:fill="i % 5 == 0 ? majorGraduationColor : minorGraduationColor"/>
+
+	<line
+		:x1="5 - (Math.sin(sAngle) * (sHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(sAngle) * (sHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(sAngle) * ((sHandLengthRatio * 5) - handsPadding))"
+		:stroke="sHandColor"
+		stroke-width="0.05"/>
+
+	<line
+		:x1="5 - (Math.sin(mAngle) * (mHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(mAngle) * (mHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(mAngle) * ((mHandLengthRatio * 5) - handsPadding))"
+		:stroke="mHandColor"
+		stroke-width="0.1"/>
+
+	<line
+		:x1="5 - (Math.sin(hAngle) * (hHandLengthRatio * handsTailLength))"
+		:y1="5 + (Math.cos(hAngle) * (hHandLengthRatio * handsTailLength))"
+		:x2="5 + (Math.sin(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+		:y2="5 - (Math.cos(hAngle) * ((hHandLengthRatio * 5) - handsPadding))"
+		:stroke="hHandColor"
+		stroke-width="0.1"/>
+</svg>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+import * as tinycolor from 'tinycolor2';
+
+export default Vue.extend({
+	props: {
+		dark: {
+			type: Boolean,
+			default: false
+		},
+		smooth: {
+			type: Boolean,
+			default: false
+		}
+	},
+
+	data() {
+		return {
+			now: new Date(),
+			enabled: true,
+
+			graduationsPadding: 0.5,
+			handsPadding: 1,
+			handsTailLength: 0.7,
+			hHandLengthRatio: 0.75,
+			mHandLengthRatio: 1,
+			sHandLengthRatio: 1
+		};
+	},
+
+	computed: {
+		majorGraduationColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.3)' : 'rgba(0, 0, 0, 0.3)';
+		},
+		minorGraduationColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.2)' : 'rgba(0, 0, 0, 0.2)';
+		},
+
+		sHandColor(): string {
+			return this.dark ? 'rgba(255, 255, 255, 0.5)' : 'rgba(0, 0, 0, 0.3)';
+		},
+		mHandColor(): string {
+			return this.dark ? '#fff' : '#777';
+		},
+		hHandColor(): string {
+			return tinycolor(getComputedStyle(document.documentElement).getPropertyValue('--primary')).toHexString();
+		},
+
+		ms(): number {
+			return this.now.getMilliseconds() * this.smooth;
+		},
+		s(): number {
+			return this.now.getSeconds();
+		},
+		m(): number {
+			return this.now.getMinutes();
+		},
+		h(): number {
+			return this.now.getHours();
+		},
+
+		hAngle(): number {
+			return Math.PI * (this.h % 12 + (this.m + (this.s + this.ms / 1000) / 60) / 60) / 6;
+		},
+		mAngle(): number {
+			return Math.PI * (this.m + (this.s + this.ms / 1000) / 60) / 30;
+		},
+		sAngle(): number {
+			return Math.PI * (this.s + this.ms / 1000) / 30;
+		},
+
+		graduations(): any {
+			const angles = [];
+			for (let i = 0; i < 60; i++) {
+				const angle = Math.PI * i / 30;
+				angles.push(angle);
+			}
+
+			return angles;
+		}
+	},
+
+	mounted() {
+		const update = () => {
+			if (this.enabled) {
+				this.tick();
+				requestAnimationFrame(update);
+			}
+		};
+		update();
+	},
+
+	beforeDestroy() {
+		this.enabled = false;
+	},
+
+	methods: {
+		tick() {
+			this.now = new Date();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mk-analog-clock {
+	display: block;
+}
+</style>
diff --git a/src/client/widgets/clock.vue b/src/client/widgets/clock.vue
new file mode 100644
index 0000000000..bd521813b7
--- /dev/null
+++ b/src/client/widgets/clock.vue
@@ -0,0 +1,42 @@
+<template>
+<div class="mkw-clock">
+	<mk-container :naked="props.style % 2 === 0" :show-header="false">
+		<div class="mkw-analog-clock--body">
+			<mk-analog-clock :dark="$store.state.device.darkmode" :smooth="props.style < 2"/>
+		</div>
+	</mk-container>
+</div>
+</template>
+
+<script lang="ts">
+import define from './define';
+
+import MkContainer from '../components/ui/container.vue';
+import MkAnalogClock from '../components/analog-clock.vue';
+
+export default define({
+	name: 'clock',
+	props: () => ({
+		style: 0
+	})
+}).extend({
+	components: {
+		MkContainer,
+		MkAnalogClock
+	},
+	methods: {
+		func() {
+			this.props.style = (this.props.style + 1) % 4;
+			this.save();
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.mkw-analog-clock {
+	.mkw-analog-clock--body {
+		padding: 8px;
+	}
+}
+</style>
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
index 4743be0763..d6af41e2f8 100644
--- a/src/client/widgets/index.ts
+++ b/src/client/widgets/index.ts
@@ -6,3 +6,4 @@ Vue.component('mkw-timeline', () => import('./timeline.vue').then(m => m.default
 Vue.component('mkw-calendar', () => import('./calendar.vue').then(m => m.default));
 Vue.component('mkw-rss', () => import('./rss.vue').then(m => m.default));
 Vue.component('mkw-trends', () => import('./trends.vue').then(m => m.default));
+Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));