diff --git a/playground/app.vue b/playground/app.vue
index 0542cd01..6a7119aa 100644
--- a/playground/app.vue
+++ b/playground/app.vue
@@ -27,6 +27,7 @@ const components = [
'slideover',
'switch',
'tabs',
+ 'textarea',
'tooltip'
]
diff --git a/playground/pages/textarea.vue b/playground/pages/textarea.vue
new file mode 100644
index 00000000..7cfe291f
--- /dev/null
+++ b/playground/pages/textarea.vue
@@ -0,0 +1,34 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/runtime/components/Textarea.vue b/src/runtime/components/Textarea.vue
new file mode 100644
index 00000000..d9ef06d5
--- /dev/null
+++ b/src/runtime/components/Textarea.vue
@@ -0,0 +1,175 @@
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/theme/index.ts b/src/theme/index.ts
index 54f796db..7f9689ed 100644
--- a/src/theme/index.ts
+++ b/src/theme/index.ts
@@ -20,3 +20,4 @@ export { default as slideover } from './slideover'
export { default as switch } from './switch'
export { default as tabs } from './tabs'
export { default as tooltip } from './tooltip'
+export { default as textarea } from './textarea'
diff --git a/src/theme/textarea.ts b/src/theme/textarea.ts
new file mode 100644
index 00000000..e0470f24
--- /dev/null
+++ b/src/theme/textarea.ts
@@ -0,0 +1,63 @@
+export default (config: { colors: string[] }) => ({
+ slots: {
+ root: 'relative',
+ base: 'relative block w-full disabled:cursor-not-allowed disabled:opacity-75 focus:outline-none border-0 rounded-md placeholder-gray-400 dark:placeholder-gray-500'
+ },
+
+ variants: {
+ size: {
+ '2xs': {
+ base: 'text-xs gap-x-1 px-2 py-1'
+ },
+ xs: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1.5'
+ },
+ sm: {
+ base: 'text-sm gap-x-1.5 px-2.5 py-1.5'
+ },
+ md: {
+ base: 'text-sm gap-x-1.5 px-3 py-2'
+ },
+ lg: {
+ base: 'text-sm gap-x-2.5 px-3.5 py-2.5'
+ },
+ xl: {
+ base: 'text-base gap-x-2.5 px-3.5 py-2.5'
+ }
+ },
+
+ variant: {
+ outline: '',
+ none: 'bg-transparent focus:ring-0 focus:shadow-none'
+ },
+
+ color: {
+ ...Object.fromEntries(config.colors.map((color: string) => [color, ''])),
+ white: '',
+ gray: ''
+ }
+
+ },
+
+ compoundVariants: [
+ ...config.colors.map((color: string) => ({
+ color,
+ variant: 'outline',
+ class: `shadow-sm bg-transparent text-gray-900 dark:text-white ring ring-inset ring-${color}-500 dark:ring-${color}-400 focus:ring-2 focus:ring-${color}-500 dark:focus:ring-${color}-400`
+ })), {
+ color: 'white',
+ variant: 'outline',
+ class: 'shadow-sm bg-white dark:bg-gray-900 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }, {
+ color: 'gray',
+ variant: 'outline',
+ class: 'shadow-sm bg-gray-50 dark:bg-gray-800 text-gray-900 dark:text-white ring ring-inset ring-gray-300 dark:ring-gray-700 focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400'
+ }
+ ],
+
+ defaultVariants: {
+ size: 'sm',
+ color: 'white',
+ variant: 'outline'
+ }
+})
diff --git a/test/components/Textarea.spec.ts b/test/components/Textarea.spec.ts
new file mode 100644
index 00000000..3a0dd437
--- /dev/null
+++ b/test/components/Textarea.spec.ts
@@ -0,0 +1,60 @@
+import { describe, it, expect, test } from 'vitest'
+import Textarea, { type TextareaProps } from '../../src/runtime/components/Textarea.vue'
+import ComponentRender from '../component-render'
+import { mount } from '@vue/test-utils'
+
+describe('Textarea', () => {
+ it.each([
+ ['basic case', {}],
+ ['with id', { props: { id: 'exampleId' } }],
+ ['with name', { props: { name: 'exampleName' } }],
+ ['with placeholder', { props: { placeholder: 'examplePlaceholder' } }],
+ ['with required', { props: { required: true } }],
+ ['with disabled', { props: { disabled: true } }],
+ ['with rows', { props: { rows: 5 } }],
+ ['with size', { props: { size: 'sm' } }],
+ ['with color', { props: { color: 'blue' } }],
+ ['with size 2xs', { props: { size: '2xs' as const } }],
+ ['with size xs', { props: { size: 'xs' as const } }],
+ ['with size sm', { props: { size: 'sm' as const } }],
+ ['with size md', { props: { size: 'md' as const } }],
+ ['with size lg', { props: { size: 'lg' as const } }],
+ ['with size xl', { props: { size: 'xl' as const } }],
+ ['with variant', { variant: 'outline' }],
+ ['with default slot', { slots: { default: () => 'Default slot' } }]
+ // @ts-ignore
+ ])('renders %s correctly', async (nameOrHtml: string, options: { props: TextareaProps, slots?: any }) => {
+ const html = await ComponentRender(nameOrHtml, options, Textarea)
+ expect(html).toMatchSnapshot()
+ })
+
+ it.each([
+ ['with .trim modifier', { props: { modelModifiers: { trim: true } } }, { input: 'input ', expected: 'input' } ],
+ ['with .number modifier', { props: { modelModifiers: { number: true } } }, { input: '42', expected: 42 } ],
+ ['with .lazy modifier', { props: { modelModifiers: { lazy: true } } }, { input: 'input', expected: 'input' } ]
+ ])('%s works', async (_nameOrHtml: string, options: { props?: any, slots?: any }, spec: { input: any, expected: any }) => {
+ const wrapper = await mount(Textarea, {
+ ...options
+ })
+
+ const input = wrapper.find('textarea')
+ await input.setValue(spec.input)
+
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [[spec.expected]] })
+ })
+
+ test('with .lazy modifier updates on change only', async () => {
+ const wrapper = mount(Textarea, {
+ props: {
+ modelModifiers: { lazy: true }
+ }
+ })
+
+ const input = wrapper.find('textarea')
+ await input.trigger('update')
+ expect(wrapper.emitted()).toMatchObject({ })
+
+ await input.trigger('change')
+ expect(wrapper.emitted()).toMatchObject({ 'update:modelValue': [['']] })
+ })
+})
diff --git a/test/components/__snapshots__/Textarea.spec.ts.snap b/test/components/__snapshots__/Textarea.spec.ts.snap
new file mode 100644
index 00000000..d733bafe
--- /dev/null
+++ b/test/components/__snapshots__/Textarea.spec.ts.snap
@@ -0,0 +1,35 @@
+// Vitest Snapshot v1, https://vitest.dev/guide/snapshot.html
+
+exports[`Textarea > renders basic case correctly 1`] = `"
"`;
+
+exports[`Textarea > renders with color correctly 1`] = `""`;
+
+exports[`Textarea > renders with default slot correctly 1`] = `"Default slot
"`;
+
+exports[`Textarea > renders with disabled correctly 1`] = `""`;
+
+exports[`Textarea > renders with id correctly 1`] = `""`;
+
+exports[`Textarea > renders with name correctly 1`] = `""`;
+
+exports[`Textarea > renders with placeholder correctly 1`] = `""`;
+
+exports[`Textarea > renders with required correctly 1`] = `""`;
+
+exports[`Textarea > renders with rows correctly 1`] = `""`;
+
+exports[`Textarea > renders with size 2xs correctly 1`] = `""`;
+
+exports[`Textarea > renders with size correctly 1`] = `""`;
+
+exports[`Textarea > renders with size lg correctly 1`] = `""`;
+
+exports[`Textarea > renders with size md correctly 1`] = `""`;
+
+exports[`Textarea > renders with size sm correctly 1`] = `""`;
+
+exports[`Textarea > renders with size xl correctly 1`] = `""`;
+
+exports[`Textarea > renders with size xs correctly 1`] = `""`;
+
+exports[`Textarea > renders with variant correctly 1`] = `""`;