Repository: IduxFE/components
Pull request #74 (2020-12-26 16:04:12)

feat(comp:button): add button style
## PR Checklist

Please check if your PR fulfills the following requirements:

- [x] The commit message follows [our guidelines](https://github.com/IduxFE/components/blob/main/docs/contributing.zh.md#commit)
- [x] Tests for the changes have been added/updated or not needed
- [x] Docs and demo have been added/updated or not needed

## PR Type

What kind of change does this PR introduce?

- [ ] Bugfix
- [ ] Feature
- [x] Component style update
- [ ] Code style update (formatting, local variables)
- [ ] Refactoring (no functional changes, no api changes)
- [ ] Build related changes
- [ ] CI related changes
- [ ] Documentation content changes
- [ ] Application (the showcase website) / infrastructure changes
- [ ] Other... Please describe:

## What is the current behavior?
<!-- Please describe the current behavior that you are modifying, or link to a relevant issue. -->

## What is the new behavior?

## Other information


codecov[bot]: # [Codecov](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=h1) Report
> Merging [#74](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=desc) (ff07cde) into [main](https://codecov.io/gh/IduxFE/components/commit/0c37aee5dd4c4e5d7fdb422522ab5d7fada34a89?el=desc) (0c37aee) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/IduxFE/components/pull/74/graphs/tree.svg?width=650&height=150&src=pr&token=PGAUXP06V3)](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=tree)

```diff
@@           Coverage Diff           @@
##             main      #74   +/-   ##
=======================================
  Coverage   92.20%   92.20%           
=======================================
  Files          31       31           
  Lines         372      372           
  Branches       70       70           
=======================================
  Hits          343      343           
  Misses         11       11           
  Partials       18       18           
```


| [Impacted Files](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/components/button/src/Button.vue](https://codecov.io/gh/IduxFE/components/pull/74/diff?src=pr&el=tree#diff-cGFja2FnZXMvY29tcG9uZW50cy9idXR0b24vc3JjL0J1dHRvbi52dWU=) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=footer). Last update [0c37aee...7fb5ff3](https://codecov.io/gh/IduxFE/components/pull/74?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/packages/components/button/demo/basic.md b/packages/components/button/demo/basic.md
deleted file mode 100644
index 95f663f3..00000000
--- a/packages/components/button/demo/basic.md
+++ /dev/null
@@ -1,21 +0,0 @@
----
-order: 0
-title:
-  zh: 按钮类型
----
-
-## zh
-
-按钮有 5 种类型：主按钮、默认按钮、虚线按钮和链接按钮。
-
-## demo
-
-```html
-<template>
-  <ix-button mode="primary">Primary Button</ix-button>
-  <ix-button>Default Button</ix-button>
-  <ix-button mode="dashed">Dashed Button</ix-button>
-  <ix-button mode="text">Text Button</ix-button>
-  <ix-button mode="link" href="https://github.com/IduxFE/components" target="_blank">Link Button</ix-button>
-</template>
-```
diff --git a/packages/components/button/demo/block.md b/packages/components/button/demo/block.md
new file mode 100644
index 00000000..f730de55
--- /dev/null
+++ b/packages/components/button/demo/block.md
@@ -0,0 +1,21 @@
+---
+order: 7
+title:
+  zh: Block 按钮
+---
+
+## zh
+
+通过设置 `block` 将按钮宽度调整为自适应其父元素的宽度。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary" block>Primary</ix-button>
+  <ix-button block>Default</ix-button>
+  <ix-button mode="dashed" block>Dashed</ix-button>
+  <ix-button mode="text" block>Text</ix-button>
+  <ix-button mode="link" block href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+</template>
+```
diff --git a/packages/components/button/demo/danger.md b/packages/components/button/demo/danger.md
new file mode 100644
index 00000000..18336fc7
--- /dev/null
+++ b/packages/components/button/demo/danger.md
@@ -0,0 +1,21 @@
+---
+order: 1
+title:
+  zh: 危险按钮
+---
+
+## zh
+
+通过设置 `danger` 将按钮标识为危险状态。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary" danger>Primary</ix-button>
+  <ix-button danger>Default</ix-button>
+  <ix-button mode="dashed" danger>Dashed</ix-button>
+  <ix-button mode="text" danger>Text</ix-button>
+  <ix-button mode="link" danger href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+</template>
+```
diff --git a/packages/components/button/demo/disabled.md b/packages/components/button/demo/disabled.md
new file mode 100644
index 00000000..94d04d30
--- /dev/null
+++ b/packages/components/button/demo/disabled.md
@@ -0,0 +1,37 @@
+---
+order: 3
+title:
+  zh: 禁用状态
+---
+
+## zh
+
+通过设置 `disabled` 将按钮设为禁用状态。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary" disabled>Primary</ix-button>
+  <ix-button disabled>Default</ix-button>
+  <ix-button mode="dashed" disabled>Dashed</ix-button>
+  <ix-button mode="text" disabled>Text</ix-button>
+  <ix-button mode="link" disabled href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+  <br />
+  <ix-button :disabled="disabled">Disabled: {{disabled}}</ix-button>
+  <ix-button @click="changeDisabled">Change Disabled</ix-button>
+</template>
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const disabled = ref(false)
+    const changeDisabled = () => {
+      disabled.value = !disabled.value
+    }
+    return { disabled, changeDisabled }
+  },
+})
+</script>
+```
diff --git a/packages/components/button/demo/ghost.md b/packages/components/button/demo/ghost.md
new file mode 100644
index 00000000..f17efa3d
--- /dev/null
+++ b/packages/components/button/demo/ghost.md
@@ -0,0 +1,29 @@
+---
+order: 2
+title:
+  zh: 幽灵按钮
+---
+
+## zh
+
+通过设置 `ghost` 将按钮的背景设为透明，通常在有色背景上。
+
+## demo
+
+```html
+<template>
+<div class="button-ghost-demo">
+  <ix-button mode="primary" ghost>Primary</ix-button>
+  <ix-button ghost>Default</ix-button>
+  <ix-button mode="dashed" ghost>Dashed</ix-button>
+  <ix-button danger ghost>Danger</ix-button>
+</div>
+</template>
+
+<style lang="less" scoped>
+.button-ghost-demo {
+  background: rgb(200, 200, 200);
+  padding: 16px;
+}
+</style>
+```
diff --git a/packages/components/button/demo/icon.md b/packages/components/button/demo/icon.md
index b6e4a2d3..a1e8a717 100644
--- a/packages/components/button/demo/icon.md
+++ b/packages/components/button/demo/icon.md
@@ -1,18 +1,20 @@
 ---
-order: 1
+order: 8
 title:
   zh: 按钮图标
 ---
 
 ## zh
 
-按钮支持设置图标，当 loading 状态时图标失效。
+通过设置 `icon` 给按钮设置图标。
 
 ## demo
 
 ```html
 <template>
