diff --git a/__tests__/Input.test.js b/__tests__/Input.test.js
new file mode 100644
index 0000000..2eda163
--- /dev/null
+++ b/__tests__/Input.test.js
@@ -0,0 +1,68 @@
+import React from 'react'
+import { mount } from 'enzyme'
+import Input from '../src/Input'
+
+describe('Input', () => {
+ it('should render without crashing', () => {
+ mount()
+ })
+
+ it('should render with base styles', () => {
+ const expected =
+ 'block w-full text-sm dark:border-gray-600 focus:outline-none dark:text-gray-300 form-input'
+ const wrapper = mount()
+
+ expect(wrapper.find('input').getDOMNode().getAttribute('class')).toContain(expected)
+ })
+
+ it('should render with active styles', () => {
+ const expected =
+ 'focus:border-purple-400 focus:shadow-outline-purple dark:focus:border-gray-600 dark:focus:shadow-outline-gray dark:bg-gray-700'
+ const wrapper = mount()
+
+ expect(wrapper.find('input').getDOMNode().getAttribute('class')).toContain(expected)
+ })
+
+ it('should render with disabled styles', () => {
+ const expected = 'cursor-not-allowed opacity-50 bg-gray-300 dark:bg-gray-800'
+ const wrapper = mount()
+
+ expect(wrapper.find('input').getDOMNode().getAttribute('class')).toContain(expected)
+ })
+
+ it('should render with valid styles', () => {
+ const expected =
+ 'border-green-600 dark:bg-gray-700 focus:border-green-400 focus:shadow-outline-green'
+ const wrapper = mount()
+
+ expect(wrapper.find('input').getDOMNode().getAttribute('class')).toContain(expected)
+ })
+
+ it('should render with invalid styles', () => {
+ const expected =
+ 'border-red-600 dark:text-gray-300 dark:bg-gray-700 focus:border-red-400 focus:shadow-outline-red'
+ const wrapper = mount()
+
+ expect(wrapper.find('input').getDOMNode().getAttribute('class')).toContain(expected)
+ })
+
+ it('should render with radio styles', () => {
+ const expected =
+ 'text-purple-600 form-radio focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray'
+ const wrapper = mount()
+
+ expect(wrapper.find('input[type="radio"]').getDOMNode().getAttribute('class')).toContain(
+ expected
+ )
+ })
+
+ it('should render with checkbox styles', () => {
+ const expected =
+ 'text-purple-600 form-checkbox focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray'
+ const wrapper = mount()
+
+ expect(wrapper.find('input[type="checkbox"]').getDOMNode().getAttribute('class')).toContain(
+ expected
+ )
+ })
+})
diff --git a/src/Input.js b/src/Input.js
new file mode 100644
index 0000000..bd4a766
--- /dev/null
+++ b/src/Input.js
@@ -0,0 +1,65 @@
+import React, { useContext } from 'react'
+import classNames from 'classnames'
+import PropTypes from 'prop-types'
+import { ThemeContext } from './context/ThemeContext'
+import defaultTheme from './themes/default'
+
+const Input = React.forwardRef(function Input(props, ref) {
+ const { valid, disabled, className, type, ...other } = props
+
+ const { input } = useContext(ThemeContext) || defaultTheme
+
+ const baseStyle = input.base
+ const activeStyle = input.active
+ const disabledStyle = input.disabled
+ const validStyle = input.valid
+ const invalidStyle = input.invalid
+ const radioStyle = input.radio
+ const checkStyle = input.checkbox
+
+ function hasValidation(valid) {
+ return valid !== undefined
+ }
+
+ function validationStyle(valid) {
+ if (hasValidation(valid)) {
+ return valid ? validStyle : invalidStyle
+ }
+ }
+
+ function typeStyle(type) {
+ switch (type) {
+ case 'radio':
+ return radioStyle
+ case 'checkbox':
+ return checkStyle
+ default:
+ return baseStyle
+ }
+ }
+
+ const cls = classNames(
+ typeStyle(type),
+ // don't apply activeStyle if has valid or disabled
+ !hasValidation(valid) && !disabled && activeStyle,
+ // don't apply disabledStyle if has valid
+ !hasValidation(valid) && disabled && disabledStyle,
+ validationStyle(valid),
+ className
+ )
+
+ return
+})
+
+Input.propTypes = {
+ type: PropTypes.string,
+ disabled: PropTypes.bool,
+ valid: PropTypes.bool,
+ className: PropTypes.string,
+}
+
+Input.defaultProps = {
+ type: 'text',
+}
+
+export default Input
diff --git a/src/index.js b/src/index.js
index c6373a5..2e867f5 100644
--- a/src/index.js
+++ b/src/index.js
@@ -2,3 +2,4 @@ export { default as Button } from './Button'
export { default as Card } from './Card'
export { default as CardBody } from './CardBody'
export { default as HelperText } from './HelperText'
+export { default as Input } from './Input'
diff --git a/src/themes/default.js b/src/themes/default.js
index 9571dcd..f981569 100644
--- a/src/themes/default.js
+++ b/src/themes/default.js
@@ -1,4 +1,19 @@
export default {
+ // Input
+ input: {
+ base:
+ 'block w-full text-sm dark:border-gray-600 focus:outline-none dark:text-gray-300 form-input',
+ active:
+ 'focus:border-purple-400 focus:shadow-outline-purple dark:focus:border-gray-600 dark:focus:shadow-outline-gray dark:bg-gray-700',
+ disabled: 'cursor-not-allowed opacity-50 bg-gray-300 dark:bg-gray-800',
+ valid: 'border-green-600 dark:bg-gray-700 focus:border-green-400 focus:shadow-outline-green',
+ invalid:
+ 'border-red-600 dark:text-gray-300 dark:bg-gray-700 focus:border-red-400 focus:shadow-outline-red',
+ radio:
+ 'text-purple-600 form-radio focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray',
+ checkbox:
+ 'text-purple-600 form-checkbox focus:border-purple-400 focus:outline-none focus:shadow-outline-purple dark:focus:shadow-outline-gray',
+ },
// HelperText
helperText: {
base: 'text-xs',