diff --git a/client/css/comments.styl b/client/css/comments.styl
index dafdaa9..c1583b7 100644
--- a/client/css/comments.styl
+++ b/client/css/comments.styl
@@ -5,18 +5,15 @@
     margin: 0 0 2em 0
     padding: 0
 
-.comment
-    margin: 0 0 1em 0
-    padding: 0
-    display: -webkit-flex
-    display: flex
 
+
+.comment-form-container
     &:not(.editing)
         .tabs nav
             display: none
         .tabs .edit.tab
             display: none
-        .content
+        .comment-content
             margin-left: 0.5em
     &.editing
         .tab:not(.active)
@@ -25,10 +22,10 @@
             background: $active-tab-background-color
         .tab
             padding: 1em
-            .content-wrapper
+            .comment-content-wrapper
                 background: $window-color
                 overflow: hidden
-                .content
+                .comment-content
                     margin: 1em
             textarea
                 resize: vertical
@@ -36,6 +33,27 @@
                 max-height: 80vh
                 box-sizing: padding-box
 
+    form
+        width: auto
+        margin: 0
+
+    nav
+        vertical-align: middle !important
+        margin: 0 0.3em 0.5em 0 !important
+        &.buttons
+            float: left
+        &.actions
+            float: left
+            margin-top: 0.3em !important
+
+
+
+.comment
+    margin: 0 0 1em 0
+    padding: 0
+    display: -webkit-flex
+    display: flex
+
     .avatar
         margin-right: 1em
         -webkit-flex-shrink: 0
@@ -80,64 +98,55 @@
                 display: inline-block
                 width: 2em
 
-        form
-            width: auto
-            margin: 0
-
-        nav
-            vertical-align: middle
-            margin: 0 0.8em 0.5em 0
-            &.buttons
-                float: left
-            &.actions
-                float: left
-                margin-top: 0.3em
-
         .messages
             margin: 1em 0
 
-    .content
-        ul
-            list-style-position: inside
-            margin: 1em 0
-            padding: 0
 
-        .sjis
-            font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
-            background: #fbfbfb
-            color: #111
-            font-size: 12pt
-            line-height: 1
-            margin: 0
-            padding: 4px
-            overflow: auto
-            white-space: pre
-            word-wrap: normal
 
-        p:first-child
-            margin-top: 0
+.comment-content
+    ul
+        list-style-position: inside
+        margin: 1em 0
+        padding: 0
 
-        .spoiler
-            background: #eee
-            color: #eee
-            &:hover
-                color: dimgray
-            &:before
-                content: '['
-                color: #000
-            &:after
-                content: ']'
-                color: #000
+    .sjis
+        font-family: 'MS PGothic', 'MS Pゴシック', 'IPAMonaPGothic', 'Trebuchet MS', Verdana, Futura, Arial, Helvetica, sans-serif
+        background: #fbfbfb
+        color: #111
+        font-size: 12pt
+        line-height: 1
+        margin: 0
+        padding: 4px
+        overflow: auto
+        white-space: pre
+        word-wrap: normal
+
+    p:first-child
+        margin-top: 0
+
+    .spoiler
+        background: #eee
+        color: #eee
+        &:hover
+            color: dimgray
+        &:before
+            content: '['
+            color: #000
+        &:after
+            content: ']'
+            color: #000
+
+    blockquote
+        border-left: 3px solid #eee
+        margin-left: 0
+        padding: 0.3em 0.3em 0.3em 0.7em
+        background: #fafafa
+        color: #444
+
+    blockquote :last-child
+        margin-bottom: 0
 
-        blockquote
-            border-left: 3px solid #eee
-            margin-left: 0
-            padding: 0.3em 0.3em 0.3em 0.7em
-            background: #fafafa
-            color: #444
 
-        blockquote :last-child
-            margin-bottom: 0
 
 .global-comment-list
     text-align: left
diff --git a/client/html/comment.tpl b/client/html/comment.tpl
index aac6346..32eba69 100644
--- a/client/html/comment.tpl
+++ b/client/html/comment.tpl
@@ -44,32 +44,6 @@
             --><% } %><!--
         --></header>
 
