This document outlines the steps to create Input
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// input.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 inputVariants = 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// input.component.tsx
2import clsx from "clsx";
3import { useEffect, useRef, useState } from "react";
4import type { VariantProps } from "class-variance-authority";
5import {
6 errorMessageVariants,
7 inputVariants,
8 labelVariants,
9} from "./input.helpers";
10
11export interface InputProps
12 extends React.InputHTMLAttributes<HTMLInputElement>,
13 VariantProps<typeof inputVariants> {
14 label?: string;
15 isFloatLabel?: boolean;
16 error?: string | null;
17 wrapperClassName?: string;
18}
19
20const Input: React.FC<InputProps> = ({
21 inputSize,
22 label,
23 isFloatLabel,
24 error,
25 placeholder,
26 className,
27 wrapperClassName,
28 onFocus,
29 onBlur,
30 onChange,
31 ...props
32}) => {
33 const inputRef = useRef<HTMLInputElement | null>(null);
34 const [hasValue, setHasValue] = useState<boolean>(false);
35 const [focusing, setFocusing] = useState<boolean>(false);
36
37 useEffect(() => {
38 const defaultValue = inputRef.current!.defaultValue;
39 setHasValue(defaultValue.trim() !== "");
40 }, []);
41
42 const handleFocus = (e: React.FocusEvent<HTMLInputElement, Element>) => {
43 setFocusing(true);
44 onFocus && onFocus(e);
45 };
46
47 const handleBlur = (e: React.FocusEvent<HTMLInputElement, Element>) => {
48 setFocusing(false);
49 onBlur && onBlur(e);
50 };
51
52 const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
53 setHasValue(e.target.value.trim() !== "");
54 onChange && onChange(e);
55 };
56
57 const handleClickLabel = () => {
58 if (!isFloatLabel || focusing) return;
59 inputRef.current!.focus();
60 };
61
62 const isFloatingLabel = isFloatLabel && !focusing && !hasValue;
63
64 return (
65 <div className={cn("flex flex-col", wrapperClassName)}>
66 {label && (
67 <label
68 data-float={Boolean(isFloatLabel).toString()}
69 data-floating={Boolean(isFloatingLabel).toString()}
70 data-invalid={Boolean(error).toString()}
71 className={labelVariants({ inputSize })}
72 onClick={handleClickLabel}
73 >
74 {label}
75 </label>
76 )}
77 <input
78 {...props}
79 ref={inputRef}
80 placeholder={!isFloatLabel ? placeholder : undefined}
81 data-invalid={Boolean(error).toString()}
82 className={cn(inputVariants({ inputSize }), className)}
83 onFocus={handleFocus}
84 onBlur={handleBlur}
85 onChange={handleChange}
86 />
87 {error && <p className={errorMessageVariants({ inputSize })}>{error}</p>}
88 </div>
89 );
90};
91
92export default Input;
93