Search

Ctrl + K

Input

This document outlines the steps to create Input component styled withTailwind CSS and using some npm dependency libraries.

Prerequisites

  • 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.
Step 1: Create variant styles
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
Step 2: Create Input component
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