diff --git a/.eslintrc b/.eslintrc
index 7a74d6ef9b..0943cb4b64 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -14,6 +14,7 @@
 		"vue/no-unused-vars": false,
 		"vue/attributes-order": false,
 		"vue/require-prop-types": false,
+		"vue/require-default-prop": false,
 		"no-console": 0,
 		"no-unused-vars": 0,
 		"no-empty": 0
diff --git a/src/client/app/desktop/views/components/list-timeline.vue b/src/client/app/desktop/views/components/list-timeline.vue
new file mode 100644
index 0000000000..61300f6f8f
--- /dev/null
+++ b/src/client/app/desktop/views/components/list-timeline.vue
@@ -0,0 +1,75 @@
+<template>
+	<mk-notes ref="timeline" :more="more"/>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+const fetchLimit = 10;
+
+export default Vue.extend({
+	props: ['list'],
+	data() {
+		return {
+			fetching: true,
+			moreFetching: false,
+			existMore: false,
+			connection: null
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	mounted() {
+		this.connection = new UserListStream((this as any).os, (this as any).os.i, this.list.id);
+		this.connection.on('note', this.onNote);
+		this.connection.on('userAdded', this.onUserAdded);
+		this.connection.on('userRemoved', this.onUserRemoved);
+
+		this.fetch();
+	},
+	beforeDestroy() {
+		this.connection.off('note', this.onNote);
+		this.connection.off('userAdded', this.onUserAdded);
+		this.connection.off('userRemoved', this.onUserRemoved);
+		this.connection.close();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+
+			(this as any).api('notes/list-timeline', {
+				limit: fetchLimit + 1,
+				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+			}).then(notes => {
+				if (notes.length == fetchLimit + 1) {
+					notes.pop();
+					this.existMore = true;
+				}
+				(this.$refs.timeline as any).init(notes);
+				this.fetching = false;
+				this.$emit('loaded');
+			});
+		},
+		more() {
+			this.moreFetching = true;
+
+			(this as any).api('notes/list-timeline', {
+				limit: fetchLimit + 1,
+				untilId: (this.$refs.timeline as any).tail().id,
+				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
+				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
+			}).then(notes => {
+				if (notes.length == fetchLimit + 1) {
+					notes.pop();
+				} else {
+					this.existMore = false;
+				}
+				notes.forEach(n => (this.$refs.timeline as any).append(n));
+				this.moreFetching = false;
+			});
+		}
+	}
+});
+</script>
diff --git a/src/client/app/desktop/views/components/lists-window.vue b/src/client/app/desktop/views/components/lists-window.vue
index 7d6a5def2c..7097e5ed4b 100644
--- a/src/client/app/desktop/views/components/lists-window.vue
+++ b/src/client/app/desktop/views/components/lists-window.vue
@@ -2,10 +2,8 @@
 <mk-window ref="window" is-modal width="500px" height="550px" @closed="$destroy">
 	<span slot="header" :class="$style.header">%fa:list% リスト</span>
 
-	<button class="ui">リストを作成</button>
-	<a v-for="list in lists" :key="list.id">
-
-	</a>
+	<button class="ui" @click="add">リストを作成</button>
+	<router-link v-for="list in lists" :key="list.id" :to="`/i/lists/${list.id}`">{{ list.title }}</router-link>
 </mk-window>
 </template>
 
@@ -25,6 +23,17 @@ export default Vue.extend({
 		});
 	},
 	methods: {
+		add() {
+			(this as any).apis.input({
+				title: 'リスト名',
+			}).then(async title => {
+				const list = await (this as any).api('users/lists/create', {
+					title
+				});
+
+				this.$router.push(`i/lists/${ list.id }`);
+			});
+		},
 		close() {
 			(this as any).$refs.window.close();
 		}
diff --git a/src/client/app/desktop/views/components/notes.vue b/src/client/app/desktop/views/components/notes.vue
index 1a33a4240b..2822cb8c0f 100644
--- a/src/client/app/desktop/views/components/notes.vue
+++ b/src/client/app/desktop/views/components/notes.vue
@@ -9,8 +9,11 @@
 			</p>
 		</template>
 	</transition-group>
-	<footer>
-		<slot name="footer"></slot>
+	<footer v-if="loadMore">
+		<button @click="loadMore" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
+			<template v-if="!moreFetching">%i18n:@load-more%</template>
+			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
+		</button>
 	</footer>
 </div>
 </template>
@@ -19,16 +22,29 @@
 import Vue from 'vue';
 import XNote from './notes.note.vue';
 
+const displayLimit = 30;
+
 export default Vue.extend({
 	components: {
 		XNote
 	},
+
 	props: {
-		notes: {
-			type: Array,
-			default: () => []
+		more: {
+			type: Function,
+			required: false
 		}
 	},
+
+	data() {
+		return {
+			notes: [],
+			queue: [],
+			fetching: false,
+			moreFetching: false
+		};
+	},
+
 	computed: {
 		_notes(): any[] {
 			return (this.notes as any).map(note => {
@@ -40,12 +56,74 @@ export default Vue.extend({
 			});
 		}
 	},
+
+	mounted() {
+		window.addEventListener('scroll', this.onScroll);
+	},
+
+	beforeDestroy() {
+		window.removeEventListener('scroll', this.onScroll);
+	},
+
 	methods: {
+		isScrollTop() {
+			return window.scrollY <= 8;
+		},
+
 		focus() {
 			(this.$el as any).children[0].focus();
 		},
+
 		onNoteUpdated(i, note) {
 			Vue.set((this as any).notes, i, note);
+		},
+
+		init(notes) {
+			this.queue = [];
+			this.notes = notes;
+		},
+
+		prepend(note) {
+			if (this.isScrollTop()) {
+				this.notes.unshift(note);
+
+				// オーバーフローしたら古い投稿は捨てる
+				if (this.notes.length >= displayLimit) {
+					this.notes = this.notes.slice(0, displayLimit);
+				}
+			} else {
+				this.queue.unshift(note);
+			}
+		},
+
+		append(note) {
+			this.notes.push(note);
+		},
+
+		tail() {
+			return this.notes[this.notes.length - 1];
+		},
+
+		releaseQueue() {
+			this.queue.forEach(n => this.prepend(n));
+			this.queue = [];
+		},
+
+		async loadMore() {
+			this.moreFetching = true;
+			await this.more();
+			this.moreFetching = false;
+		},
+
+		onScroll() {
+			if (this.isScrollTop()) {
+				this.releaseQueue();
+			}
+
+			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
+				const current = window.scrollY + window.innerHeight;
+				if (current > document.body.offsetHeight - 8) this.loadMore();
+			}
 		}
 	}
 });
diff --git a/src/client/app/desktop/views/components/timeline.core.vue b/src/client/app/desktop/views/components/timeline.core.vue
index 719425c3c7..11c7adf158 100644
--- a/src/client/app/desktop/views/components/timeline.core.vue
+++ b/src/client/app/desktop/views/components/timeline.core.vue
@@ -1,5 +1,5 @@
 <template>
-<div class="mk-home-timeline">
+<div class="mk-timeline-core">
 	<div class="newer-indicator" :style="{ top: $store.state.uiHeaderHeight + 'px' }" v-show="queue.length > 0"></div>
 	<mk-friends-maker v-if="src == 'home' && alone"/>
 	<div class="fetching" v-if="fetching">
@@ -8,12 +8,7 @@
 	<p class="empty" v-if="notes.length == 0 && !fetching">
 		%fa:R comments%%i18n:@empty%
 	</p>
-	<mk-notes :notes="notes" ref="timeline">
-		<button slot="footer" @click="more" :disabled="moreFetching" :style="{ cursor: moreFetching ? 'wait' : 'pointer' }">
-			<template v-if="!moreFetching">%i18n:@load-more%</template>
-			<template v-if="moreFetching">%fa:spinner .pulse .fw%</template>
-		</button>
-	</mk-notes>
+	<mk-notes :notes="notes" ref="timeline" :more="canFetchMore ? more : null"/>
 </div>
 </template>
 
@@ -22,7 +17,6 @@ import Vue from 'vue';
 import { url } from '../../../config';
 
 const fetchLimit = 10;
-const displayLimit = 30;
 
 export default Vue.extend({
 	props: {
@@ -37,8 +31,6 @@ export default Vue.extend({
 			fetching: true,
 			moreFetching: false,
 			existMore: false,
-			notes: [],
-			queue: [],
 			connection: null,
 			connectionId: null,
 			date: null
@@ -67,7 +59,7 @@ export default Vue.extend({
 		},
 
 		canFetchMore(): boolean {
-			return !this.moreFetching && !this.fetching && this.notes.length > 0 && this.existMore;
+			return !this.moreFetching && !this.fetching && this.existMore;
 		}
 	},
 
@@ -82,7 +74,6 @@ export default Vue.extend({
 		}
 
 		document.addEventListener('keydown', this.onKeydown);
-		window.addEventListener('scroll', this.onScroll);
 
 		this.fetch();
 	},
@@ -96,7 +87,6 @@ export default Vue.extend({
 		this.stream.dispose(this.connectionId);
 
 		document.removeEventListener('keydown', this.onKeydown);
-		window.removeEventListener('scroll', this.onScroll);
 	},
 
 	methods: {
@@ -105,7 +95,6 @@ export default Vue.extend({
 		},
 
 		fetch(cb?) {
-			this.queue = [];
 			this.fetching = true;
 
 			(this as any).api(this.endpoint, {
@@ -118,7 +107,7 @@ export default Vue.extend({
 					notes.pop();
 					this.existMore = true;
 				}
-				this.notes = notes;
+				(this.$refs.timeline as any).init(notes);
 				this.fetching = false;
 				this.$emit('loaded');
 				if (cb) cb();
@@ -132,7 +121,7 @@ export default Vue.extend({
 
 			(this as any).api(this.endpoint, {
 				limit: fetchLimit + 1,
-				untilId: this.notes[this.notes.length - 1].id,
+				untilId: (this.$refs.timeline as any).tail().id,
 				includeMyRenotes: (this as any).os.i.clientSettings.showMyRenotes,
 				includeRenotedMyNotes: (this as any).os.i.clientSettings.showRenotedMyNotes
 			}).then(notes => {
@@ -141,33 +130,11 @@ export default Vue.extend({
 				} else {
 					this.existMore = false;
 				}
-				this.notes = this.notes.concat(notes);
+				notes.forEach(n => (this.$refs.timeline as any).append(n));
 				this.moreFetching = false;
 			});
 		},
 
-		prependNote(note, silent = false) {
-			// サウンドを再生する
-			if ((this as any).os.isEnableSounds && !silent) {
-				const sound = new Audio(`${url}/assets/post.mp3`);
-				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
-				sound.play();
-			}
-
-			// Prepent a note
-			this.notes.unshift(note);
-
-			// オーバーフローしたら古い投稿は捨てる
-			if (this.notes.length >= displayLimit) {
-				this.notes = this.notes.slice(0, displayLimit);
-			}
-		},
-
-		releaseQueue() {
-			this.queue.forEach(n => this.prependNote(n, true));
-			this.queue = [];
-		},
-
 		onNote(note) {
 			//#region 弾く
 			const isMyNote = note.userId == (this as any).os.i.id;
@@ -186,11 +153,15 @@ export default Vue.extend({
 			}
 			//#endregion
 
-			if (this.isScrollTop()) {
-				this.prependNote(note);
-			} else {
-				this.queue.unshift(note);
+			// サウンドを再生する
+			if ((this as any).os.isEnableSounds) {
+				const sound = new Audio(`${url}/assets/post.mp3`);
+				sound.volume = localStorage.getItem('soundVolume') ? parseInt(localStorage.getItem('soundVolume'), 10) / 100 : 0.5;
+				sound.play();
 			}
+
+			// Prepend a note
+			(this.$refs.timeline as any).prepend(note);
 		},
 
 		onChangeFollowing() {
@@ -206,17 +177,6 @@ export default Vue.extend({
 			this.fetch();
 		},
 
-		onScroll() {
-			if ((this as any).os.i.clientSettings.fetchOnScroll !== false) {
-				const current = window.scrollY + window.innerHeight;
-				if (current > document.body.offsetHeight - 8) this.more();
-			}
-
-			if (this.isScrollTop()) {
-				this.releaseQueue();
-			}
-		},
-
 		onKeydown(e) {
 			if (e.target.tagName != 'INPUT' && e.target.tagName != 'TEXTAREA') {
 				if (e.which == 84) { // t
@@ -231,7 +191,7 @@ export default Vue.extend({
 <style lang="stylus" scoped>
 @import '~const.styl'
 
-.mk-home-timeline
+.mk-timeline-core
 	> .newer-indicator
 		position -webkit-sticky
 		position sticky
diff --git a/src/client/app/desktop/views/components/ui.header.account.vue b/src/client/app/desktop/views/components/ui.header.account.vue
index 61197d4017..93ee5aa195 100644
--- a/src/client/app/desktop/views/components/ui.header.account.vue
+++ b/src/client/app/desktop/views/components/ui.header.account.vue
@@ -45,6 +45,7 @@
 
 <script lang="ts">
 import Vue from 'vue';
+import MkListsWindow from './lists-window.vue';
 import MkSettingsWindow from './settings-window.vue';
 import MkDriveWindow from './drive-window.vue';
 import contains from '../../../common/scripts/contains';
@@ -83,6 +84,10 @@ export default Vue.extend({
 			this.close();
 			(this as any).os.new(MkDriveWindow);
 		},
+		list() {
+			this.close();
+			(this as any).os.new(MkListsWindow);
+		},
 		settings() {
 			this.close();
 			(this as any).os.new(MkSettingsWindow);
diff --git a/src/client/app/desktop/views/pages/list.vue b/src/client/app/desktop/views/pages/list.vue
new file mode 100644
index 0000000000..70130eae68
--- /dev/null
+++ b/src/client/app/desktop/views/pages/list.vue
@@ -0,0 +1,72 @@
+<template>
+<mk-ui>
+	<header :class="$style.header">
+		<h1>{{ list.title }}</h1>
+	</header>
+	<mk-list-timeline :list="list"/>
+</mk-ui>
+</template>
+
+<script lang="ts">
+import Vue from 'vue';
+
+export default Vue.extend({
+	data() {
+		return {
+			fetching: true,
+			list: null
+		};
+	},
+	watch: {
+		$route: 'fetch'
+	},
+	mounted() {
+		this.fetch();
+	},
+	methods: {
+		fetch() {
+			this.fetching = true;
+
+			(this as any).api('users/lists/show', {
+				id: this.$route.params.list
+			}).then(list => {
+				this.list = list;
+				this.fetching = false;
+			});
+		}
+	}
+});
+</script>
+
+<style lang="stylus" module>
+.header
+	width 100%
+	max-width 600px
+	margin 0 auto
+	color #555
+
+.notes
+	max-width 600px
+	margin 0 auto
+	border solid 1px rgba(0, 0, 0, 0.075)
+	border-radius 6px
+	overflow hidden
+
+.loading
+	padding 64px 0
+
+.empty
+	display block
+	margin 0 auto
+	padding 32px
+	max-width 400px
+	text-align center
+	color #999
+
+	> [data-fa]
+		display block
+		margin-bottom 16px
+		font-size 3em
+		color #ccc
+
+</style>