diff --git a/packages/client/src/components/global/a.vue b/packages/client/src/components/global/a.vue
index 77ee7525a4..d3bc2235b8 100644
--- a/packages/client/src/components/global/a.vue
+++ b/packages/client/src/components/global/a.vue
@@ -4,130 +4,113 @@
 </a>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { inject } from 'vue';
 import * as os from '@/os';
 import copyToClipboard from '@/scripts/copy-to-clipboard';
 import { router } from '@/router';
 import { url } from '@/config';
-import { popout } from '@/scripts/popout';
-import { ColdDeviceStorage } from '@/store';
+import { popout as popout_ } from '@/scripts/popout';
+import { i18n } from '@/i18n';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	inject: {
-		navHook: {
-			default: null
-		},
-		sideViewHook: {
-			default: null
+const props = withDefaults(defineProps<{
+	to: string;
+	activeClass?: null | string;
+	behavior?: null | 'window' | 'browser' | 'modalWindow';
+}>(), {
+	activeClass: null,
+	behavior: null,
+});
+
+const navHook = inject('navHook', null);
+const sideViewHook = inject('sideViewHook', null);
+
+const active = $computed(() => {
+	if (props.activeClass == null) return false;
+	const resolved = router.resolve(props.to);
+	if (resolved.path === router.currentRoute.value.path) return true;
+	if (resolved.name == null) return false;
+	if (router.currentRoute.value.name == null) return false;
+	return resolved.name === router.currentRoute.value.name;
+});
+
+function onContextmenu(ev) {
+	if (window.getSelection().toString() !== '') return;
+	os.contextMenu([{
+		type: 'label',
+		text: props.to,
+	}, {
+		icon: 'fas fa-window-maximize',
+		text: i18n.locale.openInWindow,
+		action: () => {
+			os.pageWindow(props.to);
 		}
-	},
-
-	props: {
-		to: {
-			type: String,
-			required: true,
-		},
-		activeClass: {
-			type: String,
-			required: false,
-		},
-		behavior: {
-			type: String,
-			required: false,
-		},
-	},
-
-	computed: {
-		active() {
-			if (this.activeClass == null) return false;
-			const resolved = router.resolve(this.to);
-			if (resolved.path == this.$route.path) return true;
-			if (resolved.name == null) return false;
-			if (this.$route.name == null) return false;
-			return resolved.name == this.$route.name;
+	}, sideViewHook ? {
+		icon: 'fas fa-columns',
+		text: i18n.locale.openInSideView,
+		action: () => {
+			sideViewHook(props.to);
 		}
-	},
+	} : undefined, {
+		icon: 'fas fa-expand-alt',
+		text: i18n.locale.showInPage,
+		action: () => {
+			router.push(props.to);
+		}
+	}, null, {
+		icon: 'fas fa-external-link-alt',
+		text: i18n.locale.openInNewTab,
+		action: () => {
+			window.open(props.to, '_blank');
+		}
+	}, {
+		icon: 'fas fa-link',
+		text: i18n.locale.copyLink,
+		action: () => {
+			copyToClipboard(`${url}${props.to}`);
+		}
+	}], ev);
+}
 
-	methods: {
-		onContextmenu(e) {
-			if (window.getSelection().toString() !== '') return;
-			os.contextMenu([{
-				type: 'label',
-				text: this.to,
-			}, {
-				icon: 'fas fa-window-maximize',
-				text: this.$ts.openInWindow,
-				action: () => {
-					os.pageWindow(this.to);
-				}
-			}, this.sideViewHook ? {
-				icon: 'fas fa-columns',
-				text: this.$ts.openInSideView,
-				action: () => {
-					this.sideViewHook(this.to);
-				}
-			} : undefined, {
-				icon: 'fas fa-expand-alt',
-				text: this.$ts.showInPage,
-				action: () => {
-					this.$router.push(this.to);
-				}
-			}, null, {
-				icon: 'fas fa-external-link-alt',
-				text: this.$ts.openInNewTab,
-				action: () => {
-					window.open(this.to, '_blank');
-				}
-			}, {
-				icon: 'fas fa-link',
-				text: this.$ts.copyLink,
-				action: () => {
-					copyToClipboard(`${url}${this.to}`);
-				}
-			}], e);
-		},
+function window() {
+	os.pageWindow(props.to);
+}
 
-		window() {
-			os.pageWindow(this.to);
-		},
+function modalWindow() {
+	os.modalPageWindow(props.to);
+}
 
-		modalWindow() {
-			os.modalPageWindow(this.to);
-		},
+function popout() {
+	popout_(props.to);
+}
 
-		popout() {
-			popout(this.to);
-		},
+function nav() {
+	if (props.behavior === 'browser') {
+		location.href = props.to;
+		return;
+	}
 
-		nav() {
-			if (this.behavior === 'browser') {
-				location.href = this.to;
-				return;
-			}
-
-			if (this.behavior) {
-				if (this.behavior === 'window') {
-					return this.window();
-				} else if (this.behavior === 'modalWindow') {
-					return this.modalWindow();
-				}
-			}
-
-			if (this.navHook) {
-				this.navHook(this.to);
-			} else {
-				if (this.$store.state.defaultSideView && this.sideViewHook && this.to !== '/') {
-					return this.sideViewHook(this.to);
-				}
-
-				if (this.$router.currentRoute.value.path === this.to) {
-					window.scroll({ top: 0, behavior: 'smooth' });
-				} else {
-					this.$router.push(this.to);
-				}
-			}
+	if (props.behavior) {
+		if (props.behavior === 'window') {
+			return window();
+		} else if (props.behavior === 'modalWindow') {
+			return modalWindow();
 		}
 	}
-});
+
+	if (navHook) {
+		navHook(props.to);
+	} else {
+		if (defaultStore.state.defaultSideView && sideViewHook && props.to !== '/') {
+			return sideViewHook(props.to);
+		}
+
+		if (router.currentRoute.value.path === props.to) {
+			window.scroll({ top: 0, behavior: 'smooth' });
+		} else {
+			router.push(props.to);
+		}
+	}
+}
 </script>
diff --git a/packages/client/src/components/global/avatar.vue b/packages/client/src/components/global/avatar.vue
index 300e5e079f..9e8979fe56 100644
--- a/packages/client/src/components/global/avatar.vue
+++ b/packages/client/src/components/global/avatar.vue
@@ -1,74 +1,54 @@
 <template>
-<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :title="acct(user)" @click="onClick">
+<span v-if="disableLink" v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :title="acct(user)" @click="onClick">
 	<img class="inner" :src="url" decoding="async"/>
 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 </span>
-<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat, square: $store.state.squareAvatars }" :to="userPage(user)" :title="acct(user)" :target="target">
+<MkA v-else v-user-preview="disablePreview ? undefined : user.id" class="eiwwqkts _noSelect" :class="{ cat: user.isCat, square: $store.state.squareAvatars }" :style="{ color }" :to="userPage(user)" :title="acct(user)" :target="target">
 	<img class="inner" :src="url" decoding="async"/>
 	<MkUserOnlineIndicator v-if="showIndicator" class="indicator" :user="user"/>
 </MkA>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, watch } from 'vue';
