Most React Tailwind login templates suffer from duplicated code, poor state management, or missing TypeScript support. In this guide, you’ll get:
✅ A TypeScript-first implementation for type safety
✅ Reusable Input and Button components to slash redundancy
✅ Production-ready features like loaders, error handling, and simulated API calls
To make the component scalable and modular, this login page relies on three components:
You can find the code for all these components below.
<Input />
: Handles labels, errors, and styling<Button />
: Supports loaders and dynamic states<DummyLogo />
: Placeholder logoMimic API calls with setTimeout
and display user-friendly errors.
Strongly typed props for inputs and buttons prevent runtime errors.
src/
├─ components/
│ ├─ Input.tsx # Reusable input component
│ └─ Button.tsx # Button with loader
└─ LoginPage.tsx # Main form UI
1import { ChangeEvent, FC } from 'react' 2 3interface InputProps { 4 type: 'text' | 'number' | 'email' | 'password' 5 label?: string 6 value: string | number 7 name: string 8 placeholder: string 9 error?: string 10 disabled?: boolean 11 onChange: (e: ChangeEvent<HTMLInputElement>) => void 12 icon?: React.ReactNode 13} 14 15const Input: React.FC<InputProps> = ({ 16 type, 17 name, 18 disabled, 19 placeholder, 20 label, 21 value, 22 onChange, 23 error, 24 icon, 25 ...props 26}) => { 27 return ( 28 <div className="mb-6"> 29 {label && ( 30 <label htmlFor={label} className="mb-1.5 block text-sm font-medium text-[#344054]"> 31 {label} 32 </label> 33 )} 34 <div className="relative flex items-center"> 35 {icon && <span className="absolute left-3 text-[#667085]">{icon}</span>} 36 <input 37 type={type} 38 name={name} 39 id={label} 40 placeholder={placeholder} 41 value={value} 42 onChange={onChange} 43 disabled={disabled} 44 className={`w-full rounded-lg border border-[#D0D5DD] px-4 py-2.5 pl-10 text-gray-700 placeholder:text-[#667085] focus:border-blue-200 focus:outline-none focus:ring-2 focus:ring-blue-200 ${ 45 error && 'ring-2 ring-red-200' 46 }`} 47 {...props} 48 /> 49 </div> 50 {error && <p className="ml-3 mt-1 block text-sm text-red-600">{error}</p>} 51 </div> 52 ) 53} 54 55export default Input
What It Does:
1import { LoaderCircle } from 'lucide-react' 2import React from 'react' 3 4type ButtonProps = { 5 text: string 6 loading?: boolean 7 disabled?: boolean 8} 9 10const Button: React.FC<ButtonProps> = ({ text, loading = false, disabled }) => { 11 return ( 12 <button 13 className="w-full cursor-pointer rounded-lg border border-neutral-800 bg-neutral-800 px-4 py-2 text-white hover:border-gray-700 hover:bg-gray-900 disabled:cursor-not-allowed disabled:bg-gray-300 disabled:text-gray-500" 14 type="submit" 15 disabled={disabled} 16 > 17 {!loading ? ( 18 text 19 ) : ( 20 <LoaderCircle className="inline-block animate-spin text-center" color="#fff" /> 21 )} 22 </button> 23 ) 24} 25 26export default Button
What It Does:
1const DummyLogo = () => ( 2 <div className="mb-4 flex justify-center"> 3 <span className="text-3xl font-bold text-yellow-500">⚡</span> 4 </div> 5)
What It Does:
Feel free to use this code in your project.
Build better and faster UIs.Get the latest Tailwind UI components directly in your inbox. No spam!