-        <div class='tabs'>
-            <form>
-                <div class='tabs-wrapper'>
-                    <div class='preview tab'>
-                        <div class='content-wrapper'><div class='content'><%= ctx.makeMarkdown(ctx.comment.text) %></div></div>
-                    </div>
-
-                    <div class='edit tab'>
-                        <textarea required minlength=1><%= ctx.comment.text %></textarea>
-                    </div>
-                </div>
-
-                <nav class='buttons'>
-                    <ul>
-                        <li class='preview'><a href='#'>Preview</a></li>
-                        <li class='edit'><a href='#'>Edit</a></li>
-                    </ul>
-                </nav>
-
-                <nav class='actions'>
-                    <input type='submit' class='save' value='Save'/>
-                    <input type='button' class='cancel discourage' value='Cancel'/>
-                </nav>
-            </form>
-
-            <div class='messages'></div>
-        </div>
+        <div class='comment-form-container'></div>
     </div>
 </div>
diff --git a/client/html/comment_form.tpl b/client/html/comment_form.tpl
new file mode 100644
index 0000000..ec35386
--- /dev/null
+++ b/client/html/comment_form.tpl
@@ -0,0 +1,31 @@
+<div class='tabs'>
+    <form>
+        <div class='tabs-wrapper'><!--
+            --><div class='preview tab'><!--
+                --><div class='comment-content-wrapper'><!--
+                    --><div class='comment-content'><!--
+                        --><%= ctx.makeMarkdown(ctx.comment.text) %><!--
+                    --></div><!--
+                --></div><!--
+            --></div><!--
+
+            --><div class='edit tab'><!--
+                --><textarea required minlength=1><%= ctx.comment.text %></textarea><!--
+            --></div><!--
+        --></div>
+
+        <nav class='buttons'>
+            <ul>
+                <li class='preview'><a href='#'>Preview</a></li>
+                <li class='edit'><a href='#'>Edit</a></li>
+            </ul>
+        </nav>
+
+        <nav class='actions'>
+            <input type='submit' class='save' value='Save'/>
+            <input type='button' class='cancel discourage' value='Cancel'/>
+        </nav>
+    </form>
+
+    <div class='messages'></div>
+</div>
diff --git a/client/html/post.tpl b/client/html/post.tpl
index 39bf9d6..d6d3030 100644
--- a/client/html/post.tpl
+++ b/client/html/post.tpl
@@ -46,6 +46,13 @@
     <div class='content'>
         <div class='post-container'></div>
 
-        <div class='comments-container'></div>
+        <% if (ctx.canListComments) { %>
+            <div class='comments-container'></div>
+        <% } %>
+
+        <% if (ctx.canCreateComments) { %>
+            <h2>Add comment</h2>
+            <div class='comment-form-container'></div>
+        <% } %>
     </div>
 </div>