-  <ix-button icon="up">up Button</ix-button>
-  <ix-button icon="up" loading>loading Button</ix-button>
+  <ix-button icon="search">Search</ix-button>
+  <ix-button icon="search"></ix-button>
+  <ix-button icon="download">Download</ix-button>
+  <ix-button icon="download"></ix-button>
 </template>
 ```
diff --git a/packages/components/button/demo/loding.md b/packages/components/button/demo/loding.md
new file mode 100644
index 00000000..09811b31
--- /dev/null
+++ b/packages/components/button/demo/loding.md
@@ -0,0 +1,37 @@
+---
+order: 4
+title:
+  zh: 加载中状态
+---
+
+## zh
+
+通过设置 `loading` 将按钮设为加载中状态, 此时设置的 icon 无效。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary" icon="search" loading>Primary</ix-button>
+  <ix-button icon="search" loading>Default</ix-button>
+  <ix-button mode="dashed" loading>Dashed</ix-button>
+  <ix-button mode="text" loading>Text</ix-button>
+  <ix-button mode="link" loading href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+  <br />
+  <ix-button icon="search" :loading="isLoading">Loading: {{isLoading}}</ix-button>
+  <ix-button @click="changeLoading">Change Loading</ix-button>
+</template>
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const isLoading = ref(false)
+    const changeLoading = () => {
+      isLoading.value = !isLoading.value
+    }
+    return { isLoading, changeLoading }
+  },
+})
+</script>
+```
diff --git a/packages/components/button/demo/mode.md b/packages/components/button/demo/mode.md
new file mode 100644
index 00000000..0544054b
--- /dev/null
+++ b/packages/components/button/demo/mode.md
@@ -0,0 +1,21 @@
+---
+order: 0
+title:
+  zh: 按钮类型
+---
+
+## zh
+
+按钮共有 5 种类型：主按钮、默认按钮、虚线按钮和链接按钮，通过设置 `mode` 来使用不同的类型。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary">Primary</ix-button>
+  <ix-button>Default</ix-button>
+  <ix-button mode="dashed">Dashed</ix-button>
+  <ix-button mode="text">Text</ix-button>
+  <ix-button mode="link" href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+</template>
+```
diff --git a/packages/components/button/demo/shape.md b/packages/components/button/demo/shape.md
new file mode 100644
index 00000000..0937349f
--- /dev/null
+++ b/packages/components/button/demo/shape.md
@@ -0,0 +1,27 @@
+---
+order: 6
+title:
+  zh: 按钮形状
+---
+
+## zh
+
+通过设置 `shape` 来改变按钮的形状。
+
+## demo
+
+```html
+<template>
+  <ix-button mode="primary" icon="search" shape="circle"></ix-button>
+  <ix-button icon="download" shape="circle"></ix-button>
+  <ix-button mode="dashed" icon="download" shape="circle"></ix-button>
+  <ix-button mode="text" shape="circle">T</ix-button>
+  <ix-button mode="link" shape="circle" href="https://github.com/IduxFE/components" target="_blank">A</ix-button>
+  <br />
+  <ix-button mode="primary" icon="download" shape="round"></ix-button>
+  <ix-button icon="search" shape="round"></ix-button>
+  <ix-button mode="dashed" icon="download" shape="round">Download</ix-button>
+  <ix-button mode="text" shape="round">Text</ix-button>
+  <ix-button mode="link" shape="round" href="https://github.com/IduxFE/components" target="_blank">A</ix-button>
+</template>
+```
diff --git a/packages/components/button/demo/size.md b/packages/components/button/demo/size.md
new file mode 100644
index 00000000..d99045e6
--- /dev/null
+++ b/packages/components/button/demo/size.md
@@ -0,0 +1,38 @@
+---
+order: 5
+title:
+  zh: 按钮尺寸
+---
+
+## zh
+
+按钮共有 3 种尺寸：大、中、下，通过设置 `size` 来使用不同的尺寸，默认为中。
+
+## demo
+
+```html
+<template>
+  <ix-button @click="changeSize('large')">Large</ix-button>
+  <ix-button @click="changeSize('medium')">Medium</ix-button>
+  <ix-button @click="changeSize('small')">Small</ix-button>
+  <br />
+  <ix-button mode="primary" :size="size">Primary</ix-button>
+  <ix-button :size="size">Default</ix-button>
+  <ix-button mode="dashed" :size="size">Dashed</ix-button>
+  <ix-button mode="text" :size="size">Text</ix-button>
+  <ix-button mode="link" :size="size" href="https://github.com/IduxFE/components" target="_blank">Link</ix-button>
+</template>
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const size = ref('large')
+    const changeSize = (value: string) => {
+      size.value = value
+    }
+    return { size, changeSize }
+  },
+})
+</script>
+```
diff --git a/packages/components/button/docs/index.zh.md b/packages/components/button/docs/index.zh.md
index 5d16d6b2..455eef77 100644
--- a/packages/components/button/docs/index.zh.md
+++ b/packages/components/button/docs/index.zh.md
@@ -31,25 +31,64 @@ cover:
 
 ### `ix-button`
 
-> 当 `mode` 不为 `link` 时，除以下表格之外还支持原生 `button` 的[所有属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button)。
-> 当 `mode` 为 `link` 时，除以下表格之外还支持原生 `a` 的[所有属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a)。
-
-| 属性 | 说明 | 类型  | 全局配置 |
-| --- | --- | --- | --- |
-| `mode` | 设置按钮种类 | `'primary'|'default'|'dashed'|'text'|'link'` | `'default'` |
-| `danger` | 设置危险状态 | `boolean` | - |
-| `ghost` | 设置幽灵状态 | `boolean` | - |
-| `disabled` | 设置禁用状态 | `boolean` | - |
-| `loading` | 设置加载中状态 | `boolean` |  - |
-| `size` | 设置按钮大小 | `'large'|'medium'|'small'` | `'medium'` |
-| `shape` | 设置按钮形状 | `'circle'|'round'` | - |
-| `block` | 将按钮宽度调整为其父宽度的选项 | `boolean` | - |
-| `icon` | 设置图标类型(loading 状态时无效) | `string` | - |
+> 当 `mode` 不为 `link` 时，除以下表格之外还支持原生 `button` 元素的[所有属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/button)。
+> 当 `mode` 为 `link` 时，除以下表格之外还支持原生 `a` 元素的[所有属性](https://developer.mozilla.org/zh-CN/docs/Web/HTML/Element/a)。
+
+| 属性 | 说明 | 类型  | 全局配置| 默认值| 备注 |
+| --- | --- | --- | --- | --- | --- |
+| `mode` | 设置按钮种类 | `primary\|default\|dashed\|text\|link` | `default` | - |- |
+| `danger` | 设置危险状态 | `boolean` | - | - |- |
+| `ghost` | 设置幽灵状态 | `boolean` | - | - |- |
+| `disabled` | 设置禁用状态 | `boolean` | - | - |- |
+| `loading` | 设置加载中状态 | `boolean` |  - | - |- |
+| `size` | 设置按钮大小 | `large\|medium\|small` | `medium` | - |- |
+| `shape` | 设置按钮形状 | `circle\|round` | - | - |- |
+| `block` | 将按钮宽度调整为自适应其父元素的宽度 | `boolean` | - | - |- |
+| `icon` | 设置图标类型 | `string` | - | - | `loading` 为 `true` 时无效 |
 
 ### `ix-button-group`
 
-| 属性 | 说明 | 类型  | 全局配置 |
-| --- | --- | --- | --- |
-| `mode` | 设置组内按钮种类， | `'primary'|'default'|'dashed'|'text'|'link'` | - |
-| `size` | 设置组内按钮大小 | `'large'|'medium'|'small'` | - |
-| `shape` | 设置组内按钮形状 | `'circle'|'round'` | - |
+| 属性 | 说明 | 类型  | 全局配置| 默认值| 备注 |
+| --- | --- | --- | --- | --- | --- |
+| `mode` | 设置组内按钮种类 | `primary\|default\|dashed\|text\|link` | - | - |- |
+| `size` | 设置组内按钮大小 | `large\|medium\|small` | - | - |- |
+| `shape` | 设置组内按钮形状 | `circle\|round` | - | - |- |
+
+### 主题变量
+
+| 变量名 | default 主题| 说明 |
+| --- | --- | --- |
+| @button-font-weight | @font-weight-md | - |
+| @button-line-height | @line-height-md | - |
+| @button-height-sm | @height-sm | - |
+| @button-height-md | @height-md | - |
+| @button-height-lg | @height-lg | - |
+| @button-font-size-sm | @font-size-sm | - |
+| @button-font-size-md | @font-size-sm | - |
+| @button-font-size-lg | @font-size-md | - |
+| @button-padding-sm | @padding-xs | - |
+| @button-padding-md | @padding-md | - |
+| @button-padding-lg | @padding-lg | - |
+| @button-icon-margin-left | @margin-xs | - |
+| @button-border-style | @border-style | - |
+| @button-border-size | @border-size-sm | - |
+| @button-border-radius | @border-radius-sm | - |
+| @button-disable-fade | 40% | - |
+| @button-shadow | 0 2px 0 rgba(0, 0, 0, 0.015) | - |
+| @button-primary-color | @white | - |
+| @button-primary-bg | @primary-color | - |
+| @button-primary-border | color(~\`colorPalette('@{button-primary-bg}', 10) \`) | - |
+| @button-primary-text-shadow | 0 -1px 0 rgba(0, 0, 0, 0.15) | - |
+| @button-default-color | @text-color | - |
+| @button-default-bg | @component-background | - |
+| @button-default-border | @border-color | - |
+| @button-danger-color | @error-color | - |
+| @button-danger-bg | @error-color | - |
+| @button-danger-border | color(~\`colorPalette('@{button-danger-bg}', 10) \`) | - |
+| @button-ghost-color | @component-background | - |
+| @button-ghost-color | transparent | - |
+| @button-ghost-border | @component-background | - |
+| @button-link-color | @primary-color | - |
+| @button-link-hover-bg | transparent | - |
+| @button-text-color | @text-color | - |
+| @button-text-hover-bg | rgba(0, 0, 0, 0.12) | - |
diff --git a/packages/components/button/src/Button.vue b/packages/components/button/src/Button.vue
index bf4103ba..27036c11 100644
--- a/packages/components/button/src/Button.vue
+++ b/packages/components/button/src/Button.vue
@@ -1,21 +1,21 @@
 <template>
-  <component :is="tag" class="ix-button" :class="classes" :disabled="disabled || loading">
+  <component :is="tag" class="ix-button" :class="classes" :disabled="disabled || loading ? true : undefined">
     <ix-icon v-if="loading" name="loading" />
     <ix-icon v-else-if="icon" :name="icon" />
-    <slot></slot>
+    <span v-if="hasDefaultSolot"><slot></slot></span>
   </component>
 </template>
 
 <script lang="ts">
-import type { ComputedRef, PropType } from 'vue'
-import type { ButtonMode, ComponentSize } from '@idux/components/core/types'
-import type { ButtonConfig } from '@idux/components/core/config'
-import type { ButtonGroupProps, ButtonProps, ButtonShape } from './types'
-
-import { computed, defineComponent, inject } from 'vue'
+import { computed, defineComponent, inject, onUpdated, ref } from 'vue'
 import { useGlobalConfig } from '@idux/components/core/config'
-import { buttonGroupInjectionKey } from './button'
 import { IxIcon } from '@idux/components/icon'
+import { buttonGroupInjectionKey } from './button'
+
+import type { ComputedRef, PropType, Ref } from 'vue'
+import type { ButtonConfig } from '@idux/components/core/config'
+import type { ButtonMode, ComponentSize } from '@idux/components/core/types'
+import type { ButtonGroupProps, ButtonProps, ButtonShape } from './types'
 
 export default defineComponent({
   name: 'IxButton',
@@ -31,15 +31,21 @@ export default defineComponent({
     block: Boolean,
     icon: { type: String, default: undefined },
   },
-  setup(props: ButtonProps) {
+  setup(props: ButtonProps, { slots }) {
     const groupProps = inject(buttonGroupInjectionKey, {})
     const buttonConfig = useGlobalConfig('button')
 
-    const mode = computed(() => (props.mode !== undefined ? props.mode : groupProps.mode || buttonConfig.mode))
+    const mode = computed(() => props.mode ?? (groupProps.mode || buttonConfig.mode))
+
+    const hasDefaultSolot = ref(!!slots.default)
+    onUpdated(() => {
+      hasDefaultSolot.value = !!slots.default
+    })
+    const classes = useClasses(props, groupProps, buttonConfig, mode, hasDefaultSolot)
+
     const tag = computed(() => (mode.value === 'link' ? 'a' : 'button'))
 
-    const classes = useClasses(props, groupProps, buttonConfig, mode)
-    return { classes, tag }
+    return { classes, tag, hasDefaultSolot }
   },
 })
 
@@ -48,10 +54,11 @@ const useClasses = (
   groupProps: ButtonGroupProps,
   config: ButtonConfig,
   mode: ComputedRef<ButtonMode>,
+  hasDefaultSolot: Ref<boolean>,
 ) => {
   return computed(() => {
-    const size = props.size !== undefined ? props.size : groupProps.size || config.size
-    const shape = props.shape !== undefined ? props.shape : groupProps.shape
+    const size = props.size ?? (groupProps.size || config.size)
+    const shape = props.shape ?? groupProps.shape
     return [
       mode.value !== 'default' ? `ix-button-${mode.value}` : '',
       size !== 'medium' ? `ix-button-${size}` : '',
@@ -62,6 +69,7 @@ const useClasses = (
         'ix-button-disabled': props.disabled,
         'ix-button-loading': props.loading,
         'ix-button-block': props.block,
+        'ix-button-icon-only': !hasDefaultSolot.value && (!!props.icon || props.loading),
       },
     ]
   })
diff --git a/packages/components/button/style/index.less b/packages/components/button/style/index.less
index 1492472a..e7767ab1 100644
--- a/packages/components/button/style/index.less
+++ b/packages/components/button/style/index.less
@@ -1,7 +1,115 @@
 @import '../../style/default.less';
+@import './mixin.less';
 
 @button-prefix: ~'@{idux-prefix}-button';
 
 .@{button-prefix} {
-  color: @primary-color;
+  line-height: @button-line-height;
+  .button;
+  .button-default;
+
+  > span {
+    display: inline-block;
+  }
+
+  &-primary {
+    .button-primary;
+  }
+
+  &-dashed {
+    .button-dashed;
+  }
+
+  &-text {
+    .button-text;
+  }
+
+  &-link {
+    .button-link;
+  }
+
+  &-danger {
+    .button-danger;
+  }
+
+  &-danger&-primary {
+    .button-danger-primary;
+  }
+
+  &-danger&-text {
+    .button-danger-text;
+  }
+
+  &-danger&-link {
+    .button-danger-link;
+  }
+
+  &-ghost {
+    .button-ghost;
+  }
+
+  &-ghost&-primary {
+    .button-ghost-primary;
+  }
+
+  &-ghost&-danger {
+    .button-ghost-danger;
+  }
+
+  &&-loading {
+    position: relative;
+
+    &::before {
+      display: block;
+    }
+  }
+
+  &-round {
+    .button-round(@button-prefix);
+  }
+
+  &-circle {
+    .button-circle(@button-prefix);
+  }
+
+  &-block {
+    width: 100%;
+  }
+
+  &-icon-only {
+    .icon-only(@button-prefix; @icon-prefix);
+  }
+
+  &::before {
+    position: absolute;
+    top: -1px;
+    right: -1px;
+    bottom: -1px;
+    left: -1px;
+    z-index: 1;
+    display: none;
+    background: @button-default-bg;
+    border-radius: inherit;
+    opacity: 0.35;
+    transition: opacity 0.2s;
+    content: '';
+    pointer-events: none;
+  }
+
+  &:focus > span,
+  &:active > span {
+    position: relative;
+  }
+
+  > .@{icon-prefix} + span,
+  > span + .@{icon-prefix} {
+    margin-left: @button-icon-margin-left;
+  }
+
+  &:empty {
+    display: inline-block;
+    width: 0;
+    visibility: hidden;
+    content: '\a0';
+  }
 }
diff --git a/packages/components/button/style/mixin.less b/packages/components/button/style/mixin.less
new file mode 100644
index 00000000..4bda44b3
--- /dev/null
+++ b/packages/components/button/style/mixin.less
@@ -0,0 +1,311 @@
+.button-size(@height; @font-size; @padding-horizontal; @border-radius) {
+  @padding-vertical: max(round((@height - @font-size * @button-line-height) / 2 * 10) / 10 - @button-border-size, 0);
+
+  height: @height;
+  padding: @padding-vertical @padding-horizontal;
+  font-size: @font-size;
+  border-radius: @border-radius;
+}
+
+.button-disabled(@color; @background; @border) {
+  &[disabled] {
+    &,
+    &:hover,
+    &:focus,
+    &:active {
+      .button-color(
+        fade(@color, @button-disable-fade); 
+        fade(@background, @button-disable-fade); 
+        fade(@border, @button-disable-fade)
+      );
+      text-shadow: none;
+      box-shadow: none;
+    }
+  }
+}
+
+.button-variant-primary(@color; @background; @border) {
+  .button-color(@color; @background; @border);
+  text-shadow: @button-primary-text-shadow;
+
+  &:hover,
+  &:focus {
+    & when (@theme = default) {
+      .button-color(@color; ~`colorPalette('@{background}', -10) `; ~`colorPalette('@{border}', -10) `);
+    }
+  }
+
+  &:active {
+    & when (@theme = default) {
+      .button-color(@color; ~`colorPalette('@{background}', 10) `; ~`colorPalette('@{border}', 10) `);
+    }
+  }
+
+  .button-disabled(@color; @background; @border);
+}
+
+.button-variant-other(@color; @background; @border) {
+  .button-color(@color; @background; @border);
+
+  &:hover,
+  &:focus {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-primary-bg}', -10) `; @background; ~`colorPalette('@{button-primary-bg}', -10) `);
+    }
+  }
+  &:active {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-primary-bg}', 10) `; @background; ~`colorPalette('@{button-primary-bg}', 10) `);
+    }
+  }
+  .button-disabled(@color; @component-background; @border);
+}
+
+.button-variant-ghost(@color; @border: @color) {
+  text-shadow: none;
+  &:hover,
+  &:focus {
+    & when (@border = transparent) {
+      & when (@theme = default) {
+        .button-color(~`colorPalette('@{color}', -10) `; transparent; transparent);
+      }
+    }
+    & when not (@border = transparent) {
+      & when (@theme = default) {
+        .button-color(~`colorPalette('@{color}', -10) `; transparent; ~`colorPalette('@{color}', -10) `);
+      }
+    }
+  }
+  &:active {
+    & when (@border = transparent) {
+      & when (@theme = default) {
+        .button-color(~`colorPalette('@{color}', 10) `; transparent; transparent);
+      }
+    }
+    & when not(@border = transparent) {
+      & when (@theme = default) {
+        .button-color(~`colorPalette('@{color}', 10) `; transparent; ~`colorPalette('@{color}', 10) `);
+      }
+    }
+  }
+}
+
+.button-color(@color; @background; @border) {
+  color: @color;
+  background: @background;
+  border-color: @border;
+  > a:only-child {
+    color: currentColor;
+    &::after {
+      position: absolute;
+      top: 0;
+      right: 0;
+      bottom: 0;
+      left: 0;
+      background: transparent;
+      content: '';
+    }
+  }
+}
+
+.button() {
+  position: relative;
+  display: inline-block;
+  font-weight: @button-font-weight;
+  white-space: nowrap;
+  text-align: center;
+  background-image: none;
+  border: @button-border-size @button-border-style transparent;
+  box-shadow: @button-shadow;
+  cursor: pointer;
+  transition: all 0.3s @ease-in-out;
+  user-select: none;
+  touch-action: manipulation;
+  .button-size(@button-height-md; @button-font-size-md; @button-padding-md; @button-border-radius);
+  &,
+  &:active,
+  &:focus {
+    outline: 0;
+  }
+  &:not([disabled]):hover {
+    text-decoration: none;
+  }
+  &:not([disabled]):active {
+    outline: 0;
+    box-shadow: none;
+  }
+  &[disabled] {
+    cursor: not-allowed;
+    > * {
+      pointer-events: none;
+    }
+  }
+  &-small {
+    .button-size(@button-height-sm; @button-font-size-sm;  @button-padding-sm;@button-border-radius);
+  }
+  &-large {
+    .button-size(@button-height-lg; @button-font-size-lg; @button-padding-lg; @button-border-radius);
+  }
+}
+
+.button-default() {
+  .button-variant-other(@button-default-color; @button-default-bg; @button-default-border);
+  &:hover,
+  &:focus,
+  &:active {
+    text-decoration: none;
+    background: @button-default-bg;
+  }
+}
+
+.button-primary() {
+  .button-variant-primary(@button-primary-color; @button-primary-bg; @button-primary-border);
+}
+
+.button-dashed() {
+  .button-variant-other(@button-default-color, @button-default-bg, @button-default-border);
+  border-style: dashed;
+}
+
+.button-text() {
+  .button-variant-other(@button-text-color, transparent, transparent);
+  box-shadow: none;
+  &:hover,
+  &:focus {
+    color: @button-text-color;
+    background: @button-text-hover-bg;
+    border-color: transparent;
+  }
+
+  &:active {
+    color: @button-text-color;
+    background: fadein(@button-text-hover-bg, 1%);
+    border-color: transparent;
+  }
+
+  .button-disabled(@button-text-color; @component-background; @component-background);
+}
+
+.button-link() {
+  .button-variant-other(@button-link-color, transparent, transparent);
+  box-shadow: none;
+  &:hover {
+    background: @button-link-hover-bg;
+  }
+  &:hover,
+  &:focus,
+  &:active {
+    border-color: transparent;
+  }
+  .button-disabled(@button-link-color; @component-background; @component-background);
+}
+
+.button-danger() {
+  .button-color(@button-danger-color, @button-default-bg, @button-danger-border);
+  &:hover,
+  &:focus {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-danger-color}', -10) `; @button-default-bg; ~`colorPalette('@{button-danger-border}', -10) `);
+    }
+  }
+  &:active {
+    & when (@theme = default) {
+      .button-color( ~`colorPalette('@{button-danger-color}', 10) `; @button-default-bg; ~`colorPalette('@{button-danger-border}', 10) `);
+    }
+  }
+  .button-disabled(@button-danger-color; @button-default-bg; @button-danger-border);
+}
+
+.button-danger-primary() {
+  .button-variant-primary(@button-primary-color, @button-danger-bg, @button-danger-border);
+}
+
+.button-danger-text() {
+  .button-variant-other(@button-danger-color, transparent, transparent);
+  box-shadow: none;
+  &:hover,
+  &:focus {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-danger-color}', -10) `; @button-text-hover-bg; transparent);
+    }
+  }
+
+  &:active {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-danger-color}', 10) `; fadein(@button-text-hover-bg, 1%); transparent);
+    }
+  }
+  .button-disabled(@button-danger-color; @component-background; @component-background);
+}
+
+.button-danger-link() {
+  .button-variant-other(@button-danger-color, transparent, transparent);
+  box-shadow: none;
+  &:hover,
+  &:focus {
+    & when (@theme = default) {
+      .button-color(~`colorPalette('@{button-danger-color}', -10) `; transparent; transparent);
+    }
+  }
+  &:active {
+    & when (@theme = dark) {
+      .button-color(~`colorPalette('@{button-danger-color}', 10) `; transparent; transparent);
+    }
+  }
+  .button-disabled(@button-danger-color; @component-background; @component-background);
+}
+
+.button-ghost() {
+  .button-color(@button-ghost-color; @button-ghost-bg; @button-ghost-border);
+  .button-variant-ghost(@button-primary-border;);
+  .button-disabled(@button-ghost-color; @component-background; @button-ghost-border);
+}
+
+.button-ghost-primary() {
+  .button-color(@button-primary-border; @button-ghost-bg; @button-primary-border);
+  .button-variant-ghost(@button-primary-border);
+  .button-disabled(@button-primary-border; @component-background; @button-primary-border);
+}
+
+.button-ghost-danger() {
+  .button-color(@button-danger-color; @button-ghost-bg; @button-danger-color);
+  .button-variant-ghost(@button-danger-border);
+  .button-disabled(@button-danger-color; @component-background; @button-danger-border);
+}
+
+.button-round(@buttonPrefix) {
+  .button-size(@button-height-md; @button-font-size-md; @button-height-md / 2; @button-height-md);
+  &.@{buttonPrefix}-small {
+    .button-size(@button-height-sm; @button-font-size-sm; @button-height-sm / 2; @button-height-sm);
+  }
+  &.@{buttonPrefix}-large {
+    .button-size(@button-height-lg; @button-font-size-lg; @button-height-lg / 2; @button-height-lg);
+  }
+}
+
+.button-circle(@buttonPrefix) {
+  min-width: @button-height-md;
+  padding-right: 0;
+  padding-left: 0;
+  text-align: center;
+  border-radius: 50%;
+  &.@{buttonPrefix}-small {
+    min-width: @button-height-sm;
+    border-radius: 50%;
+  }
+  &.@{buttonPrefix}-large {
+    min-width: @button-height-lg;
+    border-radius: 50%;
+  }
+}
+
+.icon-only(@buttonPrefix; @icon-prefix) {
+  line-height: 1;
+  font-size: @button-font-size-md + 2;
+  &.@{buttonPrefix}-small {
+    font-size: @button-font-size-sm + 2;
+  }
+  &.@{buttonPrefix}-large {
+    font-size: @button-font-size-lg + 2;
+  }
+}
diff --git a/packages/components/index.less b/packages/components/index.less
index e1bdb3a6..ebc0c257 100644
--- a/packages/components/index.less
+++ b/packages/components/index.less
@@ -1,2 +1,3 @@
 @import './style/reset.less';