+import * as misskey from 'misskey-js';
 import { getStaticImageUrl } from '@/scripts/get-static-image-url';
 import { extractAvgColorFromBlurhash } from '@/scripts/extract-avg-color-from-blurhash';
 import { acct, userPage } from '@/filters/user';
 import MkUserOnlineIndicator from '@/components/user-online-indicator.vue';
+import { defaultStore } from '@/store';
 
-export default defineComponent({
-	components: {
-		MkUserOnlineIndicator
-	},
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		target: {
-			required: false,
-			default: null
-		},
-		disableLink: {
-			required: false,
-			default: false
-		},
-		disablePreview: {
-			required: false,
-			default: false
-		},
-		showIndicator: {
-			required: false,
-			default: false
-		}
-	},
-	emits: ['click'],
-	computed: {
-		cat(): boolean {
-			return this.user.isCat;
-		},
-		url(): string {
-			return this.$store.state.disableShowingAnimatedImages
-				? getStaticImageUrl(this.user.avatarUrl)
-				: this.user.avatarUrl;
-		},
-	},
-	watch: {
-		'user.avatarBlurhash'() {
-			if (this.$el == null) return;
-			this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-		}
-	},
-	mounted() {
-		this.$el.style.color = extractAvgColorFromBlurhash(this.user.avatarBlurhash);
-	},
-	methods: {
-		onClick(e) {
-			this.$emit('click', e);
-		},
-		acct,
-		userPage
-	}
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+	target?: string | null;
+	disableLink?: boolean;
+	disablePreview?: boolean;
+	showIndicator?: boolean;
+}>(), {
+	target: null,
+	disableLink: false,
+	disablePreview: false,
+	showIndicator: false,
+});
+
+const emit = defineEmits<{
+	(e: 'click', ev: MouseEvent): void;
+}>();
+
+const url = defaultStore.state.disableShowingAnimatedImages
+	? getStaticImageUrl(props.user.avatarUrl)
+	: props.user.avatarUrl;
+
+function onClick(ev: MouseEvent) {
+	emit('click', ev);
+}
+
+let color = $ref();
+
+watch(() => props.user.avatarBlurhash, () => {
+	color = extractAvgColorFromBlurhash(props.user.avatarBlurhash);
+}, {
+	immediate: true,
 });
 </script>
 
