diff --git a/packages/frontend/src/components/MkMarquee.vue b/packages/frontend/src/components/MkMarquee.vue
deleted file mode 100644
index 4a89d21b92..0000000000
--- a/packages/frontend/src/components/MkMarquee.vue
+++ /dev/null
@@ -1,112 +0,0 @@
-<!--
-SPDX-FileCopyrightText: syuilo and misskey-project
-SPDX-License-Identifier: AGPL-3.0-only
--->
-
-<script lang="ts">
-import { h, onMounted, onUnmounted, ref, watch } from 'vue';
-
-export default {
-	name: 'MarqueeText',
-	props: {
-		duration: {
-			type: Number,
-			default: 15,
-		},
-		repeat: {
-			type: Number,
-			default: 2,
-		},
-		paused: {
-			type: Boolean,
-			default: false,
-		},
-		reverse: {
-			type: Boolean,
-			default: false,
-		},
-	},
-	setup(props) {
-		const contentEl = ref<HTMLElement>();
-
-		function calc() {
-			if (contentEl.value == null) return;
-			const eachLength = contentEl.value.offsetWidth / props.repeat;
-			const factor = 3000;
-			const duration = props.duration / ((1 / eachLength) * factor);
-
-			contentEl.value.style.animationDuration = `${duration}s`;
-		}
-
-		watch(() => props.duration, calc);
-
-		onMounted(() => {
-			calc();
-		});
-
-		onUnmounted(() => {
-		});
-
-		return {
-			contentEl,
-		};
-	},
-	render({
-		$slots, $style, $props: {
-			duration, repeat, paused, reverse,
-		},
-	}) {
-		return h('div', { class: [$style.wrap] }, [
-			h('span', {
-				ref: 'contentEl',
-				class: [
-					paused
-						? $style.paused
-						: undefined,
-					$style.content,
-				],
-			}, Array(repeat).fill(
-				h('span', {
-					class: $style.text,
-					style: {
-						animationDirection: reverse
-							? 'reverse'
-							: undefined,
-					},
-				}, $slots.default()),
-			)),
-		]);
-	},
-};
-</script>
-
-<style lang="scss" module>
-.wrap {
-	overflow: clip;
-	animation-play-state: running;
-
-	&:hover {
-		animation-play-state: paused;
-	}
-}
-.content {
-	display: inline-block;
-	white-space: nowrap;
-	animation-play-state: inherit;
-}
-.text {
-	display: inline-block;
-	animation-name: marquee;
-	animation-timing-function: linear;
-	animation-iteration-count: infinite;
-	animation-duration: inherit;
-	animation-play-state: inherit;
-}
-.paused .text {
-	animation-play-state: paused;
-}
-@keyframes marquee {
-	0% { transform:translateX(0); }
-	100% { transform:translateX(-100%); }
-}
-</style>
diff --git a/packages/frontend/src/components/MkMarqueeText.vue b/packages/frontend/src/components/MkMarqueeText.vue
new file mode 100644
index 0000000000..a2c365afe9
--- /dev/null
+++ b/packages/frontend/src/components/MkMarqueeText.vue
@@ -0,0 +1,89 @@
+<!--
+SPDX-FileCopyrightText: syuilo and misskey-project
+SPDX-License-Identifier: AGPL-3.0-only
+-->
+
+<template>
+<div :class="$style.wrap">
+	<span
+		ref="contentEl"
+		:class="[$style.content, {
+			[$style.paused]: paused,
+			[$style.reverse]: reverse,
+		}]"
+	>
+		<span v-for="key in repeat" :key="key" :class="$style.text">
+			<slot></slot>
+		</span>
+	</span>
+</div>
+</template>
+
+<script lang="ts" setup>
+import { onMounted, useTemplateRef, watch } from 'vue';
+
+const props = withDefaults(defineProps<{
+	duration?: number;
+	repeat?: number;
+	paused?: boolean;
+	reverse?: boolean;
+}>(), {
+	duration: 15,
+	repeat: 2,
+	paused: false,
+	reverse: false,
+});
+
+const contentEl = useTemplateRef('contentEl');
+
+function calcDuration() {
+	if (contentEl.value == null) return;
+	const eachLength = contentEl.value.offsetWidth / props.repeat;
+	const factor = 3000;
+	const duration = props.duration / ((1 / eachLength) * factor);
+	contentEl.value.style.animationDuration = `${duration}s`;
+}
+
+watch(() => props.duration, calcDuration);
+
+onMounted(calcDuration);
+</script>
+
+<style lang="scss" module>
+.wrap {
+	overflow: clip;
+	animation-play-state: running;
+
+	&:hover {
+		animation-play-state: paused;
+	}
+}
+
+.content {
+	display: inline-block;
+	white-space: nowrap;
+	animation-play-state: inherit;
+}
+
+.text {
+	display: inline-block;
+	animation-name: marquee;
+	animation-timing-function: linear;
+	animation-iteration-count: infinite;
+	animation-duration: inherit;
+	animation-play-state: inherit;
+}
+
+.paused .text {
+	animation-play-state: paused;
+}
+
+.reverse .text {
+	animation-direction: reverse;
+}
+
+@keyframes marquee {
+	0% { transform: translateX(0); }
+	100% { transform: translateX(-100%); }
+}
+</style>
diff --git a/packages/frontend/src/pages/welcome.entrance.a.vue b/packages/frontend/src/pages/welcome.entrance.a.vue
index d131c17340..c2cf937c71 100644
--- a/packages/frontend/src/pages/welcome.entrance.a.vue
+++ b/packages/frontend/src/pages/welcome.entrance.a.vue
@@ -17,13 +17,13 @@ SPDX-License-Identifier: AGPL-3.0-only
 		<MkVisitorDashboard/>
 	</div>
 	<div v-if="instances && instances.length > 0" :class="$style.federation">