+@import './style/base.less';
 @import './components.less';
diff --git a/packages/components/style/base.less b/packages/components/style/base.less
new file mode 100644
index 00000000..3235e099
--- /dev/null
+++ b/packages/components/style/base.less
@@ -0,0 +1,11 @@
+@import './default.less';
+
+a {
+  &[disabled] {
+    pointer-events: none;
+  }
+}
+
+.clearfix {
+  .clearfix();
+}
diff --git a/packages/components/style/default.less b/packages/components/style/default.less
index f13abca6..316d83d7 100644
--- a/packages/components/style/default.less
+++ b/packages/components/style/default.less
@@ -5,8 +5,71 @@
 
 @idux-prefix: ix;
 
-/* status color -------------------------- */
+/* height -------------------------- */
+@height-sm: 24px;
+@height-md: 32px;
+@height-lg: 36px;
+
+@line-height-md: 1.5715;
+
+/* color -------------------------- */
 @primary-color: @blue;
+@info-color: @primary-color;
 @success-color: @green;
-@warning-color: @gold;
 @error-color: @red;
+
+@body-background: @white;
+@component-background: @white;
+
+/* text -------------------------- */
+@text-color: #333333;
+
+/* button -------------------------- */
+@button-font-weight: @font-weight-md;
+
+@button-line-height: @line-height-md;
+
+@button-height-sm: @height-sm;
+@button-height-md: @height-md;
+@button-height-lg: @height-lg;
+
+@button-font-size-sm: @font-size-sm;
+@button-font-size-md: @font-size-sm;
+@button-font-size-lg: @font-size-md;
+
+@button-padding-sm: @padding-sm;
+@button-padding-md: @padding-md;
+@button-padding-lg: @padding-lg;
+
+@button-icon-margin-left: @margin-xs;
+
+@button-border-style: @border-style;
+@button-border-size: @border-size-sm;
+@button-border-radius: @border-radius-sm;
+
+@button-disable-fade: 40%;
+
+@button-shadow: 0 2px 0 rgba(0, 0, 0, 0.015);
+
+@button-primary-color: @white;
+@button-primary-bg: @primary-color;
+@button-primary-border: color(~`colorPalette('@{button-primary-bg}', 10) `);
+@button-primary-text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.15);
+
+@button-default-color: @text-color;
+@button-default-bg: @component-background;
+@button-default-border: @border-color;
+
+@button-danger-color: @error-color;
+@button-danger-bg: @error-color;
+@button-danger-border: color(~`colorPalette('@{button-danger-bg}', 10) `);
+
+@button-ghost-color: @component-background;
+@button-ghost-bg: transparent;
+@button-ghost-border: @component-background;
+
+@button-link-color: @primary-color;
+@button-link-hover-bg: transparent;
+
+@button-text-color: @text-color;
+@button-text-hover-bg: rgba(0, 0, 0, 0.12);
diff --git a/packages/components/style/variable/font.less b/packages/components/style/variable/font.less
index c678a6fc..8d1d2ec0 100644
--- a/packages/components/style/variable/font.less
+++ b/packages/components/style/variable/font.less
@@ -1,5 +1,5 @@
 /* font -------------------------- */
