diff --git a/packages/react/src/components/Forms/TextArea/index.scss b/packages/react/src/components/Forms/TextArea/index.scss
index 86e75a4..e10c821 100644
--- a/packages/react/src/components/Forms/TextArea/index.scss
+++ b/packages/react/src/components/Forms/TextArea/index.scss
@@ -1,3 +1,14 @@
+.c__textarea__label {
+ display: block;
+ font-size: var(--c--components--forms-labelledbox--classic-label-font-size);
+ color: var(--c--components--forms-labelledbox--label-color--small);
+ margin-bottom: var(--c--components--forms-labelledbox--classic-label-margin-bottom);
+
+ &--disabled {
+ color: var(--c--components--forms-labelledbox--label-color--small--disabled);
+ }
+}
+
.c__field--textarea {
width: inherit;
min-width: var(--c--components--forms-field--width);
@@ -75,6 +86,13 @@
border-color: var(--c--contextuals--border--semantic--neutral--tertiary);
}
}
+
+ &--classic {
+ .c__textarea {
+ padding-top: 0.5rem;
+ padding-bottom: 0.5rem;
+ }
+ }
}
.c__field {
diff --git a/packages/react/src/components/Forms/TextArea/index.spec.tsx b/packages/react/src/components/Forms/TextArea/index.spec.tsx
index 72f93a9..98095a0 100644
--- a/packages/react/src/components/Forms/TextArea/index.spec.tsx
+++ b/packages/react/src/components/Forms/TextArea/index.spec.tsx
@@ -199,4 +199,104 @@ describe("", () => {
document.querySelector(".c__field--textarea.my-custom-class"),
).toBeInTheDocument();
});
+
+ describe("classic variant", () => {
+ it("renders with classic variant", () => {
+ render();
+ // In classic mode, label is rendered outside the wrapper with its own class
+ expect(document.querySelector(".c__textarea__label")).toBeInTheDocument();
+ expect(screen.getByText("Description")).toBeInTheDocument();
+ });
+
+ it("label is always static in classic variant", async () => {
+ const user = userEvent.setup();
+ render(
+
+
+
+
,
+ );
+
+ const textarea: HTMLTextAreaElement = screen.getByRole("textbox", {
+ name: "Description",
+ });
+ const label = screen.getByText("Description");
+
+ // In classic variant, label is outside the wrapper and has c__textarea__label class
+ expect(label.classList.contains("c__textarea__label")).toBe(true);
+
+ // Focusing should not change anything
+ await user.click(textarea);
+ expect(label.classList.contains("c__textarea__label")).toBe(true);
+
+ // Typing should not change anything
+ await user.type(textarea, "Some text");
+ expect(label.classList.contains("c__textarea__label")).toBe(true);
+ });
+
+ it("shows placeholder in classic variant", () => {
+ render(
+ ,
+ );
+ const textarea: HTMLTextAreaElement = screen.getByRole("textbox", {
+ name: "Description",
+ });
+ expect(textarea.placeholder).toEqual("Enter a description");
+ });
+
+ it("ignores placeholder in floating variant", () => {
+ render(
+ ,
+ );
+ const textarea: HTMLTextAreaElement = screen.getByRole("textbox", {
+ name: "Description",
+ });
+ expect(textarea.placeholder).toEqual("");
+ });
+
+ it("defaults to floating variant (placeholder ignored)", () => {
+ render(
+ ,
+ );
+ const textarea: HTMLTextAreaElement = screen.getByRole("textbox", {
+ name: "Description",
+ });
+ expect(textarea.placeholder).toEqual("");
+ expect(
+ document.querySelector(".c__textarea__label"),
+ ).not.toBeInTheDocument();
+ });
+ });
+
+ describe("hideLabel", () => {
+ it("hides label visually but keeps it accessible in floating variant", () => {
+ render();
+ const textarea = screen.getByRole("textbox", { name: "Description" });
+ expect(textarea).toBeInTheDocument();
+ // Label should be visually hidden via LabelledBox
+ const label = screen.getByText("Description");
+ expect(label.closest("label")).toHaveClass("c__offscreen");
+ });
+
+ it("hides label visually but keeps it accessible in classic variant", () => {
+ render();
+ const textarea = screen.getByRole("textbox", { name: "Description" });
+ expect(textarea).toBeInTheDocument();
+ // Label should be visually hidden with c__offscreen class
+ const label = screen.getByText("Description");
+ expect(label).toHaveClass("c__offscreen");
+ // The visible label class should not be present
+ expect(
+ document.querySelector(".c__textarea__label"),
+ ).not.toBeInTheDocument();
+ });
+ });
});
diff --git a/packages/react/src/components/Forms/TextArea/index.stories.tsx b/packages/react/src/components/Forms/TextArea/index.stories.tsx
index 1f4758b..1520c49 100644
--- a/packages/react/src/components/Forms/TextArea/index.stories.tsx
+++ b/packages/react/src/components/Forms/TextArea/index.stories.tsx
@@ -10,6 +10,12 @@ export default {
args: {
rows: 4,
},
+ argTypes: {
+ variant: {
+ control: "select",
+ options: ["floating", "classic"],
+ },
+ },
} as Meta;
export const ShowCase = () => {
@@ -175,6 +181,60 @@ export const WithRef = () => {
);
};
+export const ClassicVariant = {
+ args: {
+ label: "Description",
+ variant: "classic",
+ placeholder: "Enter a description...",
+ },
+};
+
+export const ClassicVariantFilled = {
+ args: {
+ label: "Description",
+ variant: "classic",
+ placeholder: "Enter a description...",
+ defaultValue: "This is a detailed description of the project.",
+ },
+};
+
+export const ClassicVariantDisabled = {
+ args: {
+ label: "Description",
+ variant: "classic",
+ placeholder: "Enter a description...",
+ disabled: true,
+ },
+};
+
+export const ClassicVariantError = {
+ args: {
+ label: "Description",
+ variant: "classic",
+ placeholder: "Enter a description...",
+ defaultValue: "Too short",
+ state: "error",
+ text: "Description must be at least 50 characters",
+ },
+};
+
+export const HiddenLabel = {
+ args: {
+ label: "Notes",
+ hideLabel: true,
+ placeholder: "Add your notes here...",
+ },
+};
+
+export const HiddenLabelClassic = {
+ args: {
+ label: "Notes",
+ variant: "classic",
+ hideLabel: true,
+ placeholder: "Add your notes here...",
+ },
+};
+
export const FormExample = () => {
return (
diff --git a/packages/react/src/components/Forms/TextArea/index.tsx b/packages/react/src/components/Forms/TextArea/index.tsx
index dc0861a..88fe97e 100644
--- a/packages/react/src/components/Forms/TextArea/index.tsx
+++ b/packages/react/src/components/Forms/TextArea/index.tsx
@@ -8,18 +8,24 @@ import React, {
import classNames from "classnames";
import { Field, FieldProps } from ":/components/Forms/Field";
import { LabelledBox } from ":/components/Forms/LabelledBox";
+import { ClassicLabel } from ":/components/Forms/ClassicLabel";
import { randomString } from ":/utils";
+import type { FieldVariant } from ":/components/Forms/types";
export type TextAreaProps = TextareaHTMLAttributes
&
RefAttributes &
FieldProps & {
label?: string;
+ variant?: FieldVariant;
+ hideLabel?: boolean;
charCounter?: boolean;
charCounterMax?: number;
};
export const TextArea = ({
label,
+ variant = "floating",
+ hideLabel,
id,
defaultValue,
charCounter,
@@ -27,6 +33,7 @@ export const TextArea = ({
ref,
...props
}: TextAreaProps) => {
+ const isClassic = variant === "classic";
const areaRef = useRef(null);
const [inputFocus, setInputFocus] = useState(false);
const [value, setValue] = useState(defaultValue || props.value || "");
@@ -55,56 +62,78 @@ export const TextArea = ({
const { fullWidth, rightText, text, textItems, className, ...areaProps } =
props;
+ const textareaElement = (
+