diff --git a/src/web/app/desktop/-tags/follow-button.tag b/src/web/app/desktop/-tags/follow-button.tag
deleted file mode 100644
index fa7d43e03915e436a796f3f315459a0bec7704ed..0000000000000000000000000000000000000000
--- a/src/web/app/desktop/-tags/follow-button.tag
+++ /dev/null
@@ -1,150 +0,0 @@
-<mk-follow-button>
-	<button :class="{ wait: wait, follow: !user.is_following, unfollow: user.is_following }" v-if="!init" @click="onclick" disabled={ wait } title={ user.is_following ? 'フォロー解除' : 'フォローする' }>
-		<template v-if="!wait && user.is_following">%fa:minus%</template>
-		<template v-if="!wait && !user.is_following">%fa:plus%</template>
-		<template v-if="wait">%fa:spinner .pulse .fw%</template>
-	</button>
-	<div class="init" v-if="init">%fa:spinner .pulse .fw%</div>
-	<style lang="stylus" scoped>
-		:scope
-			display block
-
-			> button
-			> .init
-				display block
-				cursor pointer
-				padding 0
-				margin 0
-				width 32px
-				height 32px
-				font-size 1em
-				outline none
-				border-radius 4px
-
-				*
-					pointer-events none
-
-				&:focus
-					&:after
-						content ""
-						pointer-events none
-						position absolute
-						top -5px
-						right -5px
-						bottom -5px
-						left -5px
-						border 2px solid rgba($theme-color, 0.3)
-						border-radius 8px
-
-				&.follow
-					color #888
-					background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
-					border solid 1px #e2e2e2
-
-					&:hover
-						background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
-						border-color #dcdcdc
-
-					&:active
-						background #ececec
-						border-color #dcdcdc
-
-				&.unfollow
-					color $theme-color-foreground
-					background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
-					border solid 1px lighten($theme-color, 15%)
-
-					&:not(:disabled)
-						font-weight bold
-
-					&:hover:not(:disabled)
-						background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
-						border-color $theme-color
-
-					&:active:not(:disabled)
-						background $theme-color
-						border-color $theme-color
-
-				&.wait
-					cursor wait !important
-					opacity 0.7
-
-	</style>
-	<script lang="typescript">
-		import isPromise from '../../common/scripts/is-promise';
-
-		this.mixin('i');
-		this.mixin('api');
-
-		this.mixin('stream');
-		this.connection = this.stream.getConnection();
-		this.connectionId = this.stream.use();
-
-		this.user = null;
-		this.userPromise = isPromise(this.opts.user)
-			? this.opts.user
-			: Promise.resolve(this.opts.user);
-		this.init = true;
-		this.wait = false;
-
-		this.on('mount', () => {
-			this.userPromise.then(user => {
-				this.update({
-					init: false,
-					user: user
-				});
-				this.connection.on('follow', this.onStreamFollow);
-				this.connection.on('unfollow', this.onStreamUnfollow);
-			});
-		});
-
-		this.on('unmount', () => {
-			this.connection.off('follow', this.onStreamFollow);
-			this.connection.off('unfollow', this.onStreamUnfollow);
-			this.stream.dispose(this.connectionId);
-		});
-
-		this.onStreamFollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onStreamUnfollow = user => {
-			if (user.id == this.user.id) {
-				this.update({
-					user: user
-				});
-			}
-		};
-
-		this.onclick = () => {
-			this.wait = true;
-			if (this.user.is_following) {
-				this.api('following/delete', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = false;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			} else {
-				this.api('following/create', {
-					user_id: this.user.id
-				}).then(() => {
-					this.user.is_following = true;
-				}).catch(err => {
-					console.error(err);
-				}).then(() => {
-					this.wait = false;
-					this.update();
-				});
-			}
-		};
-	</script>
-</mk-follow-button>
diff --git a/src/web/app/desktop/views/components/follow-button.vue b/src/web/app/desktop/views/components/follow-button.vue
new file mode 100644
index 0000000000000000000000000000000000000000..588bcd6415a0f675d4e50b159770f7ee3f941225
--- /dev/null
+++ b/src/web/app/desktop/views/components/follow-button.vue
@@ -0,0 +1,149 @@
+<template>
+<button class="mk-follow-button"
+	:class="{ wait, follow: !user.is_following, unfollow: user.is_following }"
+	v-if="!init"
+	@click="onClick"
+	:disabled="wait"
+	:title="user.is_following ? 'フォロー解除' : 'フォローする'"
+>
+	<template v-if="!wait && user.is_following">%fa:minus%</template>
+	<template v-if="!wait && !user.is_following">%fa:plus%</template>
+	<template v-if="wait">%fa:spinner .pulse .fw%</template>
+</button>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+export default Vue.extend({
+	props: {
+		user: {
+			type: Object,
+			required: true
+		}
+	},
+	data() {
+		return {
+			wait: false,
+			connection: null,
+			connectionId: null
+		};
+	},
+	mounted() {
+		this.connection = this.$root.$data.os.stream.getConnection();
+		this.connectionId = this.$root.$data.os.stream.use();
+
+		this.connection.on('follow', this.onFollow);
+		this.connection.on('unfollow', this.onUnfollow);
+	},
+	beforeDestroy() {
+		this.connection.off('follow', this.onFollow);
+		this.connection.off('unfollow', this.onUnfollow);
+		this.$root.$data.os.stream.dispose(this.connectionId);
+	},
+	methods: {
+
+		onFollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onUnfollow(user) {
+			if (user.id == this.user.id) {
+				this.user.is_following = user.is_following;
+			}
+		},
+
+		onClick() {
+			this.wait = true;
+			if (this.user.is_following) {
+				this.api('following/delete', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = false;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			} else {
+				this.api('following/create', {
+					user_id: this.user.id
+				}).then(() => {
+					this.user.is_following = true;
+				}).catch(err => {
+					console.error(err);
+				}).then(() => {
+					this.wait = false;
+				});
+			}
+		}
+	}
+});
+</script>
+
+<style lang="stylus" scoped>
+.mk-follow-button
+	display block
+
+	> button
+	> .init
+		display block
+		cursor pointer
+		padding 0
+		margin 0
+		width 32px
+		height 32px
+		font-size 1em
+		outline none
+		border-radius 4px
+
+		*
+			pointer-events none
+
+		&:focus
+			&:after
+				content ""
+				pointer-events none
+				position absolute
+				top -5px
+				right -5px
+				bottom -5px
+				left -5px
+				border 2px solid rgba($theme-color, 0.3)
+				border-radius 8px
+
+		&.follow
+			color #888
+			background linear-gradient(to bottom, #ffffff 0%, #f5f5f5 100%)
+			border solid 1px #e2e2e2
+
+			&:hover
+				background linear-gradient(to bottom, #f9f9f9 0%, #ececec 100%)
+				border-color #dcdcdc
+
+			&:active
+				background #ececec
+				border-color #dcdcdc
+
+		&.unfollow
+			color $theme-color-foreground
+			background linear-gradient(to bottom, lighten($theme-color, 25%) 0%, lighten($theme-color, 10%) 100%)
+			border solid 1px lighten($theme-color, 15%)
+
+			&:not(:disabled)
+				font-weight bold
+
+			&:hover:not(:disabled)
+				background linear-gradient(to bottom, lighten($theme-color, 8%) 0%, darken($theme-color, 8%) 100%)
+				border-color $theme-color
+
+			&:active:not(:disabled)
+				background $theme-color
+				border-color $theme-color
+
+		&.wait
+			cursor wait !important
+			opacity 0.7
+
+</style>