-@font-size-base: 12px;
+@font-size-base: 14px;
 
 @font-size-xs: @font-size-base - 2;
 @font-size-sm: @font-size-base;
diff --git a/scripts/gulp/site/_site/docs/src/index.less b/scripts/gulp/site/_site/docs/src/index.less
index e91b53f5..e6f44854 100644
--- a/scripts/gulp/site/_site/docs/src/index.less
+++ b/scripts/gulp/site/_site/docs/src/index.less
@@ -1,6 +1,7 @@
 @import '../../../packages/components/index.less';
 @import './styles/header.less';
 @import './styles/side-nav.less';
+@import './styles/components.less';
 
 .site-root {
   background-color: #fff;
diff --git a/scripts/gulp/site/_site/docs/src/styles/components.less b/scripts/gulp/site/_site/docs/src/styles/components.less
new file mode 100644
index 00000000..407b62d7
--- /dev/null
+++ b/scripts/gulp/site/_site/docs/src/styles/components.less
@@ -0,0 +1,1 @@
+@import './components/button.less';
diff --git a/scripts/gulp/site/_site/docs/src/styles/components/button.less b/scripts/gulp/site/_site/docs/src/styles/components/button.less
new file mode 100644
index 00000000..9d7a7f42
--- /dev/null
+++ b/scripts/gulp/site/_site/docs/src/styles/components/button.less
@@ -0,0 +1,3 @@
+[id^='components-button-demo-'] .ix-button {
+  margin: 8px 12px 8px 0;
+}


Test patch:
diff --git a/packages/components/button/__tests__/__snapshots__/button.spec.ts.snap b/packages/components/button/__tests__/__snapshots__/button.spec.ts.snap
index 1038fdcb..8c042eb2 100644
--- a/packages/components/button/__tests__/__snapshots__/button.spec.ts.snap
+++ b/packages/components/button/__tests__/__snapshots__/button.spec.ts.snap
@@ -1,3 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`Button.vue render work 1`] = `"<button class=\\"ix-button\\"><!--v-if--></button>"`;
+exports[`Button.vue render work 1`] = `"<button class=\\"ix-button\\"><!--v-if--><!--v-if--></button>"`;
diff --git a/packages/components/button/__tests__/__snapshots__/buttonGroup.spec.ts.snap b/packages/components/button/__tests__/__snapshots__/buttonGroup.spec.ts.snap
index 68624587..e6734d02 100644
--- a/packages/components/button/__tests__/__snapshots__/buttonGroup.spec.ts.snap
+++ b/packages/components/button/__tests__/__snapshots__/buttonGroup.spec.ts.snap
@@ -1,3 +1,3 @@
 // Jest Snapshot v1, https://goo.gl/fbAQLP
 
