diff --git a/src/components/base/textarea/Textarea.vue b/src/components/base/textarea/Textarea.vue
new file mode 100644
index 00000000..51339ab4
--- /dev/null
+++ b/src/components/base/textarea/Textarea.vue
@@ -0,0 +1,73 @@
+
+
+
+
+
+
+
diff --git a/src/components/base/textarea/__tests__/Textarea.spec.ts b/src/components/base/textarea/__tests__/Textarea.spec.ts
new file mode 100644
index 00000000..eadbbd12
--- /dev/null
+++ b/src/components/base/textarea/__tests__/Textarea.spec.ts
@@ -0,0 +1,70 @@
+import { describe, expect, it, vi } from "vitest";
+import { VTextarea } from "../index";
+import { mount } from "@vue/test-utils";
+
+describe("Textarea", () => {
+ it("should render", function () {
+ expect(VTextarea).toBeDefined();
+ expect(mount(VTextarea).html()).toMatchSnapshot();
+ });
+
+ it("should work with placeholder prop", async function () {
+ const textarea = mount(VTextarea);
+ expect(
+ textarea.find("textarea").attributes()["placeholder"]
+ ).toBeUndefined();
+
+ // set placeholder prop
+ const placeholderText = "Please enter your text";
+ await textarea.setProps({ placeholder: placeholderText });
+ expect(textarea.find("textarea").attributes()["placeholder"]).toBe(
+ placeholderText
+ );
+ });
+
+ it("should work with disabled prop", async function () {
+ const textarea = mount(VTextarea);
+ expect(textarea.find("textarea").attributes()["disabled"]).toBeUndefined();
+
+ // set disabled prop
+ await textarea.setProps({ disabled: true });
+ expect(textarea.find("textarea").attributes()["disabled"]).toBeDefined();
+ });
+
+ it("should work with rows prop", function () {
+ const textarea = mount(VTextarea, {
+ propsData: {
+ rows: 5,
+ },
+ });
+ expect(textarea.find("textarea").attributes()["rows"]).toBe("5");
+ });
+
+ it("should work with v-model", async function () {
+ const wrapper = mount({
+ data() {
+ return {
+ value: "my name is ryanwang",
+ };
+ },
+ template: `
+
+ `,
+ components: { VTextarea },
+ });
+
+ expect(wrapper.find("textarea").element.value).toBe("my name is ryanwang");
+
+ // change value
+ await wrapper.setData({
+ value: "my name is ryanwang, my website is https://ryanc.cc",
+ });
+ expect(wrapper.find("textarea").element.value).toBe(
+ "my name is ryanwang, my website is https://ryanc.cc"
+ );
+
+ // change modelValue by textarea element value
+ await wrapper.find("textarea").setValue("my name is ryanwang");
+ expect(wrapper.vm.$data.value).toBe("my name is ryanwang");
+ });
+});
diff --git a/src/components/base/textarea/__tests__/__snapshots__/Textarea.spec.ts.snap b/src/components/base/textarea/__tests__/__snapshots__/Textarea.spec.ts.snap
new file mode 100644
index 00000000..bd7fea5c
--- /dev/null
+++ b/src/components/base/textarea/__tests__/__snapshots__/Textarea.spec.ts.snap
@@ -0,0 +1,6 @@
+// Vitest Snapshot v1
+
+exports[`Textarea > should render 1`] = `
+"
"
+`;
diff --git a/src/components/base/textarea/index.ts b/src/components/base/textarea/index.ts
new file mode 100644
index 00000000..ebf6c0ae
--- /dev/null
+++ b/src/components/base/textarea/index.ts
@@ -0,0 +1 @@
+export { default as VTextarea } from "./Textarea.vue";
diff --git a/src/views/ViewComponents.vue b/src/views/ViewComponents.vue
index 400424b4..12b6e7ef 100644
--- a/src/views/ViewComponents.vue
+++ b/src/views/ViewComponents.vue
@@ -77,6 +77,19 @@
+
+ Textarea
+
+
+
+ Disabled:
+
+
+
+
Select
Size:
@@ -113,6 +126,7 @@ import { FilledLayout } from "../layouts";
import { VButton } from "@/components/base/button";
import { VInput } from "@/components/base/input";
import { VSelect, VOption } from "@/components/base/select";
+import { VTextarea } from "@/components/base/textarea";
import { ref } from "vue";
const inputValue = ref();