-		<MarqueeText :duration="40">
+		<MkMarqueeText :duration="40">
 			<MkA v-for="instance in instances" :key="instance.id" :class="$style.federationInstance" :to="`/instance-info/${instance.host}`" behavior="window">
 				<!--<MkInstanceCardMini :instance="instance"/>-->
 				<img v-if="instance.iconUrl" :class="$style.federationInstanceIcon" :src="getInstanceIcon(instance)" alt=""/>
 				<span class="_monospace">{{ instance.host }}</span>
 			</MkA>
-		</MarqueeText>
+		</MkMarqueeText>
 	</div>
 </div>
 </template>
@@ -32,7 +32,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
 import XTimeline from './welcome.timeline.vue';
-import MarqueeText from '@/components/MkMarquee.vue';
+import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import MkFeaturedPhotos from '@/components/MkFeaturedPhotos.vue';
 import misskeysvg from '/client-assets/misskey.svg';
 import { misskeyApiGet } from '@/utility/misskey-api.js';
diff --git a/packages/frontend/src/ui/_common_/statusbar-federation.vue b/packages/frontend/src/ui/_common_/statusbar-federation.vue
index 16e72fa227..7248e8826b 100644
--- a/packages/frontend/src/ui/_common_/statusbar-federation.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-federation.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:leaveToClass="$style.transition_change_leaveTo"
 			mode="default"
 		>
-			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+			<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
 				<span v-for="instance in instances" :key="instance.id" :class="[$style.item, { [$style.colored]: colored }]" :style="{ background: colored ? instance.themeColor : null }">
 					<img :class="$style.icon" :src="getInstanceIcon(instance)" alt=""/>
 					<MkA :to="`/instance-info/${instance.host}`" :class="$style.host" class="_monospace">
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkA>
 					<span></span>
 				</span>
-			</MarqueeText>
+			</MkMarqueeText>
 		</Transition>
 	</template>
 	<template v-else-if="display === 'oneByOne'">
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import MarqueeText from '@/components/MkMarquee.vue';
+import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { getProxiedImageUrlNullable } from '@/utility/media-proxy.js';
diff --git a/packages/frontend/src/ui/_common_/statusbar-rss.vue b/packages/frontend/src/ui/_common_/statusbar-rss.vue
index 4da89a181e..7db0d5267d 100644
--- a/packages/frontend/src/ui/_common_/statusbar-rss.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-rss.vue
@@ -13,11 +13,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:leaveToClass="$style.transition_change_leaveTo"
 			mode="default"
 		>
