Repository: IduxFE/components
Pull request #173 (2021-01-28 07:50:59)

feat(cdk:forms): add useValueAccessor
re #153 

## 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
- [x] Feature
- [ ] 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/173?src=pr&el=h1) Report
> Merging [#173](https://codecov.io/gh/IduxFE/components/pull/173?src=pr&el=desc) (1bf39e6) into [main](https://codecov.io/gh/IduxFE/components/commit/a1fc6c8e9a0155352fbe7a37d1f4f27b4716ed8b?el=desc) (a1fc6c8) will **increase** coverage by `0.05%`.
> The diff coverage is `100.00%`.

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

```diff
@@            Coverage Diff             @@
##             main     #173      +/-   ##
==========================================
+ Coverage   97.49%   97.54%   +0.05%     
==========================================
  Files          64       65       +1     
  Lines        1238     1264      +26     
  Branches      228      234       +6     
==========================================
+ Hits         1207     1233      +26     
  Misses         11       11              
  Partials       20       20              
```


| [Impacted Files](https://codecov.io/gh/IduxFE/components/pull/173?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [packages/cdk/forms/src/controls/abstractControl.ts](https://codecov.io/gh/IduxFE/components/pull/173/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy9jb250cm9scy9hYnN0cmFjdENvbnRyb2wudHM=) | `100.00% <ø> (ø)` | |
| [packages/cdk/forms/src/typeof.ts](https://codecov.io/gh/IduxFE/components/pull/173/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy90eXBlb2YudHM=) | `100.00% <100.00%> (ø)` | |
| [packages/cdk/forms/src/useValueAccessor.ts](https://codecov.io/gh/IduxFE/components/pull/173/diff?src=pr&el=tree#diff-cGFja2FnZXMvY2RrL2Zvcm1zL3NyYy91c2VWYWx1ZUFjY2Vzc29yLnRz) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/IduxFE/components/pull/173?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/173?src=pr&el=footer). Last update [a1fc6c8...1bf39e6](https://codecov.io/gh/IduxFE/components/pull/173?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/package.json b/package.json
index 7be27655..b8acb3cc 100644
--- a/package.json
+++ b/package.json
@@ -34,58 +34,58 @@
     "@types/jest": "^26.0.0",
     "@types/lodash": "^4.14.0",
     "@types/marked": "^1.2.0",
-    "@types/parse5": "^5.0.0",
+    "@types/parse5": "^6.0.0",
     "@types/prismjs": "^1.16.0",
     "@types/svgo": "^1.3.0",
     "@types/yaml-front-matter": "^4.1.0",
-    "@typescript-eslint/eslint-plugin": "^4.8.0",
-    "@typescript-eslint/parser": "^4.8.0",
+    "@typescript-eslint/eslint-plugin": "^4.14.0",
+    "@typescript-eslint/parser": "^4.14.0",
     "@vitejs/plugin-vue": "^1.1.0",
     "@vue/babel-plugin-jsx": "^1.0.0",
     "@vue/compiler-sfc": "^3.0.0",
     "@vue/eslint-config-prettier": "^6.0.0",
     "@vue/eslint-config-typescript": "^7.0.0",
-    "@vue/test-utils": "^2.0.0-beta.12",
+    "@vue/test-utils": "^2.0.0-beta.14",
     "babel-jest": "^26.6.0",
     "chalk": "^4.1.0",
     "codecov": "^3.8.0",
     "commitizen": "^4.2.0",
     "cz-conventional-changelog": "^3.3.0",
     "detect-port": "^1.3.0",
-    "eslint": "^7.14.0",
-    "eslint-config-prettier": "^6.15.0",
+    "eslint": "^7.18.0",
+    "eslint-config-prettier": "^7.2.0",
     "eslint-plugin-markdown": "^2.0.0-rc.0",
-    "eslint-plugin-prettier": "^3.1.0",
-    "eslint-plugin-vue": "^7.1.0",
-    "fs-extra": "^9.0.0",
+    "eslint-plugin-prettier": "^3.3.0",
+    "eslint-plugin-vue": "^7.5.0",
+    "fs-extra": "^9.1.0",
     "gulp": "^4.0.0",
     "gulp-clean": "^0.4.0",
     "husky": "^4.3.0",
     "jest": "^26.6.0",
     "jest-junit": "^12.0.0",
     "lerna": "^3.22.0",
-    "less": "^3.12.2",
+    "less": "^4.1.0",
     "lint-staged": "^10.5.0",
     "lodash": "^4.17.0",
-    "markdownlint-cli": "^0.25.0",
+    "markdownlint-cli": "^0.26.0",
     "marked": "^1.2.0",
     "parse5": "^6.0.0",
-    "prettier": "^2.0.0",
-    "prismjs": "^1.22.0",
+    "prettier": "^2.2.0",
+    "prismjs": "^1.23.0",
     "remark": "^13.0.0",
     "resolve-bin": "^0.4.0",
     "rimraf": "^3.0.0",
-    "stylelint": "^13.0.0",
+    "stylelint": "^13.9.0",
     "stylelint-config-prettier": "^8.0.0",
     "stylelint-config-standard": "^20.0.0",
     "svgo": "^1.3.0",
     "ts-jest": "^26.4.0",
     "ts-node": "^9.1.0",
-    "tslib": "^2.0.0",
+    "tslib": "^2.1.0",
     "typescript": "^4.1.0",
-    "vite": "v2.0.0-beta.48",
+    "vite": "v2.0.0-beta.50",
     "vue": "^3.0.5",
-    "vue-jest": "^5.0.0-alpha.7",
+    "vue-jest": "^5.0.0-alpha.8",
     "vue-router": "^4.0.0",
     "vue-types": "^3.0.0",
     "yaml-front-matter": "^4.1.0"
diff --git a/packages/cdk/forms/demo/Basic.vue b/packages/cdk/forms/demo/Basic.vue
index fb4d1072..375373ed 100644
--- a/packages/cdk/forms/demo/Basic.vue
+++ b/packages/cdk/forms/demo/Basic.vue
@@ -26,7 +26,7 @@ export default defineComponent({
     group.watchValue(value => console.log(value))
     group.watchStatus(stats => console.log(stats))
 
-    group.get('name')?.watchStatus(stats => console.log(stats))
+    group.get('name')!.watchStatus(stats => console.log(stats))
 
     return { group }
   },
diff --git a/packages/cdk/forms/demo/BasicForm.vue b/packages/cdk/forms/demo/BasicForm.vue
index c4762205..273ed177 100644
--- a/packages/cdk/forms/demo/BasicForm.vue
+++ b/packages/cdk/forms/demo/BasicForm.vue
@@ -12,6 +12,5 @@ export default defineComponent({
   setup(props) {
     provideControl(props.control)
   },
-  template: `<form><slot /></form>`,
 })
 </script>
diff --git a/packages/cdk/forms/demo/BasicInput.vue b/packages/cdk/forms/demo/BasicInput.vue
index 861bab45..f4635724 100644
--- a/packages/cdk/forms/demo/BasicInput.vue
+++ b/packages/cdk/forms/demo/BasicInput.vue
@@ -1,23 +1,24 @@
 <template>
-  <input :value="control$.valueRef.value" @input="onChange" @blur="control$.markAsBlurred()" />
+  <input :value="valueAccessor.value" @input="onInput" @blur="onBlur" />
 </template>
 
 <script lang="ts">
 import { defineComponent } from 'vue'
-import { injectControl } from '@idux/cdk/forms'
+import { useValueAccessor } from '@idux/cdk/forms'
 
 export default defineComponent({
   // eslint-disable-next-line vue/require-prop-types
-  props: ['control'],
-  setup(props) {
-    const control$ = injectControl(props.control)!
-
-    // eslint-disable-next-line @typescript-eslint/no-explicit-any
-    const onChange = (evt: any) => {
-      control$.setValue(evt.target!.value, { dirty: true })
+  props: ['value', 'control'],
+  emits: ['update:value'],
+  setup() {
+    const valueAccessor = useValueAccessor()
+    const onInput = (evt: Event) => {
+      valueAccessor.setValue?.((evt.target as HTMLInputElement).value)
     }
-
-    return { control$, onChange }
+    const onBlur = () => {
+      valueAccessor.markAsBlurred?.()
+    }
+    return { valueAccessor, onInput, onBlur }
   },
 })
 </script>
diff --git a/packages/cdk/forms/index.ts b/packages/cdk/forms/index.ts
index baddc78a..855330cb 100644
--- a/packages/cdk/forms/index.ts
+++ b/packages/cdk/forms/index.ts
@@ -4,5 +4,6 @@ export * from './src/controls/formControl'
 export * from './src/controls/formGroup'
 export * from './src/types'
 export * from './src/useForms'
+export * from './src/useValueAccessor'
 export * from './src/utils'
 export * from './src/validators'
diff --git a/packages/cdk/forms/src/controls/abstractControl.ts b/packages/cdk/forms/src/controls/abstractControl.ts
index 4ead746d..cae49bd7 100644
--- a/packages/cdk/forms/src/controls/abstractControl.ts
+++ b/packages/cdk/forms/src/controls/abstractControl.ts
@@ -133,7 +133,7 @@ export abstract class AbstractControl<T = any> {
   /**
    * Sets a new value for the control.
    */
-  abstract setValue(value: Partial<T>, options: { dirty?: boolean }): void
+  abstract setValue(value: Partial<T>, options?: { dirty?: boolean }): void
 
   /**
    * The aggregate value of the control.
diff --git a/packages/cdk/forms/src/typeof.ts b/packages/cdk/forms/src/typeof.ts
index 411b22fc..744ed86c 100644
--- a/packages/cdk/forms/src/typeof.ts
+++ b/packages/cdk/forms/src/typeof.ts
@@ -12,7 +12,7 @@ export const isAbstractControl = (val: unknown): val is AbstractControl => {
 
 // Since AbstractControl be dependent on the function, `val instanceof FormArray` cannot be used here.
 export const isFormArray = (val: unknown): val is FormArray => {
-  return isAbstractControl(val) && hasOwnProperty(val, 'controls') && isArray((val as FormArray).controls)
+  return isAbstractControl(val) && isArray((val as FormArray).controls)
 }
 
 // Since AbstractControl be dependent on the function, `val instanceof FormGroup` cannot be used here.
diff --git a/packages/cdk/forms/src/useValueAccessor.ts b/packages/cdk/forms/src/useValueAccessor.ts
new file mode 100644
index 00000000..3043bdf4
--- /dev/null
+++ b/packages/cdk/forms/src/useValueAccessor.ts
@@ -0,0 +1,60 @@
+/* eslint-disable @typescript-eslint/no-explicit-any */
+import type { AbstractControl } from './controls/abstractControl'
+
+import { reactive, getCurrentInstance, watchEffect } from 'vue'
+import { Logger } from '@idux/components/core/logger'
+import { isDevMode } from '@idux/components/core/utils'
+import { isAbstractControl } from './typeof'
+import { injectControl } from './utils'
+
+export interface AccessorOptions {
+  controlKey?: string
+  valueKey?: string
+}
+
+export interface ValueAccessor {
+  value?: any
+  setValue?: (value: any) => void
+  markAsBlurred?: () => void
+}
+
+export function useValueAccessor(options: AccessorOptions = {}): ValueAccessor {
+  const { controlKey = 'control', valueKey = 'value' } = options
+  const accessor = reactive({} as ValueAccessor)
+  const { props, emit } = getCurrentInstance()!
+
+  watchEffect(() => {
+    const controlOrPath = props[controlKey] as string | AbstractControl
+    if (controlOrPath) {
+      let control: AbstractControl | null = null
+      if (isAbstractControl(controlOrPath)) {
+        control = controlOrPath
+      } else {
+        control = injectControl(controlOrPath)
+        if (isDevMode && !control) {
+          Logger.error(`not find control by [${controlOrPath}]`)
+        }
+      }
+      if (control) {
+        accessor.value = control.valueRef
+        accessor.setValue = (value: any) => control!.setValue(value, { dirty: true })
+        accessor.markAsBlurred = () => control!.markAsBlurred()
+      }
+    } else {
+      accessor.setValue = value => {
+        accessor.value = value
+        emit(`update:${valueKey}`, value)
+      }
+      accessor.markAsBlurred = () => {}
+    }
+  })
+
+  watchEffect(() => {
+    const control = props[controlKey]
+    if (!control) {
+      accessor.value = props[valueKey]
+    }
+  })
+
+  return accessor
+}
diff --git a/packages/cdk/utils/dom.ts b/packages/cdk/utils/dom.ts
new file mode 100644
index 00000000..bba0a9a7
--- /dev/null
+++ b/packages/cdk/utils/dom.ts
@@ -0,0 +1,87 @@
+/* istanbul ignore file */
+
+const trim = (s: string) => (s || '').replace(/^[\s\uFEFF]+|[\s\uFEFF]+$/g, '')
+
+export function on(
+  el: HTMLElement | Document | Window,
+  type: string,
+  listener: EventListenerOrEventListenerObject,
+  options?: boolean | EventListenerOptions | undefined,
+): void {
+  if (el && type && listener) {
+    el.addEventListener(type, listener, options ?? true)
+  }
+}
+
+export function off(
+  el: HTMLElement | Document | Window,
+  type: string,
+  listener: EventListenerOrEventListenerObject,
+  options?: boolean | EventListenerOptions | undefined,
+): void {
+  if (el && type && listener) {
+    el.removeEventListener(type, listener, options ?? true)
+  }
+}
+
+export function hasClass(el: HTMLElement, cls: string): boolean {
+  if (!el || !cls) {
+    return false
+  }
+  if (cls.indexOf(' ') !== -1) {
+    throw new Error('className should not contain space.')
+  }
+  if (el.classList) {
+    return el.classList.contains(cls)
+  } else {
+    return ` ${el.className} `.indexOf(` ${cls} `) > -1
+  }
+}
+
+export function addClass(el: HTMLElement, cls: string): void {
+  if (!el) {
+    return
+  }
+  let curClass = el.className
+  const classes = (cls || '').split(' ')
+
+  for (let i = 0, j = classes.length; i < j; i++) {
+    const clsName = classes[i]
+    if (!clsName) {
+      continue
+    }
+
+    if (el.classList) {
+      el.classList.add(clsName)
+    } else if (!hasClass(el, clsName)) {
+      curClass += ' ' + clsName
+    }
+  }
+  if (!el.classList) {
+    el.className = curClass
+  }
+}
+
+export function removeClass(el: HTMLElement, cls: string): void {
+  if (!el || !cls) {
+    return
+  }
+  const classes = cls.split(' ')
+  let curClass = ` ${el.className} `
+
+  for (let i = 0, j = classes.length; i < j; i++) {
+    const clsName = classes[i]
+    if (!clsName) {
+      continue
+    }
+
+    if (el.classList) {
+      el.classList.remove(clsName)
+    } else if (hasClass(el, clsName)) {
+      curClass = curClass.replace(` ${clsName} `, ' ')
+    }
+  }
+  if (!el.classList) {
+    el.className = trim(curClass)
+  }
+}
diff --git a/packages/cdk/utils/index.ts b/packages/cdk/utils/index.ts
index c54a87d9..585328bb 100644
--- a/packages/cdk/utils/index.ts
+++ b/packages/cdk/utils/index.ts
@@ -2,3 +2,4 @@ export * from './convert'
 export * from './propTypes'
 export * from './typeof'
 export * from './vNode'
+export * from './dom'
diff --git a/packages/components/button/style/index.less b/packages/components/button/style/index.less
index e7767ab1..131570bb 100644
--- a/packages/components/button/style/index.less
+++ b/packages/components/button/style/index.less
@@ -5,55 +5,55 @@
 
 .@{button-prefix} {
   line-height: @button-line-height;
-  .button;
-  .button-default;
+  .button();
+  .button-default();
 
   > span {
     display: inline-block;
   }
 
   &-primary {
-    .button-primary;
+    .button-primary();
   }
 
   &-dashed {
-    .button-dashed;
+    .button-dashed();
   }
 
   &-text {
-    .button-text;
+    .button-text();
   }
 
   &-link {
-    .button-link;
+    .button-link();
   }
 
   &-danger {
-    .button-danger;
+    .button-danger();
   }
 
   &-danger&-primary {
-    .button-danger-primary;
+    .button-danger-primary();
   }
 
   &-danger&-text {
-    .button-danger-text;
+    .button-danger-text();
   }
 
   &-danger&-link {
-    .button-danger-link;
+    .button-danger-link();
   }
 
   &-ghost {
-    .button-ghost;
+    .button-ghost();
   }
 
   &-ghost&-primary {
-    .button-ghost-primary;
+    .button-ghost-primary();
   }
 
   &-ghost&-danger {
-    .button-ghost-danger;
+    .button-ghost-danger();
   }
 
   &&-loading {
diff --git a/packages/components/button/style/mixin.less b/packages/components/button/style/mixin.less
index 25cfeebd..7d10585d 100644
--- a/packages/components/button/style/mixin.less
+++ b/packages/components/button/style/mixin.less
@@ -2,7 +2,10 @@
 @button-danger-border: color(~`colorPalette('@{button-danger-bg}', 10) `);
 
 .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);
+  @padding-vertical: max(
+    (round(((@height - @font-size * @button-line-height) / 2) * 10) / 10) - @button-border-size,
+    0
+  );
 
   height: @height;
   padding: @padding-vertical @padding-horizontal;
diff --git a/packages/components/checkbox/demo/Basic.vue b/packages/components/checkbox/demo/Basic.vue
new file mode 100644
index 00000000..4ef8ab93
--- /dev/null
+++ b/packages/components/checkbox/demo/Basic.vue
@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <ix-checkbox v-model:checked="checked"> option </ix-checkbox>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const checked = ref(true)
+
+    return {
+      checked,
+    }
+  },
+})
+</script>
+<style lang="less" scoped></style>
diff --git a/packages/components/checkbox/demo/Disabled.vue b/packages/components/checkbox/demo/Disabled.vue
new file mode 100644
index 00000000..db513bb1
--- /dev/null
+++ b/packages/components/checkbox/demo/Disabled.vue
@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <ix-checkbox v-model:checked="checked" :disabled="disabled" @change="onChange"> option </ix-checkbox>
+    <div class="operation-area">
+      <ix-button mode="primary" @click="changeChecked()">checked: {{ checked }}</ix-button>
+      <ix-button mode="primary" @click="changeDisabled()">disabled: {{ disabled }}</ix-button>
+    </div>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const checked = ref(true)
+    const disabled = ref(true)
+
+    const changeChecked = () => {
+      checked.value = !checked.value
+    }
+
+    const changeDisabled = () => {
+      disabled.value = !disabled.value
+    }
+
+    const onChange = () => {
+      console.log('onChange')
+    }
+
+    return {
+      checked,
+      disabled,
+      changeChecked,
+      changeDisabled,
+      onChange,
+    }
+  },
+})
+</script>
+<style lang="less" scoped>
+.operation-area {
+  margin-top: 30px;
+  .ix-button {
+    margin-right: 10px;
+  }
+}
+</style>
diff --git a/packages/components/checkbox/demo/Group.vue b/packages/components/checkbox/demo/Group.vue
new file mode 100644
index 00000000..a59676dc
--- /dev/null
+++ b/packages/components/checkbox/demo/Group.vue
@@ -0,0 +1,24 @@
+<template>
+  <div>
+    <ix-checkbox-group v-model:value="value">
+      <ix-checkbox value="option1"> option1 </ix-checkbox>
+      <ix-checkbox value="option2"> option2 </ix-checkbox>
+      <ix-checkbox value="option3"> option3 </ix-checkbox>
+    </ix-checkbox-group>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const value = ref(['option1', 'option2', 'option3'])
+
+    return {
+      value,
+    }
+  },
+})
+</script>
+<style lang="less" scoped></style>
diff --git a/packages/components/checkbox/demo/Indeterminate.vue b/packages/components/checkbox/demo/Indeterminate.vue
new file mode 100644
index 00000000..cac0b42d
--- /dev/null
+++ b/packages/components/checkbox/demo/Indeterminate.vue
@@ -0,0 +1,48 @@
+<template>
+  <div>
+    <ix-checkbox v-model:checked="checkAll" :indeterminate="indeterminate" @change="onCheckAllChange">
+      CheckAll
+    </ix-checkbox>
+    <ix-checkbox-group v-model:value="value">
+      <ix-checkbox value="option1"> option1 </ix-checkbox>
+      <ix-checkbox value="option2"> option2 </ix-checkbox>
+      <ix-checkbox value="option3"> option3 </ix-checkbox>
+    </ix-checkbox-group>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref, watch } from 'vue'
+
+const valueList = ['option1', 'option2', 'option3']
+export default defineComponent({
+  setup() {
+    const checkAll = ref(false)
+    const indeterminate = ref(true)
+
+    const value = ref(['option1', 'option2'])
+
+    watch(
+      () => value.value,
+      checkValue => {
+        indeterminate.value = !!checkValue.length && checkValue.length < valueList.length
+        checkAll.value = checkValue.length === valueList.length
+      },
+    )
+
+    const onCheckAllChange = (checkValue: boolean) => {
+      const isCheckAll = checkValue ? valueList : []
+      value.value = isCheckAll
+      indeterminate.value = false
+    }
+
+    return {
+      checkAll,
+      indeterminate,
+      onCheckAllChange,
+      value,
+    }
+  },
+})
+</script>
+<style lang="less" scoped></style>
diff --git a/packages/components/checkbox/demo/TrueFalse.vue b/packages/components/checkbox/demo/TrueFalse.vue
new file mode 100644
index 00000000..08f8f42f
--- /dev/null
+++ b/packages/components/checkbox/demo/TrueFalse.vue
@@ -0,0 +1,20 @@
+<template>
+  <div>
+    <ix-checkbox v-model:checked="checked" trueValue="yes" falseValue="no"> {{ checked }} </ix-checkbox>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, ref } from 'vue'
+
+export default defineComponent({
+  setup() {
+    const checked = ref('yes')
+
+    return {
+      checked,
+    }
+  },
+})
+</script>
+<style lang="less" scoped></style>
diff --git a/packages/components/checkbox/demo/basic.md b/packages/components/checkbox/demo/basic.md
new file mode 100644
index 00000000..5063d6cd
--- /dev/null
+++ b/packages/components/checkbox/demo/basic.md
@@ -0,0 +1,14 @@
+---
+order: 0
+title:
+  zh: 基本使用
+  en: Basic usage
+---
+
+## zh
+
+`ix-checkbox` 的基础用法。
+
+## en
+
+The basic usage of `ix-checkbox`.
diff --git a/packages/components/checkbox/demo/disabled.md b/packages/components/checkbox/demo/disabled.md
new file mode 100644
index 00000000..8e86a17c
--- /dev/null
+++ b/packages/components/checkbox/demo/disabled.md
@@ -0,0 +1,14 @@
+---
+order: 2
+title:
+  zh: Disabled 基本使用
+  en: Disabled usage
+---
+
+## zh
+
+用来控制`ix-checkbox`的禁用状态，与`checked`独立。
+
+## en
+
+It is used to control the disabled state of the checkbox and is independent of checked.
diff --git a/packages/components/checkbox/demo/group.md b/packages/components/checkbox/demo/group.md
new file mode 100644
index 00000000..9bba4e99
--- /dev/null
+++ b/packages/components/checkbox/demo/group.md
@@ -0,0 +1,14 @@
+---
+order: 3
+title:
+  zh: CheckboxGroup 使用
+  en: CheckboxGroup usage
+---
+
+## zh
+
+`ix-checkboxGroup` 基本使用
+
+## en
+
+The basic usage of `ix-checkboxGroup`.
diff --git a/packages/components/checkbox/demo/indeterminate.md b/packages/components/checkbox/demo/indeterminate.md
new file mode 100644
index 00000000..dd733f64
--- /dev/null
+++ b/packages/components/checkbox/demo/indeterminate.md
@@ -0,0 +1,14 @@
+---
+order: 4
+title:
+  zh: indeterminate 使用
+  en: indeterminate usage
+---
+
+## zh
+
+`indeterminate` 属性用以表示 `ix-checkbox` 的不确定状态，用于实现全选的效果。
+
+## en
+
+The attribute `indeterminate` is used to represent the uncertain state of the `ix-checkbox` and to achieve the effect of selecting all.
diff --git a/packages/components/checkbox/demo/trueFalse.md b/packages/components/checkbox/demo/trueFalse.md
new file mode 100644
index 00000000..0885a642
--- /dev/null
+++ b/packages/components/checkbox/demo/trueFalse.md
@@ -0,0 +1,14 @@
+---
+order: 1
+title:
+  zh: trueValue 和 falseValue 使用
+  en: trueValue and falseValue usage
+---
+
+## zh
+
+控制`ix-checkbox`是否选中的返回值。
+
+## en
+
+The return value that controls whether the `ix-checkbox` is selected.
diff --git a/packages/components/checkbox/docs/index.zh.md b/packages/components/checkbox/docs/index.zh.md
new file mode 100644
index 00000000..bcc00744
--- /dev/null
+++ b/packages/components/checkbox/docs/index.zh.md
@@ -0,0 +1,88 @@
+---
+category: components
+type: 通用
+title: Checkbox
+subtitle: 多选框
+cover:
+---
+
+## 何时使用
+
+单独使用可以表示两种状态之间的切换
+在一组可选项中进行多项选择
+
+## API
+
+### `ix-checkbox`
+
+#### Props
+
+除以下表格之外还支持原生 <input type="checkbox" /> 元素的所有属性。
+| 参数 | 说明 |  类型  | 默认值 | 全局配置 | 备注 |
+| --- | --- | --- | --- | --- | --- |
+| `v-model:checked` | 指定当前勾选框是否选中 |  `boolean`  | `false` | - | - |
+| `disabled` | 禁用状态 |`boolean`| `false` | - | - |
+| `indeterminate` | 是否处于不确定状态 | `boolean` | `false`| - | 当值为true时，按钮样式处于半选状态，且不受checked影响 |
+| `trueValue` | 勾选框选中时返回的值 |  `string \| number`  | `true`| - | - |
+| `falseValue` | 勾选框不选中时返回的值 | `string \| number` | `false`| - | - |
+
+#### Slots
+
+|名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+|`defalut` | 文本区域 | - | - |
+
+#### Emits
+
+| 名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+| `change` | 点击勾选框时触发 | `Function(checkValue:boolean \| string \| number)` | - |
+
+### `ix-checkbox-group`
+
+#### Group Props
+
+| 名称 | 说明 | 类型  | 默认值 | 全局配置 | 备注 |
+| --- | --- | --- | --- | --- | --- |value
+| `v-model:value` | 指定当前勾选框是否选中 |  `boolean`  | `false` | - | - |
+| `disabled` | 子`ix-checkbox`禁用状态 | `boolean` | `false` | - |- |
+| `name` | 子`ix-checkbox` 的 name 属性 | `string` | - | - |- |
+
+#### Group Slots
+
+|名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+|`defalut` | `ix-checkbox`区域 | - | - |
+
+#### Group Emits
+
+| 名称 | 说明 | 参数类型 | 备注 |
+| --- | --- | --- | --- |
+| `change` | `value`变化时的回调函数 | `Function(checkValue:string[])` | - |
+
+### 主题变量
+
+| 变量名 | default 主题| 说明 |
+| --- | --- | --- |
+|@checkbox-border-style| @border-size-sm solid @grey-d10| -|
+|@checkbox-border-radius| @border-radius-sm|-|
+|@checkbox-bg-color| @white|-|
+|@checkbox-tick-border-style| @border-size-md solid @white| -|
+|@checkbox-tick-width| 4px|-|
+|@checkbox-tick-height| 8px|-|
+|@checkbox-indeterminate-width| 8px|-|
+|@checkbox-indeterminate-height| 8px|-|
+|@checkbox-bg-indeterminate-color| @primary-color|-|
+|@checkbox-border-checked-color| @primary-color|-|
+|@checkbox-bg-checked-color| @primary-color|-|
+|@checkbox-border-disabled-color| @grey|-|
+|@checkbox-tick-disabled-color| @grey|-|
+|@checkbox-bg-disabled-color| @white|-|
+|@checkbox-bg-indeterminate-disabled-color| @grey|-|
+|@checkbox-label-disabled-color| @grey|-|
+|@checkbox-font-size| @font-size-md|-|
+|@checkbox-group-line-height| @line-height-base|-|
+|@checkbox-height| 16px|-|
+|@checkbox-width| 16px|-|
+|@checkbox-group-checkbox-margin-right| @margin-sm | -|
+|@checkbox-label-padding| 0 @spacing-gutter|-|
diff --git a/packages/components/checkbox/index.ts b/packages/components/checkbox/index.ts
new file mode 100644
index 00000000..fbd32e80
--- /dev/null
+++ b/packages/components/checkbox/index.ts
@@ -0,0 +1,9 @@
+import { installComponent } from '@idux/components/core/utils'
+import IxCheckbox from './src/Checkbox.vue'
+import IxCheckboxGroup from './src/CheckboxGroup.vue'
+
+IxCheckbox.install = installComponent(IxCheckbox)
+IxCheckboxGroup.install = installComponent(IxCheckboxGroup)
+
+export { IxCheckbox, IxCheckboxGroup }
+export * from './src/types'
diff --git a/packages/components/checkbox/src/Checkbox.vue b/packages/components/checkbox/src/Checkbox.vue
new file mode 100644
index 00000000..a9eb21d0
--- /dev/null
+++ b/packages/components/checkbox/src/Checkbox.vue
@@ -0,0 +1,46 @@
+<template>
+  <label
+    class="ix-checkbox"
+    role="checkbox"
+    :ariaChecked="isChecked"
+    :ariaDisabled="isDisabled"
+    :class="[classes, $attrs.class]"
+    :style="$attrs.style"
+  >
+    <span class="ix-checkbox-input-wrapper">
+      <span class="ix-checkbox-inner" :tabindex="$attrs.tabindex"></span>
+      <input
+        type="checkbox"
+        class="ix-checkbox-input"
+        :name="inputName"
+        :checked="isChecked"
+        :disabled="isDisabled"
+        :trueValue="trueValue"
+        :falseValue="falseValue"
+        :value="value"
+        v-bind="attrs"
+        @change="handleChange"
+      />
+    </span>
+    <span v-if="hasDefaultSlot" class="ix-checkbox-label"><slot></slot></span>
+  </label>
+</template>
+<script lang="ts">
+import { defineComponent } from 'vue'
+import { PropTypes, withUndefined } from '@idux/cdk/utils'
+import { setup } from './checkboxSetup'
+export default defineComponent({
+  name: 'IxCheckbox',
+  inheritAttrs: false,
+  props: {
+    checked: withUndefined(PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool])),
+    disabled: PropTypes.bool.def(false),
+    indeterminate: PropTypes.bool.def(false),
+    trueValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).def(true),
+    falseValue: PropTypes.oneOfType([PropTypes.string, PropTypes.number, PropTypes.bool]).def(false),
+    value: PropTypes.string,
+  },
+  emits: ['update:checked', 'change'],
+  setup,
+})
+</script>
diff --git a/packages/components/checkbox/src/CheckboxGroup.vue b/packages/components/checkbox/src/CheckboxGroup.vue
new file mode 100644
index 00000000..bd652bc0
--- /dev/null
+++ b/packages/components/checkbox/src/CheckboxGroup.vue
@@ -0,0 +1,58 @@
+<template>
+  <div class="ix-checkbox-group" role="group" aria-label="checkbox-group">
+    <slot></slot>
+  </div>
+</template>
+
+<script lang="ts">
+import { defineComponent, provide, watch } from 'vue'
+import { PropTypes } from '@idux/cdk/utils'
+import { checkboxGroupInjectionKey, subjectInjectKey, SubjectType } from './checkbox'
+import type { CheckboxGroupProps } from './types'
+import { Subject } from '@idux/cdk/subject'
+
+export default defineComponent({
+  name: 'IxCheckboxGroup',
+  props: {
+    value: PropTypes.arrayOf(PropTypes.string),
+    disabled: PropTypes.bool.def(false),
+    name: PropTypes.string,
+  },
+  emits: ['update:value', 'change'],
+  setup(props: CheckboxGroupProps, { emit }) {
+    provide(checkboxGroupInjectionKey, props)
+
+    const subject = new Subject<SubjectType>()
+    provide(subjectInjectKey, subject)
+
+    let groupCheckValue = props.value ?? []
+
+    watch(
+      () => props.value,
+      value => {
+        groupCheckValue = value as string[]
+      },
+    )
+
+    const handleChange = (checkValue: SubjectType) => {
+      const checkValueIndex = groupCheckValue.indexOf(checkValue)
+      const value = [...groupCheckValue]
+      if (checkValueIndex === -1) {
+        value.push(checkValue)
+      } else {
+        value.splice(checkValueIndex, 1)
+      }
+      // no value props passed
+      if (props.value === void 0) {
+        groupCheckValue = value
+      }
+      emit('update:value', value)
+      emit('change', value)
+    }
+
+    subject.subscribe(handleChange)
+
+    return {}
+  },
+})
+</script>
diff --git a/packages/components/checkbox/src/checkbox.ts b/packages/components/checkbox/src/checkbox.ts
new file mode 100644
index 00000000..e48d976c
--- /dev/null
+++ b/packages/components/checkbox/src/checkbox.ts
@@ -0,0 +1,11 @@
+import type { CheckboxGroupProps } from './types'
+
+import { InjectionKey } from 'vue'
+
+import { Subject } from '@idux/cdk/subject'
+
+export const checkboxGroupInjectionKey: InjectionKey<CheckboxGroupProps> = Symbol()
+
+export type SubjectType = string
+
+export const subjectInjectKey: InjectionKey<Subject<SubjectType>> = Symbol()
diff --git a/packages/components/checkbox/src/checkboxSetup.ts b/packages/components/checkbox/src/checkboxSetup.ts
new file mode 100644
index 00000000..fb1a79ef
--- /dev/null
+++ b/packages/components/checkbox/src/checkboxSetup.ts
@@ -0,0 +1,109 @@
+import { computed, ComputedRef, inject, Ref, ref, watch, getCurrentInstance, SetupContext } from 'vue'
+import { isArray, hasSlot } from '@idux/cdk/utils'
+import { useAttrs } from '@idux/components/core/utils/'
+import { checkboxGroupInjectionKey, subjectInjectKey } from './checkbox'
+import type { CheckboxProps, CheckboxBindings } from './types'
+
+type CheckValue = number | string | boolean
+
+export const setup = (props: CheckboxProps, { slots }: SetupContext): CheckboxBindings => {
+  const hasDefaultSlot = computed(() => hasSlot(slots))
+
+  const isDisabled = useDisabled()
+
+  const { checkeValue, handleChange } = useCheckValue()
+
+  const isChecked = useChecked(checkeValue as Ref<CheckValue>)
+
+  const classes = useClasses(isDisabled, isChecked)
+
+  const inputName = useName()
+
+  const attrs = useAttrs({ keys: ['type', 'tabindex'] })
+
+  return {
+    isChecked,
+    isDisabled,
+    hasDefaultSlot,
+    handleChange,
+    classes,
+    inputName,
+    attrs,
+  }
+}
+
+const useCheckValue = () => {
+  //eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
+  const { props, emit } = getCurrentInstance()!
+  const groupSubject = inject(subjectInjectKey, null)
+  const checkeValue = ref(props.checked ?? props.falseValue)
+
+  watch(
+    () => props.checked,
+    v => {
+      checkeValue.value = v
+    },
+  )
+
+  const handleChange = (e: Event) => {
+    const targetChecked = (e.target as EventTarget).checked
+    const targetCheckValue = targetChecked ? props.trueValue : props.falseValue
+    // no value props passed
+    if (props.checked === void 0) {
+      checkeValue.value = targetCheckValue
+    }
+    emit('update:checked', targetCheckValue)
+    emit('change', targetCheckValue)
+    groupSubject?.dispatch((props.value ?? targetCheckValue) as string)
+  }
+
+  return {
+    checkeValue,
+    handleChange,
+  }
+}
+
+const useChecked = (checkValue: Ref<CheckValue>) => {
+  const groupProps = inject(checkboxGroupInjectionKey, {})
+  //eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
+  const { props } = getCurrentInstance()!
+
+  return computed(() => {
+    const groupValue = groupProps.value
+    if (groupValue && isArray(groupValue)) {
+      return groupValue.includes((props.value ?? checkValue.value) as string)
+    }
+
+    return checkValue.value === props.trueValue
+  })
+}
+
+const useDisabled = () => {
+  const groupProps = inject(checkboxGroupInjectionKey, {})
+  //eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
+  const { props } = getCurrentInstance()!
+  return computed(() => {
+    return !!(groupProps.disabled || props.disabled)
+  })
+}
+
+const useClasses = (isDisabled: ComputedRef<boolean>, isChecked: ComputedRef<boolean>) => {
+  //eslint-disable-next-line  @typescript-eslint/no-non-null-assertion
+  const { props } = getCurrentInstance()!
+
+  return computed(() => {
+    const disabled = isDisabled.value
+    const checked = isChecked.value
+    const indeterminate = props.indeterminate
+    return {
+      'ix-checkbox-disabled': !!disabled,
+      'ix-checkbox-indeterminate': !!indeterminate,
+      'ix-checkbox-checked': !indeterminate && checked,
+    }
+  })
+}
+
+const useName = () => {
+  const groupProps = inject(checkboxGroupInjectionKey, {})
+  return computed(() => groupProps.name as string)
+}
diff --git a/packages/components/checkbox/src/types.ts b/packages/components/checkbox/src/types.ts
new file mode 100644
index 00000000..319cb5e2
--- /dev/null
+++ b/packages/components/checkbox/src/types.ts
@@ -0,0 +1,38 @@
+import type { ComputedRef, DefineComponent, Ref } from 'vue'
+
+interface CheckboxOriginalProps {
+  checked?: string | number | boolean
+  disabled?: boolean
+  indeterminate?: boolean
+  trueValue?: string | number | boolean
+  falseValue?: string | number | boolean
+  value?: string
+}
+
+interface CheckboxGroupOriginalProps {
+  value?: string[]
+  disabled?: boolean
+  name?: string
+}
+
+export interface CheckboxBindings {
+  isChecked: ComputedRef<boolean>
+  isDisabled: ComputedRef<boolean>
+  hasDefaultSlot: ComputedRef<boolean>
+  handleChange: (e: Event) => void
+  classes: ComputedRef<{
+    'ix-checkbox-disabled': boolean
+    'ix-checkbox-indeterminate': boolean
+    'ix-checkbox-checked': boolean
+  }>
+  inputName: ComputedRef<string>
+  attrs: Ref<Record<string, unknown>>
+}
+
+export type CheckboxProps = Readonly<CheckboxOriginalProps>
+
+export type CheckboxGroupProps = Readonly<CheckboxGroupOriginalProps>
+
+export type CheckboxComponent = InstanceType<DefineComponent<CheckboxProps, CheckboxBindings>>
+
+export type CheckboxGroupComponent = InstanceType<DefineComponent<CheckboxGroupProps>>
diff --git a/packages/components/checkbox/style/index.less b/packages/components/checkbox/style/index.less
new file mode 100644
index 00000000..7c405f61
--- /dev/null
+++ b/packages/components/checkbox/style/index.less
@@ -0,0 +1,142 @@
+@import '../../style/default.less';
+
+@checkbox-prefix: ~'@{idux-prefix}-checkbox';
+@checkbox-group-prefix: ~'@{idux-prefix}-checkbox-group';
+
+.@{checkbox-prefix} {
+  box-sizing: border-box;
+  font-size: @checkbox-font-size;
+  line-height: 1;
+  position: relative;
+  display: inline-block;
+  white-space: nowrap;
+  vertical-align: middle;
+  outline: none;
+  cursor: pointer;
+
+  &-input-wrapper {
+    box-sizing: border-box;
+    list-style: none;
+    position: relative;
+    display: inline-block;
+    white-space: nowrap;
+    vertical-align: middle;
+    outline: none;
+    cursor: pointer;
+  }
+
+  &-inner {
+    display: inline-block;
+    position: relative;
+    border: @checkbox-border-style;
+    border-radius: @checkbox-border-radius;
+    box-sizing: border-box;
+    width: @checkbox-width;
+    height: @checkbox-height;
+    background-color: @checkbox-bg-color;
+    z-index: 1;
+    transition: background-color ease .25s;
+
+    &::after {
+      position: absolute;
+      top: 50%;
+      left: 22%;
+      display: table;
+      width: @checkbox-tick-width;
+      height: @checkbox-tick-height;
+      border: @checkbox-tick-border-style;
+      border-top: 0;
+      border-left: 0;
+      transform: rotate(45deg) scale(0) translate(-50%, -50%);
+      transition: transform .25s ease;
+      opacity: 0;
+      content: " ";
+    }
+  }
+
+  &-input {
+    opacity: 0;
+    outline: none;
+    position: absolute;
+    width: 0;
+    height: 0;
+    z-index: -1;
+  }
+
+  &-label {
+    padding: @checkbox-label-padding;
+  }
+
+  &-checked {
+    .@{checkbox-prefix} {
+      &-inner {
+        background-color: @checkbox-bg-checked-color;
+        border-color: @checkbox-border-checked-color;
+
+        &::after {
+          opacity: 1;
+          transform: rotate(45deg) scale(1) translate(-50%, -50%);
+        }
+      }
+    }
+  }
+
+  &-indeterminate {
+    .@{checkbox-prefix} {
+      &-inner {
+
+        &::after {
+          left: 50%;
+          width: @checkbox-indeterminate-width;
+          height: @checkbox-indeterminate-height;
+          background-color: @checkbox-bg-indeterminate-color;
+          border: 0;
+          transform: translate(-50%, -50%) scale(1);
+          opacity: 1;
+          content: " "
+        }
+      }
+    }
+  }
+
+  &-disabled {
+    &.@{checkbox-prefix}-checked {
+      .@{checkbox-prefix}-inner::after {
+        border-color: @checkbox-tick-disabled-color;
+      }
+    }
+
+    &.@{checkbox-prefix}-indeterminate {
+      .@{checkbox-prefix}-inner::after {
+        background-color: @checkbox-bg-indeterminate-disabled-color;
+      }
+    }
+
+    .@{checkbox-prefix} {
+      &-inner {
+        background-color: @checkbox-bg-disabled-color;
+        border-color: @checkbox-border-disabled-color !important;
+      }
+
+      &-input {
+        cursor: not-allowed;
+      }
+
+      &-label {
+        color: @checkbox-label-disabled-color;
+        cursor: not-allowed;
+      }
+    }
+  }
+}
+
+.@{checkbox-group-prefix} {
+  box-sizing: border-box;
+  line-height: @checkbox-group-line-height;
+  list-style: none;
+  display: inline-block;
+
+  .@{checkbox-prefix} {
+    margin-right: @checkbox-group-checkbox-margin-right;
+  }
+}
diff --git a/packages/components/components.less b/packages/components/components.less
index df3b059a..4167dc8e 100644
--- a/packages/components/components.less
+++ b/packages/components/components.less
@@ -9,3 +9,4 @@
 @import './result/style/index.less';
 @import "./typography/style/index.less";
 @import './rate/style/index.less';
