This document outlines the steps to create Textarea
component styled withTailwind CSS and using some npm dependency libraries.
Node.js
and npm
installed on your machine.Tailwind CSS
installed in your project.CVA(class-variance-authority)
is a utility for managing CSS class names based on various conditions.clsx
is a tiny utility for constructing className strings conditionally.1// textarea.helpers.ts
2import { cva } from "class-variance-authority";
3
4export const labelVariants = cva(
5 "mb-1 transition-all duration-150 transform translate-y-0 data-[invalid=true]:text-red-500",
6 {
7 variants: {
8 inputSize: {
9 small:
10 "text-xs font-semibold data-[float=true]:pl-1.5 data-[floating=true]:translate-y-[1.6rem]",
11 medium:
12 "text-sm font-semibold data-[float=true]:pl-2 data-[floating=true]:translate-y-[2.1rem]",
13 large:
14 "text-lg font-medium data-[float=true]:pl-4 data-[floating=true]:translate-y-[2.55rem]",
15 },
16 },
17 defaultVariants: {
18 inputSize: "medium",
19 },
20 },
21);
22
23export const textareaVariants = cva(
24 [
25 "rounded-md border focus:border-neutral-600 focus:outline-none text-sm disabled:bg-neutral-300 disabled:text-neutral-400 dark:disabled:bg-neutral-600 dark:disabled:text-neutral-500",
26 "border-neutral-300 dark:border-neutral-500 data-[invalid=true]:!border-red-500",
27 ],
28 {
29 variants: {
30 inputSize: {
31 small: "px-2 py-1 text-xs",
32 medium: "px-3 py-2 text-sm",
33 large: "px-4 py-2 text-lg",
34 },
35 },
36 defaultVariants: {
37 inputSize: "medium",
38 },
39 },
40);
41
42export const errorMessageVariants = cva("text-red-500 font-medium mt-1", {
43 variants: {
44 inputSize: {
45 small: "text-xs",
46 medium: "text-xs",
47 large: "text-base",
48 },
49 },
50 defaultVariants: {
51 inputSize: "medium",
52 },
53});
54
1// textarea.component.tsx
2import clsx from "clsx";
3import { useEffect, useRef, useState } from "react";
4import {
5 errorMessageVariants,
6 labelVariants,
7 textareaVariants,
8} from "./textarea.helpers";
9
10const Textarea: React.FC<TextareaProps> = ({
11 inputSize,
12 label,
13 isFloatLabel,
14 error,
15 placeholder,
16 className,
17 wrapperClassName,
18 onFocus,
19 onBlur,
20 onChange,
21 ...props
22}) => {
23 const inputRef = useRef<HTMLTextAreaElement | null>(null);
24 const [hasValue, setHasValue] = useState<boolean>(false);
25 const [focusing, setFocusing] = useState<boolean>(false);
26
27 useEffect(() => {
28 const defaultValue = inputRef.current!.defaultValue;
29 setHasValue(defaultValue.trim() !== "");
30 }, []);
31
32 const handleFocus = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {
33 setFocusing(true);
34 onFocus && onFocus(e);
35 };
36
37 const handleBlur = (e: React.FocusEvent<HTMLTextAreaElement, Element>) => {
38 setFocusing(false);
39 onBlur && onBlur(e);
40 };
41
42 const handleChange = (e: React.ChangeEvent<HTMLTextAreaElement>) => {
43 setHasValue(e.target.value.trim() !== "");
44 onChange && onChange(e);
45 };
46
47 const handleClickLabel = () => {
48 if (!isFloatLabel || focusing) return;
49 inputRef.current!.focus();
50 };
51
52 const isFloatingLabel = isFloatLabel && !focusing && !hasValue;
53
54 return (
55 <div className={clsx("flex flex-col", wrapperClassName)}>
56 {label && (
57 <label
58 data-float={Boolean(isFloatLabel).toString()}
59 data-floating={Boolean(isFloatingLabel).toString()}
60 data-invalid={Boolean(error).toString()}
61 className={labelVariants({ inputSize })}
62 onClick={handleClickLabel}
63 >
64 {label}
65 </label>
66 )}
67 <textarea
68 {...props}
69 ref={inputRef}
70 placeholder={!isFloatLabel ? placeholder : undefined}
71 data-invalid={Boolean(error).toString()}
72 className={clsx(textareaVariants({ inputSize }), className)}
73 onFocus={handleFocus}
74 onBlur={handleBlur}
75 onChange={handleChange}
76 />
77 {error && <p className={errorMessageVariants({ inputSize })}>{error}</p>}
78 </div>
79 );
80};
81
82export default Textarea;
83