diff --git a/packages/frontend/src/components/MkFoldableSection.vue b/packages/frontend/src/components/MkFoldableSection.vue
index 475e01c8d4..f52c66a8be 100644
--- a/packages/frontend/src/components/MkFoldableSection.vue
+++ b/packages/frontend/src/components/MkFoldableSection.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="ssazuxis">
+<div ref="el" class="ssazuxis">
 	<header class="_button" :style="{ background: bg }" @click="showBody = !showBody">
 		<div class="title"><div><slot name="header"></slot></div></div>
 		<div class="divider"></div>
@@ -22,80 +22,67 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { onMounted, ref, shallowRef, watch } from 'vue';
 import tinycolor from 'tinycolor2';
 import { miLocalStorage } from '@/local-storage';
 import { defaultStore } from '@/store';
 
 const miLocalStoragePrefix = 'ui:folder:' as const;
 
-export default defineComponent({
-	props: {
-		expanded: {
-			type: Boolean,
-			required: false,
-			default: true,
-		},
-		persistKey: {
-			type: String,
-			required: false,
-			default: null,
-		},
-	},
-	data() {
-		return {
-			defaultStore,
-			bg: null,
-			showBody: (this.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${this.persistKey}`) === 't') : this.expanded,
-		};
-	},
-	watch: {
-		showBody() {
-			if (this.persistKey) {
-				miLocalStorage.setItem(`${miLocalStoragePrefix}${this.persistKey}`, this.showBody ? 't' : 'f');
-			}
-		},
-	},
-	mounted() {
-		function getParentBg(el: Element | null): string {
-			if (el == null || el.tagName === 'BODY') return 'var(--bg)';
-			const bg = el.style.background || el.style.backgroundColor;
-			if (bg) {
-				return bg;
-			} else {
-				return getParentBg(el.parentElement);
-			}
-		}
-		const rawBg = getParentBg(this.$el);
-		const bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
-		bg.setAlpha(0.85);
-		this.bg = bg.toRgbString();
-	},
-	methods: {
-		toggleContent(show: boolean) {
-			this.showBody = show;
-		},
+const props = withDefaults(defineProps<{
+	expanded?: boolean;
+	persistKey?: string;
+}>(), {
+	expanded: true,
+});
 
-		enter(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = 0;
-			el.offsetHeight; // reflow
-			el.style.height = elementHeight + 'px';
-		},
-		afterEnter(el) {
-			el.style.height = null;
-		},
-		leave(el) {
-			const elementHeight = el.getBoundingClientRect().height;
-			el.style.height = elementHeight + 'px';
-			el.offsetHeight; // reflow
-			el.style.height = 0;
-		},
-		afterLeave(el) {
-			el.style.height = null;
-		},
-	},
+const el = shallowRef<HTMLDivElement>();
+const bg = ref<string | null>(null);
+const showBody = ref((props.persistKey && miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`)) ? (miLocalStorage.getItem(`${miLocalStoragePrefix}${props.persistKey}`) === 't') : props.expanded);
+
+watch(showBody, () => {
+	if (props.persistKey) {
+		miLocalStorage.setItem(`${miLocalStoragePrefix}${props.persistKey}`, showBody.value ? 't' : 'f');
+	}
+});
+
+function enter(el: Element) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = 0;
+	el.offsetHeight; // reflow
+	el.style.height = elementHeight + 'px';
+}
+
+function afterEnter(el: Element) {
+	el.style.height = null;
+}
+
+function leave(el: Element) {
+	const elementHeight = el.getBoundingClientRect().height;
+	el.style.height = elementHeight + 'px';
+	el.offsetHeight; // reflow
+	el.style.height = 0;
+}
+
+function afterLeave(el: Element) {
+	el.style.height = null;
+}
+
+onMounted(() => {
+	function getParentBg(el: HTMLElement | null): string {
+		if (el == null || el.tagName === 'BODY') return 'var(--bg)';
+		const bg = el.style.background || el.style.backgroundColor;
+		if (bg) {
+			return bg;
+		} else {
+			return getParentBg(el.parentElement);
+		}
+	}
+	const rawBg = getParentBg(el.value);
+	const _bg = tinycolor(rawBg.startsWith('var(') ? getComputedStyle(document.documentElement).getPropertyValue(rawBg.slice(4, -1)) : rawBg);
+	_bg.setAlpha(0.85);
+	bg.value = _bg.toRgbString();
 });
 </script>
 
diff --git a/packages/frontend/src/components/MkFormDialog.vue b/packages/frontend/src/components/MkFormDialog.vue
index 979df2e7c1..b4ef54aecd 100644
--- a/packages/frontend/src/components/MkFormDialog.vue
+++ b/packages/frontend/src/components/MkFormDialog.vue
@@ -54,8 +54,8 @@
 </MkModalWindow>
 </template>
 
-<script lang="ts">
-import { defineComponent } from 'vue';
+<script lang="ts" setup>
+import { reactive, shallowRef } from 'vue';
 import MkInput from './MkInput.vue';
 import MkTextarea from './MkTextarea.vue';
 import MkSwitch from './MkSwitch.vue';
@@ -66,58 +66,36 @@ import MkRadios from './MkRadios.vue';
 import MkModalWindow from '@/components/MkModalWindow.vue';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkModalWindow,
-		MkInput,
-		MkTextarea,
-		MkSwitch,
-		MkSelect,
-		MkRange,
-		MkButton,
-		MkRadios,
-	},
+const props = defineProps<{
+	title: string;
+	form: any;
+}>();
 