+@import './checkbox/style/index.less';
diff --git a/packages/components/core/utils/index.ts b/packages/components/core/utils/index.ts
index ed661eff..b6a32d54 100644
--- a/packages/components/core/utils/index.ts
+++ b/packages/components/core/utils/index.ts
@@ -1,2 +1,3 @@
 export * from './installComponent'
 export * from './isDevMode'
+export * from './useAttrs'
diff --git a/packages/components/index.ts b/packages/components/index.ts
index 8352b13a..a2b96bab 100644
--- a/packages/components/index.ts
+++ b/packages/components/index.ts
@@ -10,6 +10,7 @@ import { IxEmpty } from './empty'
 import { IxResult } from './result'
 import { IxTypography } from './typography'
 import { IxRate } from './rate'
+import { IxCheckbox, IxCheckboxGroup } from './checkbox'
 
 const components = [
   IxButton,
@@ -23,6 +24,8 @@ const components = [
   IxEmpty,
   IxResult,
   IxRate,
+  IxCheckbox,
+  IxCheckboxGroup,
 ]
 
 const directives: Record<string, Directive> = {
@@ -59,3 +62,4 @@ export { IxEmpty }
 export { IxResult }
 export { IxTypography }
 export { IxRate }
+export { IxCheckbox, IxCheckboxGroup }
diff --git a/packages/components/spin/demo/Size.vue b/packages/components/spin/demo/Size.vue
index 97be9fea..b8873387 100644
--- a/packages/components/spin/demo/Size.vue
+++ b/packages/components/spin/demo/Size.vue
@@ -35,7 +35,7 @@ export default defineComponent({
 
 .operation-area {
   margin-top: 30px;
-  /deep/.ix-button {
+  .ix-button {
     margin-right: 4px;
   }
 }
diff --git a/packages/components/style/default.less b/packages/components/style/default.less
index 159118d8..bf7ee429 100644
--- a/packages/components/style/default.less
+++ b/packages/components/style/default.less
@@ -165,3 +165,26 @@
 @rate-icon-margin: 8px;
 @rate-stroke-color: #f7ba2a;
 @rate-background-color: #c6d1de;
+/* checkbox -------------------------- */
+@checkbox-border-style: @border-size-sm solid @grey-d10;
+@checkbox-border-radius: @border-radius-sm;
+@checkbox-bg-color: @white;
+@checkbox-tick-border-style: @border-size-md solid @white;
+@checkbox-tick-width: 4px;
+@checkbox-tick-height: 8px;
+@checkbox-bg-indeterminate-color: @primary-color;
+@checkbox-indeterminate-width: 8px;
+@checkbox-indeterminate-height: 8px;
+@checkbox-border-checked-color: @primary-color;
+@checkbox-bg-checked-color: @primary-color;
+@checkbox-border-disabled-color: @grey;
+@checkbox-tick-disabled-color: @grey;
+@checkbox-bg-disabled-color: @white;
+@checkbox-bg-indeterminate-disabled-color: @grey;
+@checkbox-label-disabled-color: @grey;
+@checkbox-font-size: @font-size-md;
+@checkbox-height: 16px;
+@checkbox-width: 16px;
+@checkbox-group-line-height: @line-height-base;
+@checkbox-group-checkbox-margin-right: @margin-sm;
+@checkbox-label-padding: 0 @spacing-gutter;
\ No newline at end of file


Test patch:
diff --git a/packages/cdk/forms/__tests__/useValueAccessor.spec.ts b/packages/cdk/forms/__tests__/useValueAccessor.spec.ts
new file mode 100644
index 00000000..dbe23d34
--- /dev/null
+++ b/packages/cdk/forms/__tests__/useValueAccessor.spec.ts
@@ -0,0 +1,143 @@
+import { flushPromises, mount } from '@vue/test-utils'
+import { ComponentOptions, ref, Ref } from 'vue'
+import { AbstractControl } from '../src/controls/abstractControl'
+import { FormControl } from '../src/controls/formControl'
+import { useFormControl, useFormGroup } from '../src/useForms'
+import { useValueAccessor } from '../src/useValueAccessor'
+import { provideControl } from '../src/utils'
+
+const getInputComponent = () => {
+  return {
+    template: `<input
+    :value="valueAccessor.value"
+    @input="onInput"
+    @blur="onBlur" />
+    `,
+    props: ['value', 'control'],
+    emits: ['update:value'],
+    setup() {
+      const valueAccessor = useValueAccessor()
+      const onInput = (evt: Event) => {
+        valueAccessor.setValue?.((evt.target as HTMLInputElement).value)
+      }
+      const onBlur = () => {
+        valueAccessor.markAsBlurred?.()
+      }
+      return { valueAccessor, onInput, onBlur }
+    },
+  }
+}
+
+const getApp = (
+  Comp: ComponentOptions,
+  options: {
+    group: AbstractControl
+    valueRef?: Ref<string>
+    control?: string | AbstractControl
+  },
+) => {
+  return mount({
+    components: { Comp },
+    template: `<Comp v-model:value="valueRef" :control="control"/>`,
+    setup() {
+      const { group, valueRef, control } = options
+      provideControl(group)
+      return { valueRef, control }
+    },
+  })
+}
+
+describe('useValueAccessor.ts', () => {
+  let valueRef: Ref<string>
+  let control: FormControl
+  let group: AbstractControl
+
+  beforeEach(() => {
+    valueRef = ref('valueRef')
+    control = useFormControl('control')
+    group = useFormGroup({ control })
+  })
+
+  test('valueRef work', async () => {
+    const wrapper = getApp(getInputComponent(), { group, valueRef })
+    const input = wrapper.find('input')
+
+    expect(input.element.value).toEqual('valueRef')
+
+    valueRef.value = 'valueRef change'
+    await flushPromises()
+
+    expect(input.element.value).toEqual('valueRef change')
+
+    await input.setValue('input change')
+
+    expect(valueRef.value).toEqual('input change')
+
+    await input.trigger('blur')
+
+    expect(group.getValue()).toEqual({ control: 'control' })
+    expect(group.blurred.value).toEqual(false)
+  })
+
+  test('control work', async () => {
+    const wrapper = getApp(getInputComponent(), { group, control, valueRef })
+    const input = wrapper.find('input')
+
+    expect(input.element.value).toEqual('control')
+
+    control.setValue('control change')
+    await flushPromises()
+
+    expect(input.element.value).toEqual('control change')
+
+    await input.setValue('input change')
+
+    expect(control.getValue()).toEqual('input change')
+
+    await input.trigger('blur')
+
+    expect(group.getValue()).toEqual({ control: 'input change' })
+    expect(group.blurred.value).toEqual(true)
+    expect(valueRef.value).toEqual('valueRef')
+  })
+
+  test('string control work', async () => {
+    const wrapper = getApp(getInputComponent(), { group, control: 'control', valueRef })
+    const input = wrapper.find('input')
+
+    expect(input.element.value).toEqual('control')
+
+    control.setValue('control change')
+    await flushPromises()
+
+    expect(input.element.value).toEqual('control change')
+
+    await input.setValue('input change')
+
+    expect(control.getValue()).toEqual('input change')
+
+    await input.trigger('blur')
+
+    expect(group.getValue()).toEqual({ control: 'input change' })
+    expect(group.blurred.value).toEqual(true)
+    expect(valueRef.value).toEqual('valueRef')
+  })
+
+  test('not find control work', async () => {
+    const error = jest.spyOn(console, 'error').mockImplementation(() => {})
+    const wrapper = getApp(getInputComponent(), { group, control: 'not find', valueRef })
+
+    expect(error).toBeCalled()
+
+    const input = wrapper.find('input')
+
+    expect(input.element.value).toEqual('')
+
+    await input.setValue('input change')
+    await input.trigger('blur')
+
+    expect(group.getValue()).toEqual({ control: 'control' })
+    expect(group.blurred.value).toEqual(false)
+    expect(valueRef.value).toEqual('valueRef')
+  })
+})
diff --git a/packages/components/checkbox/__tests__/__snapshots__/checkbox.spec.ts.snap b/packages/components/checkbox/__tests__/__snapshots__/checkbox.spec.ts.snap
new file mode 100644
index 00000000..c047ff4a
--- /dev/null
+++ b/packages/components/checkbox/__tests__/__snapshots__/checkbox.spec.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`Checkbox.vue render work 1`] = `"<label class=\\"ix-checkbox\\" role=\\"checkbox\\" ariachecked=\\"false\\" ariadisabled=\\"false\\"><span class=\\"ix-checkbox-input-wrapper\\"><span class=\\"ix-checkbox-inner\\"></span><input type=\\"checkbox\\" class=\\"ix-checkbox-input\\" truevalue=\\"true\\" falsevalue=\\"false\\" value=\\"\\"></span><span class=\\"ix-checkbox-label\\">option</span></label>"`;
diff --git a/packages/components/checkbox/__tests__/__snapshots__/checkboxGroup.spec.ts.snap b/packages/components/checkbox/__tests__/__snapshots__/checkboxGroup.spec.ts.snap
new file mode 100644
index 00000000..39e0d9c1
--- /dev/null
+++ b/packages/components/checkbox/__tests__/__snapshots__/checkboxGroup.spec.ts.snap
@@ -0,0 +1,3 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`CheckboxGroup.vue and Checkbox.vue render work 1`] = `"<div class=\\"ix-checkbox-group\\" role=\\"group\\" aria-label=\\"checkbox-group\\"><label class=\\"ix-checkbox\\" role=\\"checkbox\\" ariachecked=\\"false\\" ariadisabled=\\"false\\"><span class=\\"ix-checkbox-input-wrapper\\"><span class=\\"ix-checkbox-inner\\"></span><input type=\\"checkbox\\" class=\\"ix-checkbox-input\\" truevalue=\\"true\\" falsevalue=\\"false\\" value=\\"option1\\"></span><span class=\\"ix-checkbox-label\\"> option1 </span></label><label class=\\"ix-checkbox\\" role=\\"checkbox\\" ariachecked=\\"false\\" ariadisabled=\\"false\\"><span class=\\"ix-checkbox-input-wrapper\\"><span class=\\"ix-checkbox-inner\\"></span><input type=\\"checkbox\\" class=\\"ix-checkbox-input\\" truevalue=\\"true\\" falsevalue=\\"false\\" value=\\"option2\\"></span><span class=\\"ix-checkbox-label\\"> option2 </span></label><label class=\\"ix-checkbox\\" role=\\"checkbox\\" ariachecked=\\"false\\" ariadisabled=\\"false\\"><span class=\\"ix-checkbox-input-wrapper\\"><span class=\\"ix-checkbox-inner\\"></span><input type=\\"checkbox\\" class=\\"ix-checkbox-input\\" truevalue=\\"true\\" falsevalue=\\"false\\" value=\\"option3\\"></span><span class=\\"ix-checkbox-label\\"> option3 </span></label></div>"`;
diff --git a/packages/components/checkbox/__tests__/checkbox.spec.ts b/packages/components/checkbox/__tests__/checkbox.spec.ts
new file mode 100644
index 00000000..d9d1cbe5
--- /dev/null
+++ b/packages/components/checkbox/__tests__/checkbox.spec.ts
@@ -0,0 +1,164 @@
+import { mount } from '@vue/test-utils'
+import { ref, nextTick } from 'vue'
+import IxCheckbox from '../src/Checkbox.vue'
+
+describe('Checkbox.vue', () => {
+  test('render work', () => {
+    const checked = ref(false)
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked">option</ix-checkbox>`,
+      setup() {
+        return { checked }
+      },
+    })
+    expect(wrapper.html()).toMatchSnapshot()
+  })
+
+  test('checked(v-model) work', async () => {
+    const checked = ref(false)
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked">option</ix-checkbox>`,
+      setup() {
+        return { checked }
+      },
+    })
+
+    expect(wrapper.find('.ix-checkbox-checked').exists()).toBe(false)
+
+    checked.value = true
+
+    await nextTick()
+    expect(wrapper.find('.ix-checkbox-checked').exists()).toBe(true)
+
+    await wrapper.trigger('click')
+
+    expect(checked.value).toBe(false)
+    expect(wrapper.find('.ix-checkbox-checked').exists()).toBe(false)
+  })
+
+  test('no checked(v-model) work', async () => {
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox>option</ix-checkbox>`,
+    })
+
+    expect(wrapper.find('.ix-checkbox-checked').exists()).toBe(false)
+
+    await wrapper.trigger('click')
+
+    expect(wrapper.find('.ix-checkbox-checked').exists()).toBe(true)
+  })
+
+  test('trueValue and falseValue work', async () => {
+    const checked = ref('yes')
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked" trueValue="yes" falseValue="no">option</ix-checkbox>`,
+      setup() {
+        return { checked }
+      },
+    })
+
+    await wrapper.trigger('click')
+
+    expect(checked.value).toBe('no')
+  })
+
+  test('disabled work', async () => {
+    const checked = ref(true)
+    const disabled = ref(true)
+    const mockFn = jest.fn()
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked" :disabled="disabled" @change="mockFn">option</ix-checkbox>`,
+      setup() {
+        return { checked, disabled, mockFn }
+      },
+    })
+
+    expect(wrapper.classes()).toContain('ix-checkbox-disabled')
+
+    await wrapper.trigger('click')
+
+    expect(wrapper.classes()).toContain('ix-checkbox-disabled')
+    expect(checked.value).toBe(true)
+    expect(mockFn).toBeCalledTimes(0)
+
+    disabled.value = false
+
+    await nextTick()
+
+    expect(wrapper.classes()).not.toContain('ix-checkbox-disabled')
+
+    await wrapper.trigger('click')
+
+    expect(checked.value).toBe(false)
+    expect(mockFn).toBeCalledTimes(1)
+  })
+
+  test('indeterminate work', async () => {
+    const checked = ref(true)
+    const indeterminate = ref(true)
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked" :indeterminate="indeterminate">option</ix-checkbox>`,
+      setup() {
+        return { checked, indeterminate }
+      },
+    })
+
+    expect(wrapper.classes()).toContain('ix-checkbox-indeterminate')
+    expect(wrapper.classes()).not.toContain('ix-checkbox-checked')
+
+    indeterminate.value = false
+
+    await nextTick()
+
+    expect(wrapper.classes()).not.toContain('ix-checkbox-indeterminate')
+    expect(wrapper.classes()).toContain('ix-checkbox-checked')
+  })
+
+  test('change event work', async () => {
+    const checked = ref(true)
+    const mockFn = jest.fn()
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox v-model:checked="checked" @change="mockFn">option</ix-checkbox>`,
+      setup() {
+        return { checked, mockFn }
+      },
+    })
+
+    expect(mockFn).toBeCalledTimes(0)
+
+    await wrapper.trigger('click')
+
+    expect(mockFn).toBeCalledTimes(1)
+  })
+
+  test('original checkbox attributes work', async () => {
+    const wrapper = mount({
+      components: { IxCheckbox },
+      template: `<ix-checkbox name="checkboxName" value="checkboxValue" tabindex="1" class="checkox" style="color: red;">option</ix-checkbox>`,
+    })
+
+    const checkboxWrapper = wrapper.find('.ix-checkbox')
+    const originalInput = wrapper.find("input[type='checkbox']")
+    const input = wrapper.find('.ix-checkbox-inner')
+
+    expect(checkboxWrapper.classes()).toContain('checkox')
+    expect(checkboxWrapper.attributes()['style']).toEqual('color: red;')
+    expect(checkboxWrapper.attributes()['name']).not.toBeDefined()
+    expect(checkboxWrapper.attributes()['value']).not.toBeDefined()
+    expect(checkboxWrapper.attributes()['tabindex']).not.toBeDefined()
+
+    expect(originalInput.attributes()['name']).toEqual('checkboxName')
+    expect(originalInput.attributes()['value']).toEqual('checkboxValue')
+    expect(originalInput.classes()).not.toContain('checkox')
+    expect(originalInput.attributes()['style']).not.toEqual('color: red;')
+
+    expect(input.attributes()['tabindex']).toEqual('1')
+  })
+})
diff --git a/packages/components/checkbox/__tests__/checkboxGroup.spec.ts b/packages/components/checkbox/__tests__/checkboxGroup.spec.ts
new file mode 100644
index 00000000..1334c28d
--- /dev/null
+++ b/packages/components/checkbox/__tests__/checkboxGroup.spec.ts
@@ -0,0 +1,148 @@
+import { mount } from '@vue/test-utils'
+import CheckboxGroup from '../src/CheckboxGroup.vue'
+import Checkbox from '../src/Checkbox.vue'
+import { nextTick, Ref, ref } from 'vue'
+
+describe('CheckboxGroup.vue and Checkbox.vue', () => {
+  test('render work', () => {
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group>
+        <checkbox value="option1"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+    })
+    expect(wrapper.findAll('.ix-checkbox').length).toBe(3)
+    expect(wrapper.html()).toMatchSnapshot()
+  })
+
+  test('value(v-model) work', async () => {
+    const value = ref([]) as Ref<string[]>
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group v-model:value="value">
+        <checkbox value="option1"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+      setup() {
+        return { value }
+      },
+    })
+
+    expect(wrapper.findAll('.ix-checkbox-checked').length).toBe(0)
+
+    value.value = ['option1']
+
+    await nextTick()
+
+    expect(wrapper.findAllComponents({ name: 'IxCheckbox' })[0].classes()).toContain('ix-checkbox-checked')
+
+    await wrapper.findAllComponents({ name: 'IxCheckbox' })[0].trigger('click')
+
+    expect(value.value).toEqual([])
+  })
+
+  test('no value(v-model) work', async () => {
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group>
+        <checkbox value="option1"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+      setup() {
+        return {}
+      },
+    })
+
+    expect(wrapper.findAll('.ix-checkbox-checked').length).toBe(0)
+
+    await wrapper.findAllComponents({ name: 'IxCheckbox' })[0].trigger('click')
+
+    expect(wrapper.findAllComponents({ name: 'IxCheckbox' })[0].classes()).toContain('ix-checkbox-checked')
+  })
+
+  test('change event work', async () => {
+    const mockFn = jest.fn()
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group @change="mockFn">
+        <checkbox value="option1"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+      setup() {
+        return { mockFn }
+      },
+    })
+
+    expect(mockFn).toBeCalledTimes(0)
+
+    await wrapper.findAllComponents({ name: 'IxCheckbox' })[0].trigger('click')
+
+    expect(mockFn).toBeCalledTimes(1)
+  })
+
+  test('disabled work', async () => {
+    const value = ref([]) as Ref<string[]>
+    const disabled = ref(true)
+    const mockFn = jest.fn()
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group v-model:value="value" :disabled="disabled" @change="mockFn">
+        <checkbox value="option1"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+      setup() {
+        return { value, disabled, mockFn }
+      },
+    })
+
+    expect(wrapper.findAll('.ix-checkbox-disabled').length).toBe(3)
+    await wrapper.findAllComponents({ name: 'IxCheckbox' })[0].trigger('click')
+    expect(value.value).toEqual([])
+    expect(mockFn).toBeCalledTimes(0)
+
+    disabled.value = false
+
+    await nextTick()
+
+    expect(wrapper.findAll('.ix-checkbox-disabled').length).toBe(0)
+
+    await wrapper.findAllComponents({ name: 'IxCheckbox' })[0].trigger('click')
+    expect(value.value).toEqual(['option1'])
+    expect(mockFn).toBeCalledTimes(1)
+  })
+
+  test('name work', async () => {
+    const wrapper = mount({
+      components: { CheckboxGroup, Checkbox },
+      template: `
+      <checkbox-group name="group">
+        <checkbox value="option1" name="child"> option1 </checkbox>
+        <checkbox value="option2"> option2 </checkbox>
+        <checkbox value="option3"> option3 </checkbox>
+      </checkbox-group>
+      `,
+      setup() {
+        return {}
+      },
+    })
+
+    expect(wrapper.findAll('input[name=group]').length).toBe(2)
+    expect(wrapper.findAll('input[name=child]').length).toBe(1)
+  })
+})