diff --git a/src/client/app/animation.styl b/src/client/app/animation.styl
index 9cbd3ec6c8..9a0ec2729c 100644
--- a/src/client/app/animation.styl
+++ b/src/client/app/animation.styl
@@ -31,3 +31,11 @@
 	0% { transform: rotate(0deg); }
 	100% { transform: rotate(360deg); }
 }
+
+@keyframes jump {
+	0% { transform: translateY(0); }
+	25% { transform: translateY(-16px); }
+	50% { transform: translateY(0); }
+	75% { transform: translateY(-8px); }
+	100% { transform: translateY(0); }
+}
diff --git a/src/client/app/common/views/components/mfm.ts b/src/client/app/common/views/components/mfm.ts
index 199d6bb978..542f1e34c5 100644
--- a/src/client/app/common/views/components/mfm.ts
+++ b/src/client/app/common/views/components/mfm.ts
@@ -135,6 +135,17 @@ export default Vue.component('misskey-flavored-markdown', {
 					}, genEl(token.children));
 				}
 
+				case 'jump': {
+					motionCount++;
+					const isLong = sumTextsLength(token.children) > 5 || countNodesF(token.children) > 3;
+					const isMany = motionCount > 3;
+					return (createElement as any)('span', {
+						attrs: {
+							style: (this.$store.state.settings.disableAnimatedMfm || isLong || isMany) ? 'display: inline-block;' : 'display: inline-block; animation: jump 0.75s linear infinite;'
+						},
+					}, genEl(token.children));
+				}
+
 				case 'flip': {
 					return (createElement as any)('span', {
 						attrs: {
diff --git a/src/mfm/html.ts b/src/mfm/html.ts
index acd6891aba..6dba6defe3 100644
--- a/src/mfm/html.ts
+++ b/src/mfm/html.ts
@@ -61,6 +61,12 @@ export default (tokens: MfmForest, mentionedRemoteUsers: INote['mentionedRemoteU
 			return el;
 		},
 
+		jump(token) {
+			const el = doc.createElement('i');
+			appendChildren(token.children, el);
+			return el;
+		},
+
 		flip(token) {
 			const el = doc.createElement('span');
 			appendChildren(token.children, el);
diff --git a/src/mfm/parser.ts b/src/mfm/parser.ts
index 1d72496a67..6b7c3c5845 100644
--- a/src/mfm/parser.ts
+++ b/src/mfm/parser.ts
@@ -92,6 +92,7 @@ const mfm = P.createLanguage({
 		r.big,
 		r.small,
 		r.spin,
+		r.jump,
 		r.bold,
 		r.strike,
 		r.italic,
@@ -126,6 +127,7 @@ const mfm = P.createLanguage({
 			r.emoji,
 			r.mathInline,
 			r.spin,
+			r.jump,
 			r.text
 		).atLeast(1).tryParse(x), {})),
 	//#endregion
@@ -154,6 +156,15 @@ const mfm = P.createLanguage({
 		).atLeast(1).tryParse(x), {})),
 	//#endregion
 
+	//#region Jump
+	jump: r =>
+		P.regexp(/<jump>(.+?)<\/jump>/, 1)
+		.map(x => createTree('jump', P.alt(
+			r.emoji,
+			r.text
+		).atLeast(1).tryParse(x), {})),
+	//#endregion
+
 	//#region Block code
 	blockCode: r =>
 		newline.then(
@@ -189,6 +200,7 @@ const mfm = P.createLanguage({
 			r.big,
 			r.small,
 			r.spin,
+			r.jump,
 			r.bold,
 			r.strike,
 			r.italic,
@@ -240,6 +252,7 @@ const mfm = P.createLanguage({
 			r.big,
 			r.small,
 			r.spin,
+			r.jump,
 			r.bold,
 			r.strike,
 			r.link,
@@ -297,6 +310,7 @@ const mfm = P.createLanguage({
 				r.big,
 				r.small,
 				r.spin,
+				r.jump,
 				r.bold,
 				r.strike,
 				r.italic,
@@ -347,6 +361,7 @@ const mfm = P.createLanguage({
 			r.bold,
 			r.small,
 			r.spin,
+			r.jump,
 			r.strike,
 			r.italic,
 			r.mention,
@@ -410,6 +425,7 @@ const mfm = P.createLanguage({
 				r.big,
 				r.small,
 				r.spin,
+				r.jump,
 				r.bold,
 				r.strike,
 				r.italic,
diff --git a/test/mfm.ts b/test/mfm.ts
index 7070329f31..0d07add0ee 100644
--- a/test/mfm.ts
+++ b/test/mfm.ts
@@ -262,6 +262,15 @@ describe('MFM', () => {
 			]);
 		});
 
+		it('jump', () => {
+			const tokens = analyze('<jump>:foo:</jump>');
+			assert.deepStrictEqual(tokens, [
+				tree('jump', [
+					leaf('emoji', { name: 'foo' })
+				], {}),
+			]);
+		});
+
 		describe('motion', () => {
 			it('by triple brackets', () => {
 				const tokens = analyze('(((foo)))');