-			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+			<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
 				<span v-for="item in items" :class="$style.item">
 					<a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span>
 				</span>
-			</MarqueeText>
+			</MkMarqueeText>
 		</Transition>
 	</template>
 	<template v-else-if="display === 'oneByOne'">
@@ -29,7 +29,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref } from 'vue';
 import * as Misskey from 'misskey-js';
-import MarqueeText from '@/components/MkMarquee.vue';
+import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import { useInterval } from '@@/js/use-interval.js';
 import { shuffle } from '@/utility/shuffle.js';
 
diff --git a/packages/frontend/src/ui/_common_/statusbar-user-list.vue b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
index c5bee51162..13139a1064 100644
--- a/packages/frontend/src/ui/_common_/statusbar-user-list.vue
+++ b/packages/frontend/src/ui/_common_/statusbar-user-list.vue
@@ -13,7 +13,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 			:leaveToClass="$style.transition_change_leaveTo"
 			mode="default"
 		>
-			<MarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
+			<MkMarqueeText :key="key" :duration="marqueeDuration" :reverse="marqueeReverse">
 				<span v-for="note in notes" :key="note.id" :class="$style.item">
 					<img :class="$style.avatar" :src="note.user.avatarUrl" decoding="async"/>
 					<MkA :class="$style.text" :to="notePage(note)">
@@ -21,7 +21,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 					</MkA>
 					<span :class="$style.divider"></span>
 				</span>
-			</MarqueeText>
+			</MkMarqueeText>
 		</Transition>
 	</template>
 	<template v-else-if="display === 'oneByOne'">
@@ -33,7 +33,7 @@ SPDX-License-Identifier: AGPL-3.0-only
 <script lang="ts" setup>
 import { ref, watch } from 'vue';
 import * as Misskey from 'misskey-js';
-import MarqueeText from '@/components/MkMarquee.vue';
+import MkMarqueeText from '@/components/MkMarqueeText.vue';
 import { misskeyApi } from '@/utility/misskey-api.js';
 import { useInterval } from '@@/js/use-interval.js';
 import { getNoteSummary } from '@/utility/get-note-summary.js';
diff --git a/packages/frontend/src/widgets/WidgetRssTicker.vue b/packages/frontend/src/widgets/WidgetRssTicker.vue
index b5be4d35c2..7fe7c6111a 100644
--- a/packages/frontend/src/widgets/WidgetRssTicker.vue
+++ b/packages/frontend/src/widgets/WidgetRssTicker.vue
@@ -15,11 +15,11 @@ SPDX-License-Identifier: AGPL-3.0-only
 		</div>
 		<div v-else>
 			<Transition :name="$style.change" mode="default" appear>
-				<MarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse">
+				<MkMarqueeText :key="key" :duration="widgetProps.duration" :reverse="widgetProps.reverse">
 					<span v-for="item in items" :key="item.link" :class="$style.item">
 						<a :href="item.link" rel="nofollow noopener" target="_blank" :title="item.title">{{ item.title }}</a><span :class="$style.divider"></span>
 					</span>
-				</MarqueeText>
+				</MkMarqueeText>
 			</Transition>
 		</div>
 	</div>
@@ -31,7 +31,7 @@ import { ref, watch, computed } from 'vue';
 import * as Misskey from 'misskey-js';
 import { useWidgetPropsManager } from './widget.js';
 import type { WidgetComponentEmits, WidgetComponentExpose, WidgetComponentProps } from './widget.js';
-import MarqueeText from '@/components/MkMarquee.vue';
+import MarqueeText from '@/components/MkMarqueeText.vue';
 import type { GetFormResultType } from '@/utility/form.js';
 import MkContainer from '@/components/MkContainer.vue';
 import { shuffle } from '@/utility/shuffle.js';