diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml
index a073d789a244b9d7a2aec916a2dd5589bb8a4f24..ec767aafa992523eb506aa0e5d10a5ecb7625aa2 100644
--- a/locales/ja-JP.yml
+++ b/locales/ja-JP.yml
@@ -732,6 +732,7 @@ _widgets:
   activity: "アクティビティ"
   photos: "フォト"
   digitalClock: "デジタル時計"
+  federation: "連合"
 
 _cw:
   hide: "隠す"
diff --git a/src/client/widgets/federation.vue b/src/client/widgets/federation.vue
new file mode 100644
index 0000000000000000000000000000000000000000..b99ef1b0aa4709ac47ccd7efe45ab8ae54c33e03
--- /dev/null
+++ b/src/client/widgets/federation.vue
@@ -0,0 +1,111 @@
+<template>
+<mk-container :show-header="props.showHeader">
+	<template #header><fa :icon="faGlobe"/>{{ $t('_widgets.federation') }}</template>
+
+	<div class="wbrkwalb">
+		<mk-loading v-if="fetching"/>
+		<transition-group tag="div" name="chart" class="instances" v-else>
+			<div v-for="instance in instances" :key="instance.id">
+				<div class="instance">
+					<a class="a" :href="'https://' + instance.host" target="_blank" :title="instance.host">#{{ instance.host }}</a>
+					<p>{{ instance.softwareName }} {{ instance.softwareVersion }}</p>
+				</div>
+				<x-chart class="chart" :src="stat.chart"/>
+			</div>
+		</transition-group>
+	</div>
+</mk-container>
+</template>
+
+<script lang="ts">
+import { faGlobe } from '@fortawesome/free-solid-svg-icons';
+import MkContainer from '../components/ui/container.vue';
+import define from './define';
+import XChart from './trends.chart.vue';
+
+export default define({
+	name: 'federation',
+	props: () => ({
+		showHeader: {
+			type: 'boolean',
+			default: true,
+		},
+	})
+}).extend({
+	components: {
+		MkContainer, XChart
+	},
+	data() {
+		return {
+			instances: [],
+			fetching: true,
+			faGlobe
+		};
+	},
+	mounted() {
+		this.fetch();
+		this.clock = setInterval(this.fetch, 1000 * 60);
+	},
+	beforeDestroy() {
+		clearInterval(this.clock);
+	},
+	methods: {
+		fetch() {
+			this.$root.api('federation/instances', {
+				sort: '+lastCommunicatedAt',
+				limit: 5
+			}).then(instances => {
+				this.instances = instances;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="scss" scoped>
+.wbrkwalb {
+	height: (62px + 1px) + (62px + 1px) + (62px + 1px) + (62px + 1px) + 62px;
+	overflow: hidden;
+
+	> .instances {
+		.chart-move {
+			transition: transform 1s ease;
+		}
+
+		> div {
+			display: flex;
+			align-items: center;
+			padding: 14px 16px;
+			border-bottom: solid 1px var(--divider);
+
+			> .instance {
+				flex: 1;
+				overflow: hidden;
+				font-size: 0.9em;
+				color: var(--fg);
+
+				> .a {
+					display: block;
+					width: 100%;
+					white-space: nowrap;
+					overflow: hidden;
+					text-overflow: ellipsis;
+					line-height: 18px;
+				}
+
+				> p {
+					margin: 0;
+					font-size: 75%;
+					opacity: 0.7;
+					line-height: 16px;
+				}
+			}
+
+			> .chart {
+				height: 30px;
+			}
+		}
+	}
+}
+</style>
diff --git a/src/client/widgets/index.ts b/src/client/widgets/index.ts
index 2d27d27e58ab1712294f41e40b5925429c53ebd7..743146193c9a7ab9755f55ae032d2aa73bd24909 100644
--- a/src/client/widgets/index.ts
+++ b/src/client/widgets/index.ts
@@ -11,6 +11,7 @@ Vue.component('mkw-clock', () => import('./clock.vue').then(m => m.default));
 Vue.component('mkw-activity', () => import('./activity.vue').then(m => m.default));
 Vue.component('mkw-photos', () => import('./photos.vue').then(m => m.default));
 Vue.component('mkw-digitalClock', () => import('./digital-clock.vue').then(m => m.default));
+Vue.component('mkw-federation', () => import('./federation.vue').then(m => m.default));
 
 export const widgets = [
 	'memo',
@@ -23,4 +24,5 @@ export const widgets = [
 	'activity',
 	'photos',
 	'digitalClock',
+	'federation',
 ];