diff --git a/packages/client/src/components/global/loading.vue b/packages/client/src/components/global/loading.vue
index 7bde53c12e..43ea1395ed 100644
--- a/packages/client/src/components/global/loading.vue
+++ b/packages/client/src/components/global/loading.vue
@@ -4,27 +4,17 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 
-export default defineComponent({
-	props: {
-		inline: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-		colored: {
-			type: Boolean,
-			required: false,
-			default: true
-		},
-		mini: {
-			type: Boolean,
-			required: false,
-			default: false
-		},
-	}
+const props = withDefaults(defineProps<{
+	inline?: boolean;
+	colored?: boolean;
+	mini?: boolean;
+}>(), {
+	inline: false,
+	colored: true,
+	mini: false,
 });
 </script>
 
diff --git a/packages/client/src/components/global/misskey-flavored-markdown.vue b/packages/client/src/components/global/misskey-flavored-markdown.vue
index ab20404909..243d8614ba 100644
--- a/packages/client/src/components/global/misskey-flavored-markdown.vue
+++ b/packages/client/src/components/global/misskey-flavored-markdown.vue
@@ -1,15 +1,23 @@
 <template>
-<mfm-core v-bind="$attrs" class="havbbuyv" :class="{ nowrap: $attrs['nowrap'] }"/>
+<MfmCore :text="text" :plain="plain" :nowrap="nowrap" :author="author" :customEmojis="customEmojis" :isNote="isNote" class="havbbuyv" :class="{ nowrap }"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
 import MfmCore from '@/components/mfm';
 
-export default defineComponent({
-	components: {
-		MfmCore
-	}
+const props = withDefaults(defineProps<{
+	text: string;
+	plain?: boolean;
+	nowrap?: boolean;
+	author?: any;
+	customEmojis?: any;
+	isNote?: boolean;
+}>(), {
+	plain: false,
+	nowrap: false,
+	author: null,
+	isNote: true,
 });
 </script>
 
diff --git a/packages/client/src/components/global/time.vue b/packages/client/src/components/global/time.vue
index 6a330a2307..d2788264c5 100644
--- a/packages/client/src/components/global/time.vue
+++ b/packages/client/src/components/global/time.vue
@@ -1,73 +1,57 @@
 <template>
 <time :title="absolute">
-	<template v-if="mode == 'relative'">{{ relative }}</template>
-	<template v-else-if="mode == 'absolute'">{{ absolute }}</template>
-	<template v-else-if="mode == 'detail'">{{ absolute }} ({{ relative }})</template>
+	<template v-if="mode === 'relative'">{{ relative }}</template>
+	<template v-else-if="mode === 'absolute'">{{ absolute }}</template>
+	<template v-else-if="mode === 'detail'">{{ absolute }} ({{ relative }})</template>
 </time>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onUnmounted } from 'vue';
+import { i18n } from '@/i18n';
 
-export default defineComponent({
-	props: {
-		time: {
-			type: [Date, String],
-			required: true
-		},
-		mode: {
-			type: String,
-			default: 'relative'
-		}
-	},
-	data() {
-		return {
-			tickId: null,
-			now: new Date()
-		};
-	},
-	computed: {
-		_time(): Date {
-			return typeof this.time == 'string' ? new Date(this.time) : this.time;
-		},
-		absolute(): string {
-			return this._time.toLocaleString();
-		},
-		relative(): string {
-			const time = this._time;
-			const ago = (this.now.getTime() - time.getTime()) / 1000/*ms*/;
-			return (
-				ago >= 31536000 ? this.$t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
-				ago >= 2592000  ? this.$t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
-				ago >= 604800   ? this.$t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
-				ago >= 86400    ? this.$t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
-				ago >= 3600     ? this.$t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
-				ago >= 60       ? this.$t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
-				ago >= 10       ? this.$t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
-				ago >= -1       ? this.$ts._ago.justNow :
-				ago <  -1       ? this.$ts._ago.future :
-				this.$ts._ago.unknown);
-		}
-	},
-	created() {
-		if (this.mode == 'relative' || this.mode == 'detail') {
-			this.tickId = window.requestAnimationFrame(this.tick);
-		}
-	},
-	unmounted() {
-		if (this.mode === 'relative' || this.mode === 'detail') {
-			window.clearTimeout(this.tickId);
-		}
-	},
-	methods: {
-		tick() {
-			// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
-			this.now = new Date();
-
-			this.tickId = setTimeout(() => {
-				window.requestAnimationFrame(this.tick);
-			}, 10000);
-		}
-	}
+const props = withDefaults(defineProps<{
+	time: Date | string;
+	mode?: 'relative' | 'absolute' | 'detail';
+}>(), {
+	mode: 'relative',
 });
+
+const _time = typeof props.time == 'string' ? new Date(props.time) : props.time;
+const absolute = _time.toLocaleString();
+
+let now = $ref(new Date());
+const relative = $computed(() => {
+	const ago = (now.getTime() - _time.getTime()) / 1000/*ms*/;
+	return (
+		ago >= 31536000 ? i18n.t('_ago.yearsAgo',   { n: (~~(ago / 31536000)).toString() }) :
+		ago >= 2592000  ? i18n.t('_ago.monthsAgo',  { n: (~~(ago / 2592000)).toString() }) :
+		ago >= 604800   ? i18n.t('_ago.weeksAgo',   { n: (~~(ago / 604800)).toString() }) :
+		ago >= 86400    ? i18n.t('_ago.daysAgo',    { n: (~~(ago / 86400)).toString() }) :
+		ago >= 3600     ? i18n.t('_ago.hoursAgo',   { n: (~~(ago / 3600)).toString() }) :
+		ago >= 60       ? i18n.t('_ago.minutesAgo', { n: (~~(ago / 60)).toString() }) :
+		ago >= 10       ? i18n.t('_ago.secondsAgo', { n: (~~(ago % 60)).toString() }) :
+		ago >= -1       ? i18n.locale._ago.justNow :
+		ago <  -1       ? i18n.locale._ago.future :
+		i18n.locale._ago.unknown);
+});
+
+function tick() {
+	// TODO: パフォーマンス向上のため、このコンポーネントが画面内に表示されている場合のみ更新する
+	now = new Date();
+
+	tickId = window.setTimeout(() => {
+		window.requestAnimationFrame(tick);
+	}, 10000);
+}
+
+let tickId: number;
+
+if (props.mode === 'relative' || props.mode === 'detail') {
+	tickId = window.requestAnimationFrame(tick);
+
+	onUnmounted(() => {
+		window.clearTimeout(tickId);
+	});
+}
 </script>
diff --git a/packages/client/src/components/global/user-name.vue b/packages/client/src/components/global/user-name.vue
index bc93a8ea30..090de3df30 100644
--- a/packages/client/src/components/global/user-name.vue
+++ b/packages/client/src/components/global/user-name.vue
@@ -2,19 +2,14 @@
 <Mfm :text="user.name || user.username" :plain="true" :nowrap="nowrap" :custom-emojis="user.emojis"/>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { } from 'vue';
+import * as misskey from 'misskey-js';
 
-export default defineComponent({
-	props: {
-		user: {
-			type: Object,
-			required: true
-		},
-		nowrap: {
-			type: Boolean,
-			default: true
-		},
-	}
+const props = withDefaults(defineProps<{
+	user: misskey.entities.User;
+	nowrap?: boolean;
+}>(), {
+	nowrap: true,
 });
 </script>