-	props: {
-		title: {
-			type: String,
-			required: true,
-		},
-		form: {
-			type: Object,
-			required: true,
-		},
-	},
+const emit = defineEmits<{
+	(ev: 'done', v: {
+		canceled?: boolean;
+		result?: any;
+	}): void;
+}>();
 
-	emits: ['done'],
+const dialog = shallowRef<InstanceType<typeof MkModalWindow>>();
+const values = reactive({});
 
-	data() {
-		return {
-			values: {},
-			i18n,
-		};
-	},
+for (const item in props.form) {
+	values[item] = props.form[item].default ?? null;
+}
 
-	created() {
-		for (const item in this.form) {
-			this.values[item] = this.form[item].default ?? null;
-		}
-	},
+function ok() {
+	emit('done', {
+		result: values,
+	});
+	dialog.value.close();
+}
 
-	methods: {
-		ok() {
-			this.$emit('done', {
-				result: this.values,
-			});
-			this.$refs.dialog.close();
-		},
-
-		cancel() {
-			this.$emit('done', {
-				canceled: true,
-			});
-			this.$refs.dialog.close();
-		},
-	},
-});
+function cancel() {
+	emit('done', {
+		canceled: true,
+	});
+	dialog.value.close();
+}
 </script>
diff --git a/packages/frontend/src/components/MkObjectView.value.vue b/packages/frontend/src/components/MkObjectView.value.vue
index e7fc73bce3..d48e7886eb 100644
--- a/packages/frontend/src/components/MkObjectView.value.vue
+++ b/packages/frontend/src/components/MkObjectView.value.vue
@@ -28,54 +28,38 @@
 </div>
 </template>
 
-<script lang="ts">
-import { defineComponent, reactive } from 'vue';
+<script lang="ts" setup>
+import { reactive } from 'vue';
 import number from '@/filters/number';
+import XValue from '@/components/MkObjectView.value.vue';
 
-export default defineComponent({
-	name: 'XValue',
+const props = defineProps<{
+	value: any;
+}>();
 
-	props: {
-		value: {
-			required: true,
-		},
-	},
+const collapsed = reactive({});
 
-	setup(props) {
-		const collapsed = reactive({});
+if (isObject(props.value)) {
+	for (const key in props.value) {
+		collapsed[key] = collapsable(props.value[key]);
+	}
+}
 
-		if (isObject(props.value)) {
-			for (const key in props.value) {
-				collapsed[key] = collapsable(props.value[key]);
-			}
-		}
+function isObject(v): boolean {
+	return typeof v === 'object' && !Array.isArray(v) && v !== null;
+}
 
-		function isObject(v): boolean {
-			return typeof v === 'object' && !Array.isArray(v) && v !== null;
-		}
+function isArray(v): boolean {
+	return Array.isArray(v);
+}
 
-		function isArray(v): boolean {
-			return Array.isArray(v);
-		}
+function isEmpty(v): boolean {
+	return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
+}
 
-		function isEmpty(v): boolean {
-			return (isArray(v) && v.length === 0) || (isObject(v) && Object.keys(v).length === 0);
-		}
-
-		function collapsable(v): boolean {
-			return (isObject(v) || isArray(v)) && !isEmpty(v);
-		}
-
-		return {
-			number,
-			collapsed,
-			isObject,
-			isArray,
-			isEmpty,
-			collapsable,
-		};
-	},
-});
+function collapsable(v): boolean {
+	return (isObject(v) || isArray(v)) && !isEmpty(v);
+}
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/MkRadios.vue b/packages/frontend/src/components/MkRadios.vue
index e2240fb4e1..84be10078a 100644
--- a/packages/frontend/src/components/MkRadios.vue
+++ b/packages/frontend/src/components/MkRadios.vue
@@ -1,37 +1,27 @@
 <script lang="ts">