diff --git a/client/js/controllers/posts_controller.js b/client/js/controllers/posts_controller.js
index 2765f9f..b22b7be 100644
--- a/client/js/controllers/posts_controller.js
+++ b/client/js/controllers/posts_controller.js
@@ -74,6 +74,8 @@ class PostsController {
                 nextPostId: aroundResponse.next ? aroundResponse.next.id : null,
                 prevPostId: aroundResponse.prev ? aroundResponse.prev.id : null,
                 canEditPosts: api.hasPrivilege('posts:edit'),
+                canListComments: api.hasPrivilege('comments:list'),
+                canCreateComments: api.hasPrivilege('comments:create'),
             });
         }, response => {
             this._emptyView.render();
diff --git a/client/js/controls/comment_control.js b/client/js/controls/comment_control.js
index 5ca252a..5e283d4 100644
--- a/client/js/controls/comment_control.js
+++ b/client/js/controls/comment_control.js
@@ -1,15 +1,16 @@
 'use strict';
 
 const api = require('../api.js');
-const misc = require('../util/misc.js');
 const views = require('../util/views.js');
+const CommentFormControl = require('../controls/comment_form_control.js');
 
 class CommentControl {
-    constructor(hostNode, comment) {
+    constructor(hostNode, comment, settings) {
         this._hostNode = hostNode;
         this._comment = comment;
         this._template = views.getTemplate('comment');
         this._scoreTemplate = views.getTemplate('score');
+        this._settings = settings;
 
         this.install();
     }
@@ -36,11 +37,6 @@ class CommentControl {
         const deleteButton = sourceNode.querySelector('.delete');
         const upvoteButton = sourceNode.querySelector('.upvote');
         const downvoteButton = sourceNode.querySelector('.downvote');
-        const previewTabButton = sourceNode.querySelector('.buttons .preview');
-        const editTabButton = sourceNode.querySelector('.buttons .edit');
-        const formNode = sourceNode.querySelector('form');
-        const cancelButton = sourceNode.querySelector('.cancel');
-        const textareaNode = sourceNode.querySelector('form textarea');
 
         if (editButton) {
             editButton.addEventListener(
@@ -64,20 +60,22 @@ class CommentControl {
                     e, () => this._comment.ownScore === -1 ? 0 : -1));
         }
 
-        previewTabButton.addEventListener(
-            'click', e => this._evtPreviewClick(e));
-        editTabButton.addEventListener(
-            'click', e => this._evtEditClick(e));
-
-        formNode.addEventListener('submit', e => this._evtSaveClick(e));
-        cancelButton.addEventListener('click', e => this._evtCancelClick(e));
-
-        for (let event of ['cut', 'paste', 'drop', 'keydown']) {
-            textareaNode.addEventListener(event, e => {
-                window.setTimeout(() => this._growTextArea(), 0);
+        this._formControl = new CommentFormControl(
+            sourceNode.querySelector('.comment-form-container'),
+            this._comment,
+            {
+                onSave: text => {
+                    return api.put('/comment/' + this._comment.id, {
+                        text: text,
+                    }).then(response => {
+                        this._comment = response;
+                        this.install();
+                    }, response => {
+                        this._formControl.showError(response.description);
+                    });
+                },
+                canCancel: true
             });
-        }
-        textareaNode.addEventListener('change', e => { this._growTextArea(); });
 
         views.showView(this._hostNode, sourceNode);
     }
@@ -97,6 +95,11 @@ class CommentControl {
             });
     }
 
+    _evtEditClick(e) {
+        e.preventDefault();
+        this._formControl.enterEditMode();
+    }
+
     _evtDeleteClick(e) {
         e.preventDefault();
         if (!window.confirm('Are you sure you want to delete this comment?')) {
@@ -104,82 +107,14 @@ class CommentControl {
         }
         api.delete('/comment/' + this._comment.id)
             .then(response => {
+                if (this._settings.onDelete) {
+                    this._settings.onDelete(this._comment);
+                }
                 this._hostNode.parentNode.removeChild(this._hostNode);
             }, response => {
                 window.alert(response.description);
             });
     }
-
-    _evtSaveClick(e) {
-        e.preventDefault();
-        api.put('/comment/' + this._comment.id, {
-            text: this._hostNode.querySelector('.edit.tab textarea').value,
-        }).then(response => {
-            this._comment = response;
-            this.install();
-        }, response => {
-            this._showError(response.description);
-        });
-    }
-
-    _evtPreviewClick(e) {
-        e.preventDefault();
-        this._hostNode.querySelector('.preview.tab .content').innerHTML
-            = misc.formatMarkdown(
-                this._hostNode.querySelector('.edit.tab textarea').value);
-        this._freezeTabHeights();
-        this._selectTab('preview');
-    }
-
-    _evtEditClick(e) {
-        e.preventDefault();
-        this._freezeTabHeights();
-        this._enterEditMode();
-        this._selectTab('edit');
-        this._growTextArea();
-    }
-
-    _evtCancelClick(e) {
-        e.preventDefault();
-        this._exitEditMode();
-        this._hostNode.querySelector('.edit.tab textarea').value
-            = this._comment.text;
-    }
-
-    _enterEditMode() {
-        this._hostNode.querySelector('.comment').classList.add('editing');
-        misc.enableExitConfirmation();
-    }
-
-    _exitEditMode() {
-        this._hostNode.querySelector('.comment').classList.remove('editing');
-        this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
-        misc.disableExitConfirmation();
-        views.clearMessages(this._hostNode);
-    }
-
-    _selectTab(tabName) {
-        this._freezeTabHeights();
-        for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) {
-            tab.classList.toggle('active', tab.classList.contains(tabName));
-        }
-    }
-
-    _freezeTabHeights() {
-        const tabsNode = this._hostNode.querySelector('.tabs-wrapper');
-        const tabsHeight = tabsNode.getBoundingClientRect().height;
-        tabsNode.style.minHeight = tabsHeight + 'px';
-    }
-
-    _growTextArea() {
-        const previewNode = this._hostNode.querySelector('.content');
-        const textareaNode = this._hostNode.querySelector('textarea');
-        textareaNode.style.height = textareaNode.scrollHeight + 'px';
-    }
-
-    _showError(message) {
-        views.showError(this._hostNode, message);
-    }
 };
 
 module.exports = CommentControl;
diff --git a/client/js/controls/comment_form_control.js b/client/js/controls/comment_form_control.js
new file mode 100644
index 0000000..94e6fdc
--- /dev/null
+++ b/client/js/controls/comment_form_control.js
@@ -0,0 +1,134 @@
+'use strict';
+
+const misc = require('../util/misc.js');
+const views = require('../util/views.js');
+
+class CommentFormControl {
+    constructor(hostNode, comment, settings) {
+        this._hostNode = hostNode;
+        this._comment = comment || {text: ''};
+        this._template = views.getTemplate('comment-form');
+        this._settings = settings;
+        this.install();
+    }
+
+    install() {
+        const sourceNode = this._template({
+            comment: this._comment,
+        });
+
+        const previewTabButton = sourceNode.querySelector('.buttons .preview');
+        const editTabButton = sourceNode.querySelector('.buttons .edit');
+        const formNode = sourceNode.querySelector('form');
+        const cancelButton = sourceNode.querySelector('.cancel');
+        const textareaNode = sourceNode.querySelector('form textarea');
+
+        previewTabButton.addEventListener(
+            'click', e => this._evtPreviewClick(e));
+        editTabButton.addEventListener(
+            'click', e => this._evtEditClick(e));
+
+        formNode.addEventListener('submit', e => this._evtSaveClick(e));
+
+        if (this._settings.canCancel) {
+            cancelButton
+                .addEventListener('click', e => this._evtCancelClick(e));
+        } else {
+            cancelButton.style.display = 'none';
+        }
+
+        for (let event of ['cut', 'paste', 'drop', 'keydown']) {
+            textareaNode.addEventListener(event, e => {
+                window.setTimeout(() => this._growTextArea(), 0);
+            });
+        }
+        textareaNode.addEventListener('change', e => {
+            misc.enableExitConfirmation();
+            this._growTextArea();
+        });
+
+        views.showView(this._hostNode, sourceNode);
+    }
+
+    enterEditMode() {
+        this._freezeTabHeights();
+        this._hostNode.classList.add('editing');
+        this._selectTab('edit');
+        this._growTextArea();
+    }
+
+    exitEditMode() {
+        this._hostNode.classList.remove('editing');
+        this._hostNode.querySelector('.tabs-wrapper').style.minHeight = null;
+        misc.disableExitConfirmation();
+        views.clearMessages(this._hostNode);
+        this._hostNode.querySelector('.edit.tab textarea').value
+            = this._comment.text;
+    }
+
+    get _textareaNode() {
+        return this._hostNode.querySelector('.edit.tab textarea');
+    }
+
+    get _contentNode() {
+        return this._hostNode.querySelector('.preview.tab .comment-content');
+    }
+
+    setText(text) {
+        this._textareaNode.value = text;
+        this._contentNode.innerHTML = misc.formatMarkdown(text);
+    }
+
+    showError(message) {
+        views.showError(this._hostNode, message);
+    }
+
+    _evtPreviewClick(e) {
+        e.preventDefault();
+        this._contentNode.innerHTML
+            = misc.formatMarkdown(this._textareaNode.value);
+        this._freezeTabHeights();
+        this._selectTab('preview');
+    }
+
+    _evtEditClick(e) {
+        e.preventDefault();
+        this.enterEditMode();
+    }
+
+    _evtSaveClick(e) {
+        e.preventDefault();
+        if (!this._settings.onSave) {
+            throw 'No save handler';
+        }
+        this._settings.onSave(this._textareaNode.value)
+            .then(() => { misc.disableExitConfirmation(); });
+    }
+
+    _evtCancelClick(e) {
+        e.preventDefault();
+        this.exitEditMode();
+    }
+
+    _selectTab(tabName) {
+        this._freezeTabHeights();
+        for (let tab of this._hostNode.querySelectorAll('.tab, .buttons li')) {
+            tab.classList.toggle('active', tab.classList.contains(tabName));
+        }
+    }
+
+    _freezeTabHeights() {
+        const tabsNode = this._hostNode.querySelector('.tabs-wrapper');
+        const tabsHeight = tabsNode.getBoundingClientRect().height;
+        tabsNode.style.minHeight = tabsHeight + 'px';
+    }
+
+    _growTextArea() {
+        this._textareaNode.style.height
+            = Math.max(
+                this._settings.minHeight || 0,
+                this._textareaNode.scrollHeight) + 'px';
+    }
+};
+
+module.exports = CommentFormControl;
diff --git a/client/js/controls/comment_list_control.js b/client/js/controls/comment_list_control.js
index d78a6ad..91b13f8 100644
--- a/client/js/controls/comment_list_control.js
+++ b/client/js/controls/comment_list_control.js
@@ -31,7 +31,15 @@ class CommentListControl {
         const commentList = new DocumentFragment();
         for (let comment of this._comments) {
             const commentListItemNode = document.createElement('li');
-            new CommentControl(commentListItemNode, comment);
+            new CommentControl(commentListItemNode, comment, {
+                onDelete: removedComment => {
+                    for (let [index, comment] of this._comments.entries()) {
+                        if (comment.id === removedComment.id) {
+                            this._comments.splice(index, 1);
+                        }
+                    }
+                },
+            });
             commentList.appendChild(commentListItemNode);
         }
         views.showView(this._hostNode.querySelector('ul'), commentList);
diff --git a/client/js/views/post_view.js b/client/js/views/post_view.js
index 0c7b541..0968682 100644
--- a/client/js/views/post_view.js
+++ b/client/js/views/post_view.js
@@ -1,5 +1,6 @@
 'use strict';
 
+const api = require('../api.js');
 const views = require('../util/views.js');
 const keyboard = require('../util/keyboard.js');
 const page = require('page');
@@ -11,6 +12,7 @@ const PostReadonlySidebarControl
 const PostEditSidebarControl
     = require('../controls/post_edit_sidebar_control.js');
 const CommentListControl = require('../controls/comment_list_control.js');
+const CommentFormControl = require('../controls/comment_form_control.js');
 
 class PostView {
     constructor() {
@@ -52,21 +54,9 @@ class PostView {
             postContainerNode.querySelector('.post-overlay'),
             ctx.post);
 
-        if (ctx.editMode) {
-            new PostEditSidebarControl(
-                postViewNode.querySelector('.sidebar-container'),
-                ctx.post,
-                this._postContentControl);
-        } else {
-            new PostReadonlySidebarControl(
-                postViewNode.querySelector('.sidebar-container'),
-                ctx.post,
-                this._postContentControl);
-        }
-
-        new CommentListControl(
-            postViewNode.querySelector('.comments-container'),
-            ctx.post.comments);
+        this._installSidebar(ctx);
+        this._installCommentForm(ctx);
+        this._installComments(ctx);
 
         keyboard.bind('e', () => {
             if (ctx.editMode) {
@@ -86,6 +76,56 @@ class PostView {
             }
         });
     }
+
+    _installSidebar(ctx) {
+        const sidebarContainerNode = document.querySelector(
+            '#content-holder .sidebar-container');
+
+        if (ctx.editMode) {
+            new PostEditSidebarControl(
+                sidebarContainerNode, ctx.post, this._postContentControl);
+        } else {
+            new PostReadonlySidebarControl(
+                sidebarContainerNode, ctx.post, this._postContentControl);
+        }
+    }
+
+    _installCommentForm(ctx) {
+        const commentFormContainer = document.querySelector(
+            '#content-holder .comment-form-container');
+        if (!commentFormContainer) {
+            return;
+        }
+
+        this._formControl = new CommentFormControl(
+            commentFormContainer,
+            null,
+            {
+                onSave: text => {
+                    return api.post('/comments', {
+                        postId: ctx.post.id,
+                        text: text,
+                    }).then(response => {
+                        ctx.post.comments.push(response);
+                        this._formControl.setText('');
+                        this._installComments(ctx);
+                    }, response => {
+                        this._formControl.showError(response.description);
+                    });
+                },
+                canCancel: false,
+                minHeight: 150,
+            });
+        this._formControl.enterEditMode();
+    }
+
+    _installComments(ctx) {
+        const commentsContainerNode = document.querySelector(
+            '#content-holder .comments-container');
+        if (commentsContainerNode) {
+            new CommentListControl(commentsContainerNode, ctx.post.comments);
+        }
+    }
 }
 
 module.exports = PostView;