-exports[`ButtonGroup.vue render work 1`] = `"<div class=\\"ix-button-group\\"><button class=\\"ix-button\\"><!--v-if-->default</button><button class=\\"ix-button ix-button-primary\\"><!--v-if-->primary</button><button class=\\"ix-button ix-button-large\\"><!--v-if-->large</button><button class=\\"ix-button ix-button-circle\\"><!--v-if-->circle</button></div>"`;
+exports[`ButtonGroup.vue render work 1`] = `"<div class=\\"ix-button-group\\"><button class=\\"ix-button\\"><!--v-if--><span>default</span></button><button class=\\"ix-button ix-button-primary\\"><!--v-if--><span>primary</span></button><button class=\\"ix-button ix-button-large\\"><!--v-if--><span>large</span></button><button class=\\"ix-button ix-button-circle\\"><!--v-if--><span>circle</span></button></div>"`;
diff --git a/packages/components/button/__tests__/button.spec.ts b/packages/components/button/__tests__/button.spec.ts
index bef18cf6..0e075efa 100644
--- a/packages/components/button/__tests__/button.spec.ts
+++ b/packages/components/button/__tests__/button.spec.ts
@@ -68,7 +68,7 @@ describe('Button.vue', () => {
     expect(wrapper.classes()).toContain('ix-button-disabled')
   })
 
-  test.only('loading work', async () => {
+  test('loading work', async () => {
     const wrapper = buttonMount({ props: { icon: 'up', loading: true } })
 
     expect(wrapper.findAll('.ix-icon').length).toEqual(1)
@@ -108,7 +108,7 @@ describe('Button.vue', () => {
     expect(wrapper.classes()).toContain('ix-button-block')
   })
 
-  test.only('icon work', async () => {
+  test('icon work', async () => {
     const wrapper = buttonMount({ props: { icon: 'up' } })
 
     expect(wrapper.find('.ix-icon-up').exists()).toBeTruthy()
@@ -116,6 +116,8 @@ describe('Button.vue', () => {
     await wrapper.setProps({ icon: 'down' })
 
     expect(wrapper.find('.ix-icon-down').exists()).toBeTruthy()
+
+    expect(wrapper.classes()).toContain('ix-button-icon-only')
   })
 
   test('slot work', async () => {