-import { VNode, defineComponent, h } from 'vue';
+import { VNode, defineComponent, h, ref, watch } from 'vue';
 import MkRadio from './MkRadio.vue';
 
 export default defineComponent({
-	components: {
-		MkRadio,
-	},
 	props: {
 		modelValue: {
 			required: false,
 		},
 	},
-	data() {
-		return {
-			value: this.modelValue,
-		};
-	},
-	watch: {
-		value() {
-			this.$emit('update:modelValue', this.value);
-		},
-	},
-	render() {
-		console.log(this.$slots, this.$slots.label && this.$slots.label());
-		if (!this.$slots.default) return null;
-		let options = this.$slots.default();
-		const label = this.$slots.label && this.$slots.label();
-		const caption = this.$slots.caption && this.$slots.caption();
+	setup(props, context) {
+		const value = ref(props.modelValue);
+		watch(value, () => {
+			context.emit('update:modelValue', value.value);
+		});
+		if (!context.slots.default) return null;
+		let options = context.slots.default();
+		const label = context.slots.label && context.slots.label();
+		const caption = context.slots.caption && context.slots.caption();
 
 		// なぜかFragmentになることがあるため
 		if (options.length === 1 && options[0].props == null) options = options[0].children as VNode[];
 
-		return h('div', {
+		return () => h('div', {
 			class: 'novjtcto',
 		}, [
 			...(label ? [h('div', {
@@ -42,8 +32,8 @@ export default defineComponent({
 			}, options.map(option => h(MkRadio, {
 				key: option.key,
 				value: option.props?.value,
-				modelValue: this.value,
-				'onUpdate:modelValue': value => this.value = value,
+				modelValue: value.value,
+				'onUpdate:modelValue': _v => value.value = _v,
 			}, () => option.children)),
 			),
 			...(caption ? [h('div', {
diff --git a/packages/frontend/src/components/MkTab.vue b/packages/frontend/src/components/MkTab.vue
index 6f819bbbd7..7274f9b310 100644
--- a/packages/frontend/src/components/MkTab.vue
+++ b/packages/frontend/src/components/MkTab.vue
@@ -7,17 +7,17 @@ export default defineComponent({
 			required: true,
 		},
 	},
-	render() {
-		const options = this.$slots.default();
+	setup(props, { emit, slots }) {
+		const options = slots.default();
 
-		return h('div', {
+		return () => h('div', {
 			class: 'pxhvhrfw',
 		}, options.map(option => withDirectives(h('button', {
-			class: ['_button', { active: this.modelValue === option.props.value }],
+			class: ['_button', { active: props.modelValue === option.props.value }],
 			key: option.key,
-			disabled: this.modelValue === option.props.value,
+			disabled: props.modelValue === option.props.value,
 			onClick: () => {
-				this.$emit('update:modelValue', option.props.value);
+				emit('update:modelValue', option.props.value);
 			},
 		}, option.children), [
 			[resolveDirective('click-anime')],
diff --git a/packages/frontend/src/components/form/suspense.vue b/packages/frontend/src/components/form/suspense.vue
index 3a44c3da3d..9b39858ca1 100644
--- a/packages/frontend/src/components/form/suspense.vue
+++ b/packages/frontend/src/components/form/suspense.vue
@@ -15,70 +15,49 @@
 </Transition>
 </template>
 
-<script lang="ts">
-import { defineComponent, PropType, ref, watch } from 'vue';
+<script lang="ts" setup>
+import { ref, watch } from 'vue';
 import MkButton from '@/components/MkButton.vue';
 import { defaultStore } from '@/store';
 import { i18n } from '@/i18n';
 
-export default defineComponent({
-	components: {
-		MkButton,
-	},
+const props = defineProps<{
+	p: () => Promise<any>;
+}>();
 
-	props: {
-		p: {
-			type: Function as PropType<() => Promise<any>>,
-			required: true,
-		},
-	},
+const pending = ref(true);
+const resolved = ref(false);
+const rejected = ref(false);
+const result = ref(null);
 
-	setup(props, context) {
-		const pending = ref(true);
-		const resolved = ref(false);
-		const rejected = ref(false);
-		const result = ref(null);
+const process = () => {
+	if (props.p == null) {
+		return;
+	}
+	const promise = props.p();
+	pending.value = true;
+	resolved.value = false;
+	rejected.value = false;
+	promise.then((_result) => {
+		pending.value = false;
+		resolved.value = true;
+		result.value = _result;
+	});
+	promise.catch(() => {
+		pending.value = false;
+		rejected.value = true;
+	});
+};
 
-		const process = () => {
-			if (props.p == null) {
-				return;
-			}
-			const promise = props.p();
-			pending.value = true;
-			resolved.value = false;
-			rejected.value = false;
-			promise.then((_result) => {
-				pending.value = false;
-				resolved.value = true;
-				result.value = _result;
-			});
-			promise.catch(() => {
-				pending.value = false;
-				rejected.value = true;
-			});
-		};
-
-		watch(() => props.p, () => {
-			process();
-		}, {
-			immediate: true,
-		});
-
-		const retry = () => {
-			process();
-		};
-
-		return {
-			pending,
-			resolved,
-			rejected,
-			result,
-			retry,
-			defaultStore,
-			i18n,
-		};
-	},
+watch(() => props.p, () => {
+	process();
+}, {
+	immediate: true,
 });
+
+const retry = () => {
+	process();
+};
 </script>
 
 <style lang="scss" scoped>
diff --git a/packages/frontend/src/components/global/i18n.ts b/packages/frontend/src/components/global/i18n.ts
index 1fd293ba10..2708b759aa 100644
--- a/packages/frontend/src/components/global/i18n.ts
+++ b/packages/frontend/src/components/global/i18n.ts
@@ -1,42 +1,24 @@
-import { h, defineComponent } from 'vue';
+import { h } from 'vue';
 
-export default defineComponent({
-	props: {
-		src: {
-			type: String,
-			required: true,
-		},
-		tag: {
-			type: String,
-			required: false,
-			default: 'span',
-		},
-		textTag: {
-			type: String,
-			required: false,
-			default: null,
-		},
-	},
-	render() {
-		let str = this.src;
-		const parsed = [] as (string | { arg: string; })[];
-		while (true) {
-			const nextBracketOpen = str.indexOf('{');
-			const nextBracketClose = str.indexOf('}');
+export default function(props: { src: string; tag?: string; textTag?: string; }, { slots }) {
+	let str = props.src;
+	const parsed = [] as (string | { arg: string; })[];
+	while (true) {
+		const nextBracketOpen = str.indexOf('{');
+		const nextBracketClose = str.indexOf('}');
 
-			if (nextBracketOpen === -1) {
-				parsed.push(str);
-				break;
-			} else {
-				if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
-				parsed.push({
-					arg: str.substring(nextBracketOpen + 1, nextBracketClose),
-				});
-			}
-
-			str = str.substr(nextBracketClose + 1);
+		if (nextBracketOpen === -1) {
+			parsed.push(str);
+			break;
+		} else {
+			if (nextBracketOpen > 0) parsed.push(str.substr(0, nextBracketOpen));
+			parsed.push({
+				arg: str.substring(nextBracketOpen + 1, nextBracketClose),
+			});
 		}
 
-		return h(this.tag, parsed.map(x => typeof x === 'string' ? (this.textTag ? h(this.textTag, x) : x) : this.$slots[x.arg]()));
-	},
-});
+		str = str.substr(nextBracketClose + 1);
+	}
+
+	return h(props.tag ?? 'span', parsed.map(x => typeof x === 'string' ? (props.textTag ? h(props.textTag, x) : x) : slots[x.arg]()));
+}
diff --git a/packages/frontend/src/components/mfm.ts b/packages/frontend/src/components/mfm.ts
index c3c07b5834..042dad7809 100644
--- a/packages/frontend/src/components/mfm.ts
+++ b/packages/frontend/src/components/mfm.ts
@@ -1,5 +1,6 @@
-import { VNode, defineComponent, h } from 'vue';
+import { VNode, h } from 'vue';
 import * as mfm from 'mfm-js';
+import * as Misskey from 'misskey-js';
 import MkUrl from '@/components/global/MkUrl.vue';
 import MkLink from '@/components/MkLink.vue';
 import MkMention from '@/components/MkMention.vue';
@@ -21,370 +22,343 @@ border-left: solid 3px var(--fg);
 opacity: 0.7;
 `.split('\n').join(' ');
 
-export default defineComponent({
-	props: {
-		text: {
-			type: String,
-			required: true,
-		},
-		plain: {
-			type: Boolean,
-			default: false,
-		},
-		nowrap: {
-			type: Boolean,
-			default: false,
-		},
-		author: {
-			type: Object,
-			default: null,
-		},
-		i: {
-			type: Object,
-			default: null,
-		},
-		isNote: {
-			type: Boolean,
-			default: true,
-		},
-		emojiUrls: {
-			type: Object,
-			default: null,
-		},
-		rootScale: {
-			type: Number,
-			default: 1,
-		}
-	},
+export default function(props: {
+	text: string;
+	plain?: boolean;
+	nowrap?: boolean;
+	author?: Misskey.entities.UserLite;
+	i?: Misskey.entities.UserLite;
+	isNote?: boolean;
+	emojiUrls?: string[];
+	rootScale?: number;
+}) {
+	const isNote = props.isNote !== undefined ? props.isNote : true;
 
-	render() {
-		if (this.text == null || this.text === '') return;
+	if (props.text == null || props.text === '') return;
 
-		const ast = (this.plain ? mfm.parseSimple : mfm.parse)(this.text);
+	const ast = (props.plain ? mfm.parseSimple : mfm.parse)(props.text);
 
-		const validTime = (t: string | null | undefined) => {
-			if (t == null) return null;
-			return t.match(/^[0-9.]+s$/) ? t : null;
-		};
+	const validTime = (t: string | null | undefined) => {
+		if (t == null) return null;
+		return t.match(/^[0-9.]+s$/) ? t : null;
+	};
 
-		const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
+	const useAnim = defaultStore.state.advancedMfm && defaultStore.state.animatedMfm;
 
-		/**
-		 * Gen Vue Elements from MFM AST
-		 * @param ast MFM AST
-		 * @param scale How times large the text is
-		 */
-		const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
-			switch (token.type) {
-				case 'text': {
-					const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
+	/**
+	 * Gen Vue Elements from MFM AST
+	 * @param ast MFM AST
+	 * @param scale How times large the text is
+	 */
+	const genEl = (ast: mfm.MfmNode[], scale: number) => ast.map((token): VNode | string | (VNode | string)[] => {
+		switch (token.type) {
+			case 'text': {
+				const text = token.props.text.replace(/(\r\n|\n|\r)/g, '\n');
 
-					if (!this.plain) {
-						const res: (VNode | string)[] = [];
-						for (const t of text.split('\n')) {
-							res.push(h('br'));
-							res.push(t);
-						}
-						res.shift();
-						return res;
-					} else {
-						return [text.replace(/\n/g, ' ')];
+				if (!props.plain) {
+					const res: (VNode | string)[] = [];
+					for (const t of text.split('\n')) {
+						res.push(h('br'));
+						res.push(t);
 					}
+					res.shift();
+					return res;
+				} else {
+					return [text.replace(/\n/g, ' ')];
 				}
+			}
 
-				case 'bold': {
-					return [h('b', genEl(token.children, scale))];
-				}
+			case 'bold': {
+				return [h('b', genEl(token.children, scale))];
+			}
 
-				case 'strike': {
-					return [h('del', genEl(token.children, scale))];
-				}
+			case 'strike': {
+				return [h('del', genEl(token.children, scale))];
+			}
 
-				case 'italic': {
-					return h('i', {
-						style: 'font-style: oblique;',
-					}, genEl(token.children, scale));
-				}
+			case 'italic': {
+				return h('i', {
+					style: 'font-style: oblique;',
+				}, genEl(token.children, scale));
+			}
 
-				case 'fn': {
-					// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
-					let style;
-					switch (token.props.name) {
-						case 'tada': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
-							break;
-						}
-						case 'jelly': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
-							break;
-						}
-						case 'twitch': {
-							const speed = validTime(token.props.args.speed) ?? '0.5s';
-							style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'shake': {
-							const speed = validTime(token.props.args.speed) ?? '0.5s';
-							style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
-							break;
-						}
-						case 'spin': {
-							const direction =
-								token.props.args.left ? 'reverse' :
-								token.props.args.alternate ? 'alternate' :
-								'normal';
-							const anime =
-								token.props.args.x ? 'mfm-spinX' :
-								token.props.args.y ? 'mfm-spinY' :
-								'mfm-spin';
-							const speed = validTime(token.props.args.speed) ?? '1.5s';
-							style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
-							break;
-						}
-						case 'jump': {
-							const speed = validTime(token.props.args.speed) ?? '0.75s';
-							style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
-							break;
-						}
-						case 'bounce': {
-							const speed = validTime(token.props.args.speed) ?? '0.75s';
-							style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
-							break;
-						}
-						case 'flip': {
-							const transform =
-								(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
-								token.props.args.v ? 'scaleY(-1)' :
-								'scaleX(-1)';
-							style = `transform: ${transform};`;
-							break;
-						}
-						case 'x2': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
-							}, genEl(token.children, scale * 2));
-						}
-						case 'x3': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
-							}, genEl(token.children, scale * 3));
-						}
-						case 'x4': {
-							return h('span', {
-								class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
-							}, genEl(token.children, scale * 4));
-						}
-						case 'font': {
-							const family =
-								token.props.args.serif ? 'serif' :
-								token.props.args.monospace ? 'monospace' :
-								token.props.args.cursive ? 'cursive' :
-								token.props.args.fantasy ? 'fantasy' :
-								token.props.args.emoji ? 'emoji' :
-								token.props.args.math ? 'math' :
-								null;
-							if (family) style = `font-family: ${family};`;
-							break;
-						}
-						case 'blur': {
-							return h('span', {
-								class: '_mfm_blur_',
-							}, genEl(token.children, scale));
-						}
-						case 'rainbow': {
-							const speed = validTime(token.props.args.speed) ?? '1s';
-							style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
-							break;
-						}
-						case 'sparkle': {
-							if (!useAnim) {
-								return genEl(token.children, scale);
-							}
-							return h(MkSparkle, {}, genEl(token.children, scale));
-						}
-						case 'rotate': {
-							const degrees = parseFloat(token.props.args.deg ?? '90');
-							style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
-							break;
-						}
-						case 'position': {
-							if (!defaultStore.state.advancedMfm) break;
-							const x = parseFloat(token.props.args.x ?? '0');
-							const y = parseFloat(token.props.args.y ?? '0');
-							style = `transform: translateX(${x}em) translateY(${y}em);`;
-							break;
-						}
-						case 'scale': {
-							if (!defaultStore.state.advancedMfm) {
-								style = '';
-								break;
-							}
-							const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
-							const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
-							style = `transform: scale(${x}, ${y});`; 
-							scale = scale * Math.max(x, y);
-							break;
-						}
-						case 'fg': {
-							let color = token.props.args.color;
-							if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
-							style = `color: #${color};`;
-							break;
-						}
-						case 'bg': {
-							let color = token.props.args.color;
-							if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
-							style = `background-color: #${color};`;
-							break;
-						}
+			case 'fn': {
+				// TODO: CSSを文字列で組み立てていくと token.props.args.~~~ 経由でCSSインジェクションできるのでよしなにやる
+				let style;
+				switch (token.props.name) {
+					case 'tada': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = 'font-size: 150%;' + (useAnim ? `animation: tada ${speed} linear infinite both;` : '');
+						break;
 					}
-					if (style == null) {
-						return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
-					} else {
+					case 'jelly': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = (useAnim ? `animation: mfm-rubberBand ${speed} linear infinite both;` : '');
+						break;
+					}
+					case 'twitch': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						style = useAnim ? `animation: mfm-twitch ${speed} ease infinite;` : '';
+						break;
+					}
+					case 'shake': {
+						const speed = validTime(token.props.args.speed) ?? '0.5s';
+						style = useAnim ? `animation: mfm-shake ${speed} ease infinite;` : '';
+						break;
+					}
+					case 'spin': {
+						const direction =
+							token.props.args.left ? 'reverse' :
+							token.props.args.alternate ? 'alternate' :
+							'normal';
+						const anime =
+							token.props.args.x ? 'mfm-spinX' :
+							token.props.args.y ? 'mfm-spinY' :
+							'mfm-spin';
+						const speed = validTime(token.props.args.speed) ?? '1.5s';
+						style = useAnim ? `animation: ${anime} ${speed} linear infinite; animation-direction: ${direction};` : '';
+						break;
+					}
+					case 'jump': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						style = useAnim ? `animation: mfm-jump ${speed} linear infinite;` : '';
+						break;
+					}
+					case 'bounce': {
+						const speed = validTime(token.props.args.speed) ?? '0.75s';
+						style = useAnim ? `animation: mfm-bounce ${speed} linear infinite; transform-origin: center bottom;` : '';
+						break;
+					}
+					case 'flip': {
+						const transform =
+							(token.props.args.h && token.props.args.v) ? 'scale(-1, -1)' :
+							token.props.args.v ? 'scaleY(-1)' :
+							'scaleX(-1)';
+						style = `transform: ${transform};`;
+						break;
+					}
+					case 'x2': {
 						return h('span', {
-							style: 'display: inline-block; ' + style,
+							class: defaultStore.state.advancedMfm ? 'mfm-x2' : '',
+						}, genEl(token.children, scale * 2));
+					}
+					case 'x3': {
+						return h('span', {
+							class: defaultStore.state.advancedMfm ? 'mfm-x3' : '',
+						}, genEl(token.children, scale * 3));
+					}
+					case 'x4': {
+						return h('span', {
+							class: defaultStore.state.advancedMfm ? 'mfm-x4' : '',
+						}, genEl(token.children, scale * 4));
+					}
+					case 'font': {
+						const family =
+							token.props.args.serif ? 'serif' :
+							token.props.args.monospace ? 'monospace' :
+							token.props.args.cursive ? 'cursive' :
+							token.props.args.fantasy ? 'fantasy' :
+							token.props.args.emoji ? 'emoji' :
+							token.props.args.math ? 'math' :
+							null;
+						if (family) style = `font-family: ${family};`;
+						break;
+					}
+					case 'blur': {
+						return h('span', {
+							class: '_mfm_blur_',
 						}, genEl(token.children, scale));
 					}
-				}
-
-				case 'small': {
-					return [h('small', {
-						style: 'opacity: 0.7;',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'center': {
-					return [h('div', {
-						style: 'text-align:center;',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'url': {
-					return [h(MkUrl, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					})];
-				}
-
-				case 'link': {
-					return [h(MkLink, {
-						key: Math.random(),
-						url: token.props.url,
-						rel: 'nofollow noopener',
-					}, genEl(token.children, scale))];
-				}
-
-				case 'mention': {
-					return [h(MkMention, {
-						key: Math.random(),
-						host: (token.props.host == null && this.author && this.author.host != null ? this.author.host : token.props.host) || host,
-						username: token.props.username,
-					})];
-				}
-
-				case 'hashtag': {
-					return [h(MkA, {
-						key: Math.random(),
-						to: this.isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
-						style: 'color:var(--hashtag);',
-					}, `#${token.props.hashtag}`)];
-				}
-
-				case 'blockCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						lang: token.props.lang,
-					})];
-				}
-
-				case 'inlineCode': {
-					return [h(MkCode, {
-						key: Math.random(),
-						code: token.props.code,
-						inline: true,
-					})];
-				}
-
-				case 'quote': {
-					if (!this.nowrap) {
-						return [h('div', {
-							style: QUOTE_STYLE,
-						}, genEl(token.children, scale))];
-					} else {
-						return [h('span', {
-							style: QUOTE_STYLE,
-						}, genEl(token.children, scale))];
+					case 'rainbow': {
+						const speed = validTime(token.props.args.speed) ?? '1s';
+						style = useAnim ? `animation: mfm-rainbow ${speed} linear infinite;` : '';
+						break;
+					}
+					case 'sparkle': {
+						if (!useAnim) {
+							return genEl(token.children, scale);
+						}
+						return h(MkSparkle, {}, genEl(token.children, scale));
+					}
+					case 'rotate': {
+						const degrees = parseFloat(token.props.args.deg ?? '90');
+						style = `transform: rotate(${degrees}deg); transform-origin: center center;`;
+						break;
+					}
+					case 'position': {
+						if (!defaultStore.state.advancedMfm) break;
+						const x = parseFloat(token.props.args.x ?? '0');
+						const y = parseFloat(token.props.args.y ?? '0');
+						style = `transform: translateX(${x}em) translateY(${y}em);`;
+						break;
+					}
+					case 'scale': {
+						if (!defaultStore.state.advancedMfm) {
+							style = '';
+							break;
+						}
+						const x = Math.min(parseFloat(token.props.args.x ?? '1'), 5);
+						const y = Math.min(parseFloat(token.props.args.y ?? '1'), 5);
+						style = `transform: scale(${x}, ${y});`; 
+						scale = scale * Math.max(x, y);
+						break;
+					}
+					case 'fg': {
+						let color = token.props.args.color;
+						if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+						style = `color: #${color};`;
+						break;
+					}
+					case 'bg': {
+						let color = token.props.args.color;
+						if (!/^[0-9a-f]{3,6}$/i.test(color)) color = 'f00';
+						style = `background-color: #${color};`;
+						break;
 					}
 				}
+				if (style == null) {
+					return h('span', {}, ['$[', token.props.name, ' ', ...genEl(token.children, scale), ']']);
+				} else {
+					return h('span', {
+						style: 'display: inline-block; ' + style,
+					}, genEl(token.children, scale));
+				}
+			}
 
-				case 'emojiCode': {
+			case 'small': {
+				return [h('small', {
+					style: 'opacity: 0.7;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'center': {
+				return [h('div', {
+					style: 'text-align:center;',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'url': {
+				return [h(MkUrl, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				})];
+			}
+
+			case 'link': {
+				return [h(MkLink, {
+					key: Math.random(),
+					url: token.props.url,
+					rel: 'nofollow noopener',
+				}, genEl(token.children, scale))];
+			}
+
+			case 'mention': {
+				return [h(MkMention, {
+					key: Math.random(),
+					host: (token.props.host == null && props.author && props.author.host != null ? props.author.host : token.props.host) || host,
+					username: token.props.username,
+				})];
+			}
+
+			case 'hashtag': {
+				return [h(MkA, {
+					key: Math.random(),
+					to: isNote ? `/tags/${encodeURIComponent(token.props.hashtag)}` : `/user-tags/${encodeURIComponent(token.props.hashtag)}`,
+					style: 'color:var(--hashtag);',
+				}, `#${token.props.hashtag}`)];
+			}
+
+			case 'blockCode': {
+				return [h(MkCode, {
+					key: Math.random(),
+					code: token.props.code,
+					lang: token.props.lang,
+				})];
+			}
+
+			case 'inlineCode': {
+				return [h(MkCode, {
+					key: Math.random(),
+					code: token.props.code,
+					inline: true,
+				})];
+			}
+
+			case 'quote': {
+				if (!props.nowrap) {
+					return [h('div', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale))];
+				} else {
+					return [h('span', {
+						style: QUOTE_STYLE,
+					}, genEl(token.children, scale))];
+				}
+			}
+
+			case 'emojiCode': {
+				// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+				if (props.author?.host == null) {
+					return [h(MkCustomEmoji, {
+						key: Math.random(),
+						name: token.props.name,
+						normal: props.plain,
+						host: null,
+						useOriginalSize: scale >= 2.5,
+					})];
+				} else {
 					// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-					if (this.author?.host == null) {
+					if (props.emojiUrls && (props.emojiUrls[token.props.name] == null)) {
+						return [h('span', `:${token.props.name}:`)];
+					} else {
 						return [h(MkCustomEmoji, {
 							key: Math.random(),
 							name: token.props.name,
-							normal: this.plain,
-							host: null,
+							// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
+							url: props.emojiUrls ? props.emojiUrls[token.props.name] : null,
+							normal: props.plain,
+							host: props.author.host,
 							useOriginalSize: scale >= 2.5,
 						})];
-					} else {
-						// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-						if (this.emojiUrls && (this.emojiUrls[token.props.name] == null)) {
-							return [h('span', `:${token.props.name}:`)];
-						} else {
-							return [h(MkCustomEmoji, {
-								key: Math.random(),
-								name: token.props.name,
-								// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
-								url: this.emojiUrls ? this.emojiUrls[token.props.name] : null,
-								normal: this.plain,
-								host: this.author.host,
-								useOriginalSize: scale >= 2.5,
-							})];
-						}
 					}
 				}
-
-				case 'unicodeEmoji': {
-					return [h(MkEmoji, {
-						key: Math.random(),
-						emoji: token.props.emoji,
-					})];
-				}
-
-				case 'mathInline': {
-					return [h('code', token.props.formula)];
-				}
-
-				case 'mathBlock': {
-					return [h('code', token.props.formula)];
-				}
-
-				case 'search': {
-					return [h(MkGoogle, {
-						key: Math.random(),
-						q: token.props.query,
-					})];
-				}
-
-				case 'plain': {
-					return [h('span', genEl(token.children, scale))];
-				}
-
-				default: {
-					// eslint-disable-next-line @typescript-eslint/no-explicit-any
-					console.error('unrecognized ast type:', (token as any).type);
-
-					return [];
-				}
 			}
-		}).flat(Infinity) as (VNode | string)[];
 
-		// Parse ast to DOM
-		return h('span', genEl(ast, this.rootScale));
-	},
-});
+			case 'unicodeEmoji': {
+				return [h(MkEmoji, {
+					key: Math.random(),
+					emoji: token.props.emoji,
+				})];
+			}
+
+			case 'mathInline': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'mathBlock': {
+				return [h('code', token.props.formula)];
+			}
+
+			case 'search': {
+				return [h(MkGoogle, {
+					key: Math.random(),
+					q: token.props.query,
+				})];
+			}
+
+			case 'plain': {
+				return [h('span', genEl(token.children, scale))];
+			}
+
+			default: {
+				// eslint-disable-next-line @typescript-eslint/no-explicit-any
+				console.error('unrecognized ast type:', (token as any).type);
+
+				return [];
+			}
+		}
+	}).flat(Infinity) as (VNode | string)[];
+
+	return h('span', genEl(ast, props.rootScale ?? 1));
+}