Skip to main content

Overview

MicroCBM provides specialized form input components designed for specific data entry scenarios:
  • PhoneInput - International phone number input with country selection
  • FileUploader - File upload with drag-and-drop and preview
  • OTPInput - One-time password entry for authentication
  • DateInput - Date and date range selection
All form components integrate seamlessly with react-hook-form for validation and state management.

PhoneInput

International phone number input with country code selection and validation.

Features

  • Country flag display
  • International phone number formatting
  • Country search functionality
  • Automatic validation
  • Error state support
  • Full keyboard navigation

Props

value
E164Number | string
Phone number value in E.164 format (e.g., “+14155552671”)
onChange
(value: E164Number) => void
Callback when phone number changes
error
string
Error message to display
defaultCountry
Country
Default country code (e.g., “US”, “GB”, “NG”)
disabled
boolean
Disable the input
className
string
Additional CSS classes

Usage Example

import { PhoneInput } from "@/components";
import { Controller, useForm } from "react-hook-form";

export function ContactForm() {
  const { control, formState: { errors } } = useForm();
  
  return (
    <Controller
      name="phone"
      control={control}
      render={({ field }) => (
        <PhoneInput
          value={field.value}
          onChange={field.onChange}
          error={errors.phone?.message}
          defaultCountry="US"
        />
      )}
    />
  );
}

Implementation Details

The PhoneInput component is built on react-phone-number-input and uses:
  • Radix UI Popover - Country selection dropdown
  • Radix UI Command - Searchable country list
  • react-phone-number-input/flags - Country flag SVGs
  • Standard Input component for text entry
Phone numbers are stored in E.164 format (e.g., “+14155552671”). This ensures consistency across the application and compatibility with backend APIs.

Country Selection

The country selector includes:
  • Searchable list of all countries
  • Flag display for visual recognition
  • Country name and dialing code display
  • Scroll area for long lists
  • Keyboard navigation support

FileUploader

File upload component with drag-and-drop support and preview functionality.

Features

  • Click to upload
  • Drag-and-drop support (visual only, implement handler separately)
  • Image preview
  • File size and type display
  • Error state handling
  • Remove file functionality

Props

label
string
Label text displayed above the uploader
value
File | null
Currently selected file
onChange
(file: File | null) => void
Callback when file is selected or removed
error
string
Error message to display
accept
string
default:"image/*"
Accepted file types (HTML accept attribute)
id
string
HTML id attribute

Usage Example

import FileUploader from "@/components/file-uploader/FileUploader";
import { useState } from "react";
import { Button } from "@/components";

export function AssetPhotoUpload() {
  const [file, setFile] = useState<File | null>(null);
  const [isUploading, setIsUploading] = useState(false);
  
  const handleUpload = async () => {
    if (!file) return;
    
    setIsUploading(true);
    const formData = new FormData();
    formData.append("file", file);
    
    const response = await uploadImage(formData);
    setIsUploading(false);
  };
  
  return (
    <div className="space-y-4">
      <FileUploader
        label="Asset Photo"
        value={file}
        onChange={setFile}
        accept="image/*"
      />
      <Button 
        onClick={handleUpload} 
        disabled={!file || isUploading}
        loading={isUploading}
      >
        Upload Photo
      </Button>
    </div>
  );
}

File Preview

The FileUploader automatically shows preview for uploaded files:
  • Images: Display image preview thumbnail
  • All Files: Show file name, size, and type
  • Remove Button: Clear the selected file

Styling States

  • Default: Dashed gray border
  • With File: Blue border with primary color tint
  • Error: Red border and background
  • Hover: Darker border color
The FileUploader stores the File object in memory. For actual uploads, you need to create a FormData object and send it to your upload endpoint.

OTPInput

One-time password input component for multi-factor authentication.

Features

  • Individual digit inputs
  • Auto-focus next input
  • Paste support
  • Backspace navigation
  • Numeric only validation

Props

length
number
default:"6"
Number of OTP digits
value
string
Current OTP value
onChange
(value: string) => void
Callback when OTP changes
error
string
Error message

Usage Example

src/app/auth/verify-otp/page.tsx
import { OTPInput } from "@/components";
import { useState } from "react";
import { Button } from "@/components";

export function VerifyOtpForm() {
  const [otp, setOtp] = useState("");
  const [error, setError] = useState("");
  const [isVerifying, setIsVerifying] = useState(false);
  
  const handleVerify = async () => {
    if (otp.length !== 6) {
      setError("Please enter all 6 digits");
      return;
    }
    
    setIsVerifying(true);
    setError("");
    
    try {
      await verifyOtp(otp);
      router.push("/dashboard");
    } catch (err) {
      setError("Invalid OTP code");
    } finally {
      setIsVerifying(false);
    }
  };
  
  return (
    <div className="space-y-6">
      <div>
        <h2 className="text-xl font-bold mb-2">Verify Your Email</h2>
        <p className="text-gray-600 text-sm">
          Enter the 6-digit code sent to your email
        </p>
      </div>
      
      <OTPInput
        length={6}
        value={otp}
        onChange={setOtp}
        error={error}
      />
      
      <Button 
        onClick={handleVerify}
        disabled={otp.length !== 6 || isVerifying}
        loading={isVerifying}
        className="w-full"
      >
        Verify Code
      </Button>
    </div>
  );
}

Keyboard Behavior

  • Number Keys: Enter digit and focus next input
  • Backspace: Clear current digit and focus previous input
  • Arrow Keys: Navigate between inputs
  • Paste: Auto-distribute pasted digits across inputs

DateInput & DateRangeFilter

Date selection components for single dates and date ranges.

