diff --git a/docs/rules/html-indent.md b/docs/rules/html-indent.md
new file mode 100644
index 000000000..9786c3b13
--- /dev/null
+++ b/docs/rules/html-indent.md
@@ -0,0 +1,41 @@
+# Enforce consistent indentation in html template (html-indent)
+
+Please describe the origin of the rule here.
+
+## :book: Rule Details
+
+This rule aims to...
+
+Examples of **incorrect** code for this rule:
+
+```html
+
+
+
+
+```
+
+Examples of **correct** code for this rule:
+
+```html
+
+
+
+
+```
+
+## :wrench: Options
+
+This rule has a mixed option:
+
+For example, for 2-space indentation:
+
+```
+vue/html-indent: [2, 2]
+```
+
+Or for tabbed indentation:
+
+```
+vue/html-indent: [2, 'tab']
+```
diff --git a/lib/rules/html-indent.js b/lib/rules/html-indent.js
new file mode 100644
index 000000000..265edb59a
--- /dev/null
+++ b/lib/rules/html-indent.js
@@ -0,0 +1,133 @@
+/**
+ * @fileoverview Enforce consistent indentation in html template
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const utils = require('../utils')
+
+const REGEXP_VTEXT = /([^\r\n]*)([\r\n]*)([\s\t]*)$/g // Get last text + caret + whitespaces
+
+// ------------------------------------------------------------------------------
+// Rule Definition
+// ------------------------------------------------------------------------------
+
+function create (context) {
+ const sourceCode = context.getSourceCode()
+ const options = context.options[0] || 2
+ const indentCount = options === 'tab' ? 1 : options
+ const indentType = options === 'tab' ? 'tab' : 'space'
+ const tagIndent = options === 'tab' ? '\t' : ' '.repeat(options)
+ const attrIndent = tagIndent // TODO: add way to configure this
+ let currentIndent = -1 // Start at -1 for root
+
+ // ----------------------------------------------------------------------
+ // Helpers
+ // ----------------------------------------------------------------------
+
+ function createErrorMessage (expectedAmount, actualSpaces, actualTabs) {
+ const expectedStatement = `${expectedAmount} ${indentType}${expectedAmount === 1 ? '' : 's'}` // e.g. "2 tabs"
+ const foundSpacesWord = `space${actualSpaces === 1 ? '' : 's'}` // e.g. "space"
+ const foundTabsWord = `tab${actualTabs === 1 ? '' : 's'}` // e.g. "tabs"
+ let foundStatement
+
+ if (actualSpaces > 0 && actualTabs > 0) {
+ foundStatement = `${actualSpaces} ${foundSpacesWord} and ${actualTabs} ${foundTabsWord}` // e.g. "1 space and 2 tabs"
+ } else if (actualSpaces > 0) {
+ foundStatement = indentType === 'space' ? actualSpaces : `${actualSpaces} ${foundSpacesWord}`
+ } else if (actualTabs > 0) {
+ foundStatement = indentType === 'tab' ? actualTabs : `${actualTabs} ${foundTabsWord}`
+ } else {
+ foundStatement = '0'
+ }
+
+ return `Expected indentation of ${expectedStatement} but found ${foundStatement}.`
+ }
+
+ function getNodeIndent (node) {
+ const prevToken = context.getTokenBefore(node) // TODO: fix
+ if (!prevToken) {
+ return
+ }
+ const prevNode = sourceCode.getNodeByRangeIndex(prevToken.range[0])
+ if (prevNode && prevNode.type === 'VText') {
+ const match = REGEXP_VTEXT.exec(prevNode.value)
+ const indentChars = (match[3] || '').split('')
+ return {
+ node: prevNode,
+ text: match[1],
+ caret: match[2],
+ ws: match[3],
+ spaces: indentChars.filter(char => char === ' ').length,
+ tabs: indentChars.filter(char => char === '\t').length
+ }
+ }
+ return null
+ }
+
+ function getDesireIndent (attribute) {
+ return tagIndent.repeat(currentIndent) + (attribute ? attrIndent : '')
+ }
+
+ // ----------------------------------------------------------------------
+ // Public
+ // ----------------------------------------------------------------------
+
+ utils.registerTemplateBodyVisitor(context, {
+ VStartTag (node) {
+ ++currentIndent
+
+ const info = getNodeIndent(node.parent)
+ if (info) {
+ // TODO: add caret check
+ if (info.ws !== getDesireIndent()) {
+ context.report({
+ node,
+ loc: node.loc,
+ message: createErrorMessage(indentCount * currentIndent, info.spaces, info.tabs)
+ // fixable: () => info.node ..... replace(info.text + info.caret + info.ws)
+ })
+ }
+ }
+
+ if (!node.endTag) {
+ --currentIndent
+ }
+ },
+ VEndTag (node) {
+ --currentIndent
+ }
+ })
+
+ return {}
+}
+
+module.exports = {
+ meta: {
+ docs: {
+ description: 'Enforce consistent indentation in html template',
+ category: 'Stylistic Issues',
+ recommended: false
+ },
+ fixable: null, // or "code" or "whitespace"
+ schema: [
+ {
+ oneOf: [
+ {
+ enum: ['tab']
+ },
+ {
+ type: 'integer',
+ minimum: 0
+ }
+ ]
+ }
+ ]
+ },
+
+ create
+}
diff --git a/tests/lib/rules/html-indent.js b/tests/lib/rules/html-indent.js
new file mode 100644
index 000000000..a320e21ad
--- /dev/null
+++ b/tests/lib/rules/html-indent.js
@@ -0,0 +1,98 @@
+/**
+ * @fileoverview Enforce consistent indentation in html template
+ * @author Armano
+ */
+'use strict'
+
+// ------------------------------------------------------------------------------
+// Requirements
+// ------------------------------------------------------------------------------
+
+const rule = require('../../../lib/rules/html-indent')
+const RuleTester = require('eslint').RuleTester
+
+// ------------------------------------------------------------------------------
+// Tests
+// ------------------------------------------------------------------------------
+
+const ruleTester = new RuleTester({
+ parser: 'vue-eslint-parser',
+ parserOptions: { ecmaVersion: 2015 }
+})
+ruleTester.run('html-indent', rule, {
+
+ valid: [
+ {
+ filename: 'test.vue',
+ code: '\n\n',
+ options: [0]
+ },
+ {
+ filename: 'test.vue',
+ code: '\n \n'
+ },
+ {
+ filename: 'test.vue',
+ code: '\n \n',
+ options: [2]
+ },
+ {
+ filename: 'test.vue',
+ code: '\n\t\n',
+ options: ['tab']
+ }
+ ],
+
+ invalid: [
+ {
+ filename: 'test.vue',
+ code: '\n\n',
+ errors: [{
+ message: 'Tag must be .',
+ type: 'VStartTag'
+ }]
+ }
+ // {
+ // filename: 'test.vue',
+ // code: '',
+ // errors: [{
+ // message: 'Element has to be in new line.',
+ // type: 'Me too'
+ // }]
+ // },
+ // {
+ // filename: 'test.vue',
+ // code: '\n',
+ // options: [2],
+ // errors: [{
+ // message: 'Element has to be in new line.',
+ // type: 'Me too'
+ // }]
+ // },
+ // {
+ // filename: 'test.vue',
+ // code: '\n\t',
+ // options: [2],
+ // errors: [{
+ // message: 'Element has to be in new line.',
+ // type: 'Me too'
+ // }]
+ // },
+ // {
+ // filename: 'test.vue',
+ // code: '\n \n',
+ // options: ['tab'],
+ // errors: [{
+ // message: 'Element has to be in new line.',
+ // type: 'Me too'
+ // }]
+ // },
+ // {
+ // code: '\n ',
+ // errors: [{
+ // message: 'Element has to be in new line.',
+ // type: 'Me too'
+ // }]
+ // }
+ ]
+})