diff --git a/components/button-group/__tests__/__snapshots__/index.test.tsx.snap b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap
new file mode 100644
index 0000000..ae8ca85
--- /dev/null
+++ b/components/button-group/__tests__/__snapshots__/index.test.tsx.snap
@@ -0,0 +1,493 @@
+// Jest Snapshot v1, https://goo.gl/fbAQLP
+
+exports[`ButtonGroup buttons should be displayed vertically 1`] = `
+"
"
+`;
+
+exports[`ButtonGroup props should be passed to each button 1`] = `
+""
+`;
+
+exports[`ButtonGroup props should be passed to each button 2`] = `
+""
+`;
+
+exports[`ButtonGroup should render correctly 1`] = `
+""
+`;
diff --git a/components/button-group/__tests__/index.test.tsx b/components/button-group/__tests__/index.test.tsx
new file mode 100644
index 0000000..d85eda9
--- /dev/null
+++ b/components/button-group/__tests__/index.test.tsx
@@ -0,0 +1,53 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import { ButtonGroup, Button } from 'components'
+import { nativeEvent } from 'tests/utils'
+
+describe('ButtonGroup', () => {
+ it('should render correctly', () => {
+ const wrapper = mount(
+
+
+ ,
+ )
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('props should be passed to each button', () => {
+ const wrapper = mount(
+
+
+ ,
+ )
+ expect(wrapper.html()).toMatchSnapshot()
+ wrapper.setProps({ ghost: true })
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+
+ it('should ignore events when group disabled', () => {
+ const handler = jest.fn()
+ const wrapper = mount(
+
+
+ ,
+ )
+ wrapper.find('button').simulate('click', nativeEvent)
+ expect(handler).toHaveBeenCalledTimes(1)
+ wrapper.setProps({ disabled: true })
+ wrapper.find('button').simulate('click', nativeEvent)
+ expect(handler).toHaveBeenCalledTimes(1)
+ })
+
+ it('buttons should be displayed vertically', () => {
+ const wrapper = mount(
+
+
+
+ ,
+ )
+ expect(wrapper.html()).toMatchSnapshot()
+ expect(() => wrapper.unmount()).not.toThrow()
+ })
+})
diff --git a/components/button-group/button-group-context.ts b/components/button-group/button-group-context.ts
new file mode 100644
index 0000000..a9c9329
--- /dev/null
+++ b/components/button-group/button-group-context.ts
@@ -0,0 +1,20 @@
+import React from 'react'
+import { NormalSizes, ButtonTypes } from '../utils/prop-types'
+
+export interface ButtonGroupConfig {
+ size?: NormalSizes
+ type?: ButtonTypes
+ ghost?: boolean
+ disabled?: boolean
+ isButtonGroup: boolean
+}
+
+const defaultContext = {
+ isButtonGroup: false,
+ disabled: false,
+}
+
+export const ButtonGroupContext = React.createContext(defaultContext)
+
+export const useButtonGroupContext = (): ButtonGroupConfig =>
+ React.useContext(ButtonGroupContext)
diff --git a/components/button-group/button-group.tsx b/components/button-group/button-group.tsx
new file mode 100644
index 0000000..867ebda
--- /dev/null
+++ b/components/button-group/button-group.tsx
@@ -0,0 +1,116 @@
+import React, { useMemo } from 'react'
+import useTheme from '../styles/use-theme'
+import withDefaults from '../utils/with-defaults'
+import { NormalSizes, ButtonTypes } from '../utils/prop-types'
+import { ButtonGroupContext, ButtonGroupConfig } from './button-group-context'
+import { getButtonColors } from '../button/styles'
+
+interface Props {
+ disabled?: boolean
+ vertical?: boolean
+ ghost?: boolean
+ size?: NormalSizes
+ type?: ButtonTypes
+ className?: string
+}
+
+const defaultProps = {
+ disabled: false,
+ vertical: false,
+ ghost: false,
+ size: 'medium' as NormalSizes,
+ type: 'default' as ButtonTypes,
+ className: '',
+}
+
+type NativeAttrs = Omit, keyof Props>
+export type ButtonGroupProps = Props & typeof defaultProps & NativeAttrs
+
+const ButtonGroup: React.FC> = ({
+ disabled,
+ size,
+ type,
+ ghost,
+ vertical,
+ children,
+ className,
+}) => {
+ const theme = useTheme()
+ const initialValue = useMemo(
+ () => ({
+ disabled,
+ size,
+ type,
+ ghost,
+ isButtonGroup: true,
+ }),
+ [disabled, size, type],
+ )
+
+ const { border } = useMemo(() => {
+ const results = getButtonColors(theme, type, disabled, ghost)
+ if (!ghost && type !== 'default')
+ return {
+ ...results,
+ border: theme.palette.background,
+ }
+ return results
+ }, [theme, type, disabled, ghost])
+
+ return (
+
+
+ {children}
+
+
+
+ )
+}
+
+const MemoButtonGroup = React.memo(ButtonGroup)
+
+export default withDefaults(MemoButtonGroup, defaultProps)
diff --git a/components/button-group/index.ts b/components/button-group/index.ts
new file mode 100644
index 0000000..cc398ed
--- /dev/null
+++ b/components/button-group/index.ts
@@ -0,0 +1,3 @@
+import ButtonGroup from './button-group'
+
+export default ButtonGroup
diff --git a/components/button/__tests__/__snapshots__/icon.test.tsx.snap b/components/button/__tests__/__snapshots__/icon.test.tsx.snap
index 07c3288..cdaf903 100644
--- a/components/button/__tests__/__snapshots__/icon.test.tsx.snap
+++ b/components/button/__tests__/__snapshots__/icon.test.tsx.snap
@@ -1,7 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`ButtonIcon should render correctly 1`] = `
-"