DateRangeFilter

Date range picker for filtering data by date periods.

Props

startDate
Date | null
Start date of the range
endDate
Date | null
End date of the range
onStartDateChange
(date: Date | null) => void
Callback when start date changes
onEndDateChange
(date: Date | null) => void
Callback when end date changes

Usage Example

src/app/(home)/samples/components/SampleFilters.tsx
import { DateRangeFilter } from "@/components";
import { useState } from "react";

export function SampleFilters() {
  const [startDate, setStartDate] = useState<Date | null>(null);
  const [endDate, setEndDate] = useState<Date | null>(null);
  
  return (
    <div className="flex gap-4">
      <DateRangeFilter
        startDate={startDate}
        endDate={endDate}
        onStartDateChange={setStartDate}
        onEndDateChange={setEndDate}
      />
    </div>
  );
}

Common Form Patterns

Form with Multiple Specialized Inputs

src/app/(home)/users/components/UserForm.tsx
import { PhoneInput, FileUploader } from "@/components";
import Input from "@/components/input/Input";
import { Button } from "@/components";
import { useForm, Controller } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";

const schema = z.object({
  first_name: z.string().min(1, "First name is required"),
  last_name: z.string().min(1, "Last name is required"),
  email: z.string().email("Invalid email address"),
  phone: z.string().min(1, "Phone number is required"),
  avatar: z.instanceof(File).optional(),
});

export function UserForm({ onSubmit }) {
  const {
    register,
    control,
    handleSubmit,
    formState: { errors, isSubmitting },
  } = useForm({
    resolver: zodResolver(schema),
  });
  
  return (
    <form onSubmit={handleSubmit(onSubmit)} className="space-y-6">
      <div className="grid grid-cols-2 gap-4">
        <Input
          label="First Name"
          {...register("first_name")}
          error={errors.first_name?.message}
        />
        <Input
          label="Last Name"
          {...register("last_name")}
          error={errors.last_name?.message}
        />
      </div>
      
      <Input
        label="Email"
        type="email"
        {...register("email")}
        error={errors.email?.message}
      />
      
      <Controller
        name="phone"
        control={control}
        render={({ field }) => (
          <div className="space-y-2">
            <Label>Phone Number</Label>
            <PhoneInput
              value={field.value}
              onChange={field.onChange}
              error={errors.phone?.message}
              defaultCountry="US"
            />
          </div>
        )}
      />
      
      <Controller
        name="avatar"
        control={control}
        render={({ field }) => (
          <FileUploader
            label="Profile Picture"
            value={field.value}
            onChange={field.onChange}
            accept="image/*"
            error={errors.avatar?.message}
          />
        )}
      />
      
      <Button type="submit" loading={isSubmitting} className="w-full">
        Save User
      </Button>
    </form>
  );
}

Conditional File Upload

Show file uploader based on form state:
import FileUploader from "@/components/file-uploader/FileUploader";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components";
import { Controller } from "react-hook-form";

export function RecommendationForm({ control, watch }) {
  const hasAttachment = watch("has_attachment");
  
  return (
    <>
      <Controller
        name="has_attachment"
        control={control}
        render={({ field }) => (
          <Select value={field.value} onValueChange={field.onChange}>
            <SelectTrigger label="Include Attachment?">
              <SelectValue />
            </SelectTrigger>
            <SelectContent>
              <SelectItem value="yes">Yes</SelectItem>
              <SelectItem value="no">No</SelectItem>
            </SelectContent>
          </Select>
        )}
      />
      
      {hasAttachment === "yes" && (
        <Controller
          name="attachment"
          control={control}
          render={({ field }) => (
            <FileUploader
              label="Attachment"
              value={field.value}
              onChange={field.onChange}
              accept="application/pdf,image/*"
            />
          )}
        />
      )}
    </>
  );
}

Validation

Use Zod schemas for type-safe validation:
import { z } from "zod";
import { isValidPhoneNumber } from "react-phone-number-input";

// Phone validation
const phoneSchema = z
  .string()
  .min(1, "Phone number is required")
  .refine((val) => isValidPhoneNumber(val), {
    message: "Invalid phone number format",
  });

// File size validation
const MAX_FILE_SIZE = 5 * 1024 * 1024; // 5MB

const fileSchema = z
  .instanceof(File)
  .refine((file) => file.size <= MAX_FILE_SIZE, {
    message: "File must be less than 5MB",
  })
  .refine(
    (file) => ["image/jpeg", "image/png", "image/webp"].includes(file.type),
    { message: "Only JPEG, PNG, and WebP images are supported" }
  );

// OTP validation
const otpSchema = z
  .string()
  .length(6, "OTP must be 6 digits")
  .regex(/^\d+$/, "OTP must contain only numbers");

// Date range validation
const dateRangeSchema = z.object({
  start_date: z.date(),
  end_date: z.date(),
}).refine((data) => data.end_date >= data.start_date, {
  message: "End date must be after start date",
  path: ["end_date"],
});

Accessibility

  • All form inputs include proper labels
  • Error messages are announced to screen readers
  • Keyboard navigation is fully supported
  • Focus indicators are visible
  • ARIA attributes are properly set
// Example of accessible form structure
<div className="space-y-2">
  <Label htmlFor="phone">Phone Number *</Label>
  <PhoneInput
    id="phone"
    value={phone}
    onChange={setPhone}
    aria-required="true"
    aria-invalid={!!error}
    aria-describedby={error ? "phone-error" : undefined}
  />
  {error && (
    <p id="phone-error" role="alert" className="text-sm text-red-600">
      {error}
    </p>
  )}
</div>

Next Steps

Build docs developers (and LLMs) love