File Upload
MediumDrag-and-drop file upload with progress indicators and file type validation
Live Demo
Basic File Upload
Simple file upload with click-to-browse functionality
Resize to see how the component adapts to different screen sizes
📖 Basic File Upload Overview
Simple file upload with click-to-browse functionality
💡 Key Features
- • Basic File Upload functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with file capabilities
- • Enhanced with upload capabilities
- • Enhanced with drag-drop capabilities
- • Enhanced with progress capabilities
- • Enhanced with validation capabilities
- • Enhanced with multiple capabilities
🏗️ Basic File Upload HTML Structure
This basic file upload component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-file-uploads-basic"
Connects this HTML element to the Basic File Upload Stimulus controller
data-action="click->forms-file-uploads-basic#method"
Defines click events that trigger basic file upload controller methods
data-forms-file-uploads-basic-target="element"
Identifies elements that the Basic File Upload controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance basic file upload accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Basic File Upload Tailwind Classes
This basic file upload component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for basic file upload contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Basic File Upload Design
- •
sm:
- Small screens (640px+) optimized for basic file upload - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures basic file upload works on all devices
⚡ Basic File Upload JavaScript Logic
The Basic File Upload component uses a dedicated Stimulus controller (forms-file-uploads-basic
) to handle basic file upload interactions and manage component state.
🎯 Basic File Upload Controller Features
Targets
DOM elements the basic file upload controller can reference and manipulate
Values
Configuration data for basic file upload behavior passed from HTML
Actions
Methods triggered by basic file upload user events and interactions
Lifecycle
Setup and cleanup methods for basic file upload initialization
🔄 Basic File Upload Event Flow
- 1. User interacts with basic file upload element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Basic File Upload controller method executes with access to targets and values
- 4. Controller updates DOM with new basic file upload state or visual changes
- 5. CSS transitions provide smooth visual feedback for basic file upload interactions
<div class="max-w-md mx-auto" data-controller="file-uploads-basic">
<div class="space-y-4">
<!-- Hidden file input -->
<input
type="file"
data-file-uploads--basic-target="input"
data-action="change->basic#handleFileSelect"
class="hidden"
accept="image/*,.pdf"
>
<!-- Upload button -->
<button
type="button"
data-action="click->basic#triggerFileSelect"
class="w-full flex items-center justify-center px-6 py-4 border-2 border-green-300 border-dashed rounded-xl hover:border-green-500 hover:bg-green-50 transition-colors duration-200 focus:outline-none focus:ring-2 focus:ring-green-500 focus:ring-offset-2"
>
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-green-400" stroke="currentColor" fill="none" viewBox="0 0 48 48">
<path d="M28 8H12a4 4 0 00-4 4v20m32-12v8m0 0v8a4 4 0 01-4 4H12a4 4 0 01-4-4v-4m32-4l-3.172-3.172a4 4 0 00-5.656 0L28 28M8 32l9.172-9.172a4 4 0 015.656 0L28 28m0 0l4 4m4-24h8m-4-4v8m-12 4h.02" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" />
</svg>
<div class="mt-4">
<p class="text-base font-medium text-green-900">Choose a file</p>
<p class="text-sm text-green-500 mt-1">Click to browse your files</p>
</div>
</div>
</button>
<!-- File preview -->
<div data-file-uploads--basic-target="preview" class="space-y-3"></div>
<!-- Feedback message -->
<div data-file-uploads--basic-target="feedback" class="hidden text-sm text-green-600 bg-green-50 border border-green-200 rounded-lg px-3 py-2"></div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "preview", "feedback"]
connect() {
console.log("Basic file upload controller connected")
}
handleFileSelect(event) {
const files = Array.from(event.target.files)
if (files.length > 0) {
this.displayFile(files[0])
this.showFeedback(`Selected: ${files[0].name}`)
}
}
displayFile(file) {
if (this.hasPreviewTarget) {
this.previewTarget.innerHTML = `
<div class="flex items-center space-x-3 p-3 bg-green-50 border border-green-200 rounded-lg">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div class="flex-1 min-w-0">
<p class="text-sm font-medium text-green-900 truncate">${file.name}</p>
<p class="text-sm text-green-500">${this.formatFileSize(file.size)}</p>
</div>
</div>
`
}
}
showFeedback(message) {
if (this.hasFeedbackTarget) {
this.feedbackTarget.textContent = message
this.feedbackTarget.classList.remove("hidden")
setTimeout(() => {
this.feedbackTarget.classList.add("hidden")
}, 3000)
}
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
triggerFileSelect() {
this.inputTarget.click()
}
}
Drag & Drop Upload
Drag and drop file upload with visual feedback
Drop files here
or click to browse
Supports images, PDFs, and documents
Resize to see how the component adapts to different screen sizes
📖 Drag & Drop Upload Overview
Drag and drop file upload with visual feedback
💡 Key Features
- • Drag & Drop Upload functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with file capabilities
- • Enhanced with upload capabilities
- • Enhanced with drag-drop capabilities
- • Enhanced with progress capabilities
- • Enhanced with validation capabilities
- • Enhanced with multiple capabilities
🏗️ Drag & Drop Upload HTML Structure
This drag & drop upload component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-file-uploads-drag-drop"
Connects this HTML element to the Drag & Drop Upload Stimulus controller
data-action="click->forms-file-uploads-drag-drop#method"
Defines click events that trigger drag & drop upload controller methods
data-forms-file-uploads-drag-drop-target="element"
Identifies elements that the Drag & Drop Upload controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance drag & drop upload accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Drag & Drop Upload Tailwind Classes
This drag & drop upload component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for drag & drop upload contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Drag & Drop Upload Design
- •
sm:
- Small screens (640px+) optimized for drag & drop upload - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures drag & drop upload works on all devices
⚡ Drag & Drop Upload JavaScript Logic
The Drag & Drop Upload component uses a dedicated Stimulus controller (forms-file-uploads-drag-drop
) to handle drag & drop upload interactions and manage component state.
🎯 Drag & Drop Upload Controller Features
Targets
DOM elements the drag & drop upload controller can reference and manipulate
Values
Configuration data for drag & drop upload behavior passed from HTML
Actions
Methods triggered by drag & drop upload user events and interactions
Lifecycle
Setup and cleanup methods for drag & drop upload initialization
🔄 Drag & Drop Upload Event Flow
- 1. User interacts with drag & drop upload element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Drag & Drop Upload controller method executes with access to targets and values
- 4. Controller updates DOM with new drag & drop upload state or visual changes
- 5. CSS transitions provide smooth visual feedback for drag & drop upload interactions
<div class="max-w-lg mx-auto" data-controller="file-uploads-drag-drop">
<div class="space-y-6">
<!-- Hidden file input -->
<input
type="file"
data-file-uploads--drag-drop-target="input"
data-action="change->drag-drop#handleFileSelect"
class="hidden"
multiple
accept="image/*,.pdf,.doc,.docx"
>
<!-- Drag & Drop Zone -->
<div
data-file-uploads--drag-drop-target="dropzone"
class="relative border-2 border-dashed border-green-300 bg-green-50 rounded-xl p-8 text-center transition-all duration-200 cursor-pointer hover:border-green-400 hover:bg-green-100"
data-action="click->drag-drop#triggerFileSelect"
>
<div class="space-y-4">
<div class="flex justify-center">
<svg class="h-16 w-16 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12" />
</svg>
</div>
<div>
<h3 class="text-lg font-medium text-green-900">Drop files here</h3>
<p class="text-green-600 mt-2">
or <span class="font-medium text-green-700 underline">click to browse</span>
</p>
<p class="text-sm text-green-500 mt-1">
Supports images, PDFs, and documents
</p>
</div>
<div class="flex items-center justify-center space-x-4 text-xs text-green-400">
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z"></path>
</svg>
Images
</span>
<span class="flex items-center">
<svg class="w-4 h-4 mr-1" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
Documents
</span>
</div>
</div>
<!-- Drag overlay (appears when dragging files over the zone) -->
<div class="absolute inset-0 bg-green-500 bg-opacity-10 rounded-xl hidden drag-overlay">
<div class="h-full flex items-center justify-center">
<div class="text-center">
<svg class="mx-auto h-12 w-12 text-green-600" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M9 12l3 3m0 0l3-3m-3 3V9" />
</svg>
<p class="mt-2 text-sm font-medium text-green-700">Release to upload</p>
</div>
</div>
</div>
</div>
<!-- File preview area -->
<div data-file-uploads--drag-drop-target="preview" class="space-y-3"></div>
<!-- Feedback message -->
<div data-file-uploads--drag-drop-target="feedback" class="hidden text-sm text-green-600 bg-green-50 border border-green-200 rounded-lg px-3 py-2"></div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["dropzone", "input", "preview", "feedback"]
connect() {
console.log("Drag & drop file upload controller connected")
this.setupDragEvents()
}
setupDragEvents() {
const dropzone = this.dropzoneTarget
// Prevent default drag behaviors
;['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, this.preventDefaults.bind(this), false)
})
// Highlight drop zone when item is dragged over it
;['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, this.highlight.bind(this), false)
})
;['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, this.unhighlight.bind(this), false)
})
// Handle dropped files
dropzone.addEventListener('drop', this.handleDrop.bind(this), false)
}
preventDefaults(e) {
e.preventDefault()
e.stopPropagation()
}
highlight() {
this.dropzoneTarget.classList.add('border-green-500', 'bg-green-100', 'ring-2', 'ring-green-300')
this.dropzoneTarget.classList.remove('border-green-300', 'bg-green-50')
}
unhighlight() {
this.dropzoneTarget.classList.remove('border-green-500', 'bg-green-100', 'ring-2', 'ring-green-300')
this.dropzoneTarget.classList.add('border-green-300', 'bg-green-50')
}
handleDrop(e) {
const dt = e.dataTransfer
const files = dt.files
this.handleFiles(files)
}
handleFileSelect(event) {
const files = event.target.files
this.handleFiles(files)
}
handleFiles(files) {
Array.from(files).forEach(file => {
this.processFile(file)
})
}
processFile(file) {
this.displayFile(file)
this.showFeedback(`Uploaded: ${file.name}`)
// Simulate file processing
setTimeout(() => {
this.showFeedback(`Processing ${file.name}...`)
}, 500)
}
displayFile(file) {
if (this.hasPreviewTarget) {
const fileElement = document.createElement('div')
fileElement.className = 'flex items-center justify-between p-3 bg-white border border-green-200 rounded-lg shadow-sm'
fileElement.innerHTML = `
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 21h10a2 2 0 002-2V9.414a1 1 0 00-.293-.707l-5.414-5.414A1 1 0 0012.586 3H7a2 2 0 00-2 2v14a2 2 0 002 2z"></path>
</svg>
</div>
<div>
<p class="text-sm font-medium text-green-900">${file.name}</p>
<p class="text-sm text-green-500">${this.formatFileSize(file.size)}</p>
</div>
</div>
<div class="flex-shrink-0">
<button class="text-green-400 hover:text-green-600 transition-colors" data-action="click->drag-drop#removeFile">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
`
this.previewTarget.appendChild(fileElement)
}
}
removeFile(event) {
const fileElement = event.target.closest('.flex')
if (fileElement) {
fileElement.remove()
this.showFeedback("File removed")
}
}
showFeedback(message) {
if (this.hasFeedbackTarget) {
this.feedbackTarget.textContent = message
this.feedbackTarget.classList.remove("hidden")
setTimeout(() => {
this.feedbackTarget.classList.add("hidden")
}, 3000)
}
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
triggerFileSelect() {
this.inputTarget.click()
}
}
Multiple Files
Upload multiple files with individual progress and preview
Multiple File Upload
0 / 5 files selected
Select up to 5 files
Supported: Images, PDFs, Documents, Text files
Resize to see how the component adapts to different screen sizes
📖 Multiple Files Overview
Upload multiple files with individual progress and preview
💡 Key Features
- • Multiple Files functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with file capabilities
- • Enhanced with upload capabilities
- • Enhanced with drag-drop capabilities
- • Enhanced with progress capabilities
- • Enhanced with validation capabilities
- • Enhanced with multiple capabilities
🏗️ Multiple Files HTML Structure
This multiple files component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-file-uploads-multiple"
Connects this HTML element to the Multiple Files Stimulus controller
data-action="click->forms-file-uploads-multiple#method"
Defines click events that trigger multiple files controller methods
data-forms-file-uploads-multiple-target="element"
Identifies elements that the Multiple Files controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance multiple files accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 Multiple Files Tailwind Classes
This multiple files component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for multiple files contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive Multiple Files Design
- •
sm:
- Small screens (640px+) optimized for multiple files - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures multiple files works on all devices
⚡ Multiple Files JavaScript Logic
The Multiple Files component uses a dedicated Stimulus controller (forms-file-uploads-multiple
) to handle multiple files interactions and manage component state.
🎯 Multiple Files Controller Features
Targets
DOM elements the multiple files controller can reference and manipulate
Values
Configuration data for multiple files behavior passed from HTML
Actions
Methods triggered by multiple files user events and interactions
Lifecycle
Setup and cleanup methods for multiple files initialization
🔄 Multiple Files Event Flow
- 1. User interacts with multiple files element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. Multiple Files controller method executes with access to targets and values
- 4. Controller updates DOM with new multiple files state or visual changes
- 5. CSS transitions provide smooth visual feedback for multiple files interactions
<div class="max-w-2xl mx-auto" data-controller="file-uploads-multiple" data-file-uploads--multiple-max-files-value="5">
<div class="space-y-6">
<!-- Header with file counter -->
<div class="flex items-center justify-between">
<div>
<h3 class="text-lg font-medium text-green-900">Multiple File Upload</h3>
<p data-file-uploads--multiple-target="counter" class="text-sm text-green-600">0 / 5 files selected</p>
</div>
<button
type="button"
data-action="click->multiple#clearAllFiles"
class="text-sm text-green-600 hover:text-green-800 underline"
>
Clear All
</button>
</div>
<!-- Hidden file input -->
<input
type="file"
data-file-uploads--multiple-target="input"
data-action="change->multiple#handleFileSelect"
class="hidden"
multiple
accept="image/*,.pdf,.doc,.docx,.txt"
>
<!-- Upload area -->
<div class="border-2 border-dashed border-green-300 rounded-xl p-6">
<div class="text-center space-y-4">
<div class="flex justify-center">
<svg class="h-12 w-12 text-green-400" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M12 6v6m0 0v6m0-6h6m-6 0H6" />
</svg>
</div>
<div>
<button
type="button"
data-action="click->multiple#triggerFileSelect"
class="inline-flex items-center px-4 py-2 border border-green-300 rounded-lg shadow-sm text-sm font-medium text-green-700 bg-white hover:bg-green-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-green-500"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M7 16a4 4 0 01-.88-7.903A5 5 0 1115.9 6L16 6a5 5 0 011 9.9M15 13l-3-3m0 0l-3 3m3-3v12"></path>
</svg>
Choose Files
</button>
</div>
<div class="text-xs text-green-500">
<p>Select up to 5 files</p>
<p class="mt-1">Supported: Images, PDFs, Documents, Text files</p>
</div>
</div>
</div>
<!-- File preview area with progress -->
<div data-file-uploads--multiple-target="preview" class="space-y-4"></div>
<!-- Overall progress (if needed) -->
<div data-file-uploads--multiple-target="progress" class="hidden">
<div class="flex items-center justify-between text-sm mb-2">
<span class="text-green-700 font-medium">Overall Progress</span>
<span class="text-green-500">75%</span>
</div>
<div class="w-full bg-green-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full transition-all duration-300" style="width: 75%"></div>
</div>
</div>
<!-- Feedback message -->
<div data-file-uploads--multiple-target="feedback" class="hidden text-sm text-green-600 bg-green-50 border border-green-200 rounded-lg px-3 py-2"></div>
<!-- Action buttons -->
<div class="flex items-center justify-between pt-4 border-t border-green-200">
<div class="text-sm text-green-500">
Click on files to add more, or remove individual files using the × button
</div>
<div class="flex space-x-3">
<button
type="button"
data-action="click->multiple#triggerFileSelect"
class="text-sm text-green-600 hover:text-green-800 font-medium"
>
Add More Files
</button>
</div>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "preview", "feedback", "counter", "progress"]
static values = { maxFiles: { type: Number, default: 5 } }
connect() {
console.log("Multiple file upload controller connected")
this.files = []
this.updateCounter()
}
handleFileSelect(event) {
const newFiles = Array.from(event.target.files)
newFiles.forEach(file => {
if (this.files.length < this.maxFilesValue) {
this.files.push(file)
this.displayFile(file)
this.simulateUpload(file)
} else {
this.showFeedback(`Maximum ${this.maxFilesValue} files allowed`)
}
})
this.updateCounter()
// Clear the input so the same file can be selected again if needed
event.target.value = ''
}
displayFile(file) {
if (this.hasPreviewTarget) {
const fileId = `file-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
const fileElement = document.createElement('div')
fileElement.className = 'bg-white border border-green-200 rounded-lg p-4 space-y-3'
fileElement.dataset.fileId = fileId
fileElement.innerHTML = `
<div class="flex items-center justify-between">
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12h6m-6 4h6m2 5H7a2 2 0 01-2-2V5a2 2 0 012-2h5.586a1 1 0 01.707.293l5.414 5.414a1 1 0 01.293.707V19a2 2 0 01-2 2z"></path>
</svg>
</div>
<div>
<p class="text-sm font-medium text-green-900">${file.name}</p>
<p class="text-sm text-green-500">${this.formatFileSize(file.size)}</p>
</div>
</div>
<button class="text-green-400 hover:text-green-600 transition-colors" data-action="click->multiple#removeFile" data-file-id="${fileId}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
<div class="space-y-2">
<div class="flex items-center justify-between text-sm">
<span class="text-green-600 upload-status">Preparing...</span>
<span class="text-green-500 upload-percentage">0%</span>
</div>
<div class="w-full bg-green-200 rounded-full h-2">
<div class="bg-green-600 h-2 rounded-full transition-all duration-300 upload-progress" style="width: 0%"></div>
</div>
</div>
`
this.previewTarget.appendChild(fileElement)
}
}
simulateUpload(file) {
const fileElement = this.previewTarget.querySelector(`[data-file-id]`)
const progressBar = fileElement.querySelector('.upload-progress')
const statusText = fileElement.querySelector('.upload-status')
const percentageText = fileElement.querySelector('.upload-percentage')
let progress = 0
const interval = setInterval(() => {
progress += Math.random() * 15
if (progress >= 100) {
progress = 100
clearInterval(interval)
statusText.textContent = 'Complete'
statusText.classList.remove('text-green-600')
statusText.classList.add('text-green-600')
progressBar.classList.remove('bg-green-600')
progressBar.classList.add('bg-green-600')
} else {
statusText.textContent = 'Uploading...'
}
progressBar.style.width = `${progress}%`
percentageText.textContent = `${Math.round(progress)}%`
}, 200)
}
removeFile(event) {
const fileId = event.target.dataset.fileId
const fileElement = this.previewTarget.querySelector(`[data-file-id="${fileId}"]`)
if (fileElement) {
// Find and remove the file from our files array
const fileName = fileElement.querySelector('p.text-sm.font-medium').textContent
this.files = this.files.filter(file => file.name !== fileName)
fileElement.remove()
this.updateCounter()
this.showFeedback("File removed")
}
}
updateCounter() {
if (this.hasCounterTarget) {
this.counterTarget.textContent = `${this.files.length} / ${this.maxFilesValue} files selected`
if (this.files.length >= this.maxFilesValue) {
this.counterTarget.classList.add('text-amber-600', 'font-medium')
this.counterTarget.classList.remove('text-green-600')
} else {
this.counterTarget.classList.remove('text-amber-600', 'font-medium')
this.counterTarget.classList.add('text-green-600')
}
}
}
showFeedback(message) {
if (this.hasFeedbackTarget) {
this.feedbackTarget.textContent = message
this.feedbackTarget.classList.remove("hidden")
setTimeout(() => {
this.feedbackTarget.classList.add("hidden")
}, 3000)
}
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
triggerFileSelect() {
this.inputTarget.click()
}
clearAllFiles() {
this.files = []
this.previewTarget.innerHTML = ''
this.updateCounter()
this.showFeedback("All files cleared")
}
}
File Validation
File type and size validation with error messages
File Upload with Validation
Upload files with strict validation rules
⚠️ Strict validation enabled
Files will be checked against all requirements
💡 Testing Tips:
- • Try uploading a file larger than 2MB to see size validation
- • Upload a .txt or .doc file to see type validation
- • Upload more than 3 files to test count limits
- • Valid files: JPG, PNG, GIF, PDF under 2MB
Resize to see how the component adapts to different screen sizes
📖 File Validation Overview
File type and size validation with error messages
💡 Key Features
- • File Validation functionality with Stimulus controller
- • Input fields, selects, checkboxes, and form validation components
- • Responsive design optimized for all screen sizes
- • Accessible markup with proper semantic HTML
- • Modern CSS transitions and interactive effects
- • Enhanced with file capabilities
- • Enhanced with upload capabilities
- • Enhanced with drag-drop capabilities
- • Enhanced with progress capabilities
- • Enhanced with validation capabilities
- • Enhanced with multiple capabilities
🏗️ File Validation HTML Structure
This file validation component uses semantic HTML structure with Stimulus data attributes to connect HTML elements to JavaScript functionality.
📋 Stimulus Data Attributes
data-controller="forms-file-uploads-validation"
Connects this HTML element to the File Validation Stimulus controller
data-action="click->forms-file-uploads-validation#method"
Defines click events that trigger file validation controller methods
data-forms-file-uploads-validation-target="element"
Identifies elements that the File Validation controller can reference and manipulate
♿ Accessibility Features
- • Semantic HTML elements provide screen reader context
- • ARIA attributes enhance file validation accessibility
- • Keyboard navigation fully supported
- • Focus management for interactive elements
🎨 File Validation Tailwind Classes
This file validation component uses Tailwind CSS utility classes with the forest theme color palette for styling and responsive design.
🎨 Form Components Colors
bg-honey-600
Primary
bg-honey-100
Light
bg-honey-400
Accent
📐 Layout & Spacing
p-4
- Padding for file validation contentmb-4
- Margin bottom between elementsspace-y-2
- Vertical spacing in listsrounded-lg
- Rounded corners for modern look📱 Responsive File Validation Design
- •
sm:
- Small screens (640px+) optimized for file validation - •
md:
- Medium screens (768px+) enhanced layout - •
lg:
- Large screens (1024px+) full functionality - • Mobile-first approach ensures file validation works on all devices
⚡ File Validation JavaScript Logic
The File Validation component uses a dedicated Stimulus controller (forms-file-uploads-validation
) to handle file validation interactions and manage component state.
🎯 File Validation Controller Features
Targets
DOM elements the file validation controller can reference and manipulate
Values
Configuration data for file validation behavior passed from HTML
Actions
Methods triggered by file validation user events and interactions
Lifecycle
Setup and cleanup methods for file validation initialization
🔄 File Validation Event Flow
- 1. User interacts with file validation element (click, hover, input, etc.)
- 2. Stimulus detects event through
data-action
attribute - 3. File Validation controller method executes with access to targets and values
- 4. Controller updates DOM with new file validation state or visual changes
- 5. CSS transitions provide smooth visual feedback for file validation interactions
<div class="max-w-2xl mx-auto" data-controller="file-uploads-validation"
data-forms--file-uploads--validation-allowed-types-value='["image/jpeg", "image/png", "image/gif", "application/pdf"]'
data-file-uploads--validation-max-size-value="2097152"
data-file-uploads--validation-max-files-value="3">
<div class="space-y-6">
<!-- Header -->
<div>
<h3 class="text-lg font-medium text-green-900">File Upload with Validation</h3>
<p class="text-sm text-green-600 mt-1">Upload files with strict validation rules</p>
</div>
<!-- Validation rules display -->
<div data-file-uploads--validation-target="errors"></div>
<!-- Hidden file input -->
<input
type="file"
data-file-uploads--validation-target="input"
data-action="change->validation#handleFileSelect"
class="hidden"
multiple
accept=".jpg,.jpeg,.png,.gif,.pdf"
>
<!-- Upload area -->
<div class="border-2 border-dashed border-amber-300 bg-amber-50 rounded-xl p-6 hover:border-amber-400 hover:bg-amber-100 transition-colors">
<div class="text-center space-y-4">
<div class="flex justify-center">
<svg class="h-12 w-12 text-amber-500" fill="none" viewBox="0 0 24 24" stroke="currentColor">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="1.5" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z" />
</svg>
</div>
<div>
<button
type="button"
data-action="click->validation#triggerFileSelect"
class="inline-flex items-center px-6 py-2 border border-amber-500 rounded-lg shadow-sm text-sm font-medium text-amber-700 bg-white hover:bg-amber-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-amber-500"
>
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m5.618-4.016A11.955 11.955 0 0112 2.944a11.955 11.955 0 01-8.618 3.04A12.02 12.02 0 003 9c0 5.591 3.824 10.29 9 11.622 5.176-1.332 9-6.03 9-11.622 0-1.042-.133-2.052-.382-3.016z"></path>
</svg>
Select Files to Validate
</button>
</div>
<div class="text-xs text-amber-600">
<p class="font-medium">⚠️ Strict validation enabled</p>
<p class="mt-1">Files will be checked against all requirements</p>
</div>
</div>
</div>
<!-- Results area -->
<div data-file-uploads--validation-target="preview" class="space-y-4"></div>
<!-- Feedback message -->
<div data-file-uploads--validation-target="feedback" class="hidden"></div>
<!-- Action buttons -->
<div class="flex items-center justify-between pt-4 border-t border-green-200">
<div class="text-sm text-green-500">
Valid files will show in green, invalid files in red with error details
</div>
<div class="flex space-x-3">
<button
type="button"
data-action="click->validation#clearAll"
class="text-sm text-red-600 hover:text-red-800 font-medium"
>
Clear All
</button>
<button
type="button"
data-action="click->validation#triggerFileSelect"
class="text-sm text-amber-600 hover:text-amber-800 font-medium"
>
Add Files
</button>
</div>
</div>
<!-- Demo section with test files info -->
<div class="mt-6 p-4 bg-slate-50 border border-slate-200 rounded-lg">
<h4 class="text-sm font-medium text-slate-900 mb-2">💡 Testing Tips:</h4>
<ul class="text-xs text-slate-600 space-y-1">
<li>• Try uploading a file larger than 2MB to see size validation</li>
<li>• Upload a .txt or .doc file to see type validation</li>
<li>• Upload more than 3 files to test count limits</li>
<li>• Valid files: JPG, PNG, GIF, PDF under 2MB</li>
</ul>
</div>
</div>
</div>
import { Controller } from "@hotwired/stimulus"
export default class extends Controller {
static targets = ["input", "preview", "feedback", "errors"]
static values = {
allowedTypes: { type: Array, default: ["image/jpeg", "image/png", "image/gif", "application/pdf"] },
maxSize: { type: Number, default: 5 * 1024 * 1024 }, // 5MB default
maxFiles: { type: Number, default: 3 }
}
connect() {
console.log("File validation controller connected")
this.validFiles = []
this.invalidFiles = []
this.displayValidationRules()
}
handleFileSelect(event) {
const files = Array.from(event.target.files)
this.validateAndProcessFiles(files)
// Clear the input
event.target.value = ''
}
validateAndProcessFiles(files) {
this.clearErrors()
const newValidFiles = []
const newInvalidFiles = []
files.forEach(file => {
const validation = this.validateFile(file)
if (validation.isValid) {
newValidFiles.push(file)
} else {
newInvalidFiles.push({ file, errors: validation.errors })
}
})
// Check total file count
if (this.validFiles.length + newValidFiles.length > this.maxFilesValue) {
this.showError(`Maximum ${this.maxFilesValue} files allowed. Currently have ${this.validFiles.length} files.`)
return
}
// Process valid files
newValidFiles.forEach(file => {
this.validFiles.push(file)
this.displayValidFile(file)
})
// Display invalid files with errors
newInvalidFiles.forEach(({ file, errors }) => {
this.invalidFiles.push({ file, errors })
this.displayInvalidFile(file, errors)
})
// Show summary feedback
if (newValidFiles.length > 0) {
this.showFeedback(`${newValidFiles.length} file(s) uploaded successfully`)
}
if (newInvalidFiles.length > 0) {
this.showError(`${newInvalidFiles.length} file(s) failed validation`)
}
}
validateFile(file) {
const errors = []
// Check file type
if (!this.allowedTypesValue.includes(file.type)) {
const allowedExtensions = this.allowedTypesValue.map(type => {
switch(type) {
case 'image/jpeg': return 'JPG'
case 'image/png': return 'PNG'
case 'image/gif': return 'GIF'
case 'application/pdf': return 'PDF'
default: return type.split('/')[1]?.toUpperCase()
}
}).join(', ')
errors.push(`File type not allowed. Allowed: ${allowedExtensions}`)
}
// Check file size
if (file.size > this.maxSizeValue) {
errors.push(`File too large. Maximum: ${this.formatFileSize(this.maxSizeValue)}`)
}
// Check if file size is 0
if (file.size === 0) {
errors.push('File is empty')
}
return {
isValid: errors.length === 0,
errors
}
}
displayValidFile(file) {
if (this.hasPreviewTarget) {
const fileElement = document.createElement('div')
fileElement.className = 'flex items-center justify-between p-3 bg-green-50 border border-green-200 rounded-lg'
fileElement.innerHTML = `
<div class="flex items-center space-x-3">
<div class="flex-shrink-0">
<svg class="w-6 h-6 text-green-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M9 12l2 2 4-4m6 2a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-green-900">${file.name}</p>
<p class="text-sm text-green-700">${this.formatFileSize(file.size)} • Valid</p>
</div>
</div>
<button class="text-green-400 hover:text-green-600 transition-colors" data-action="click->validation#removeValidFile" data-file-name="${file.name}">
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
`
this.previewTarget.appendChild(fileElement)
}
}
displayInvalidFile(file, errors) {
if (this.hasPreviewTarget) {
const fileElement = document.createElement('div')
fileElement.className = 'p-3 bg-red-50 border border-red-200 rounded-lg'
fileElement.innerHTML = `
<div class="flex items-start justify-between">
<div class="flex items-start space-x-3">
<div class="flex-shrink-0 mt-0.5">
<svg class="w-5 h-5 text-red-600" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 8v4m0 4h.01M21 12a9 9 0 11-18 0 9 9 0 0118 0z"></path>
</svg>
</div>
<div class="flex-1">
<p class="text-sm font-medium text-red-900">${file.name}</p>
<p class="text-sm text-red-700">${this.formatFileSize(file.size)}</p>
<ul class="mt-2 text-xs text-red-600 space-y-1">
${errors.map(error => `<li>• ${error}</li>`).join('')}
</ul>
</div>
</div>
<button class="text-red-400 hover:text-red-600 transition-colors" data-action="click->validation#removeInvalidFile" data-file-name="${file.name}">
<svg class="w-4 h-4" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12"></path>
</svg>
</button>
</div>
`
this.previewTarget.appendChild(fileElement)
}
}
displayValidationRules() {
if (this.hasErrorsTarget) {
const allowedExtensions = this.allowedTypesValue.map(type => {
switch(type) {
case 'image/jpeg': return 'JPG'
case 'image/png': return 'PNG'
case 'image/gif': return 'GIF'
case 'application/pdf': return 'PDF'
default: return type.split('/')[1]?.toUpperCase()
}
}).join(', ')
this.errorsTarget.innerHTML = `
<div class="bg-blue-50 border border-blue-200 rounded-lg p-4">
<h4 class="text-sm font-medium text-blue-900 mb-2">File Requirements:</h4>
<ul class="text-sm text-blue-700 space-y-1">
<li>• Allowed types: ${allowedExtensions}</li>
<li>• Maximum size: ${this.formatFileSize(this.maxSizeValue)}</li>
<li>• Maximum files: ${this.maxFilesValue}</li>
</ul>
</div>
`
}
}
removeValidFile(event) {
const fileName = event.target.dataset.fileName
const fileElement = event.target.closest('.flex')
if (fileElement) {
this.validFiles = this.validFiles.filter(file => file.name !== fileName)
fileElement.remove()
this.showFeedback("Valid file removed")
}
}
removeInvalidFile(event) {
const fileName = event.target.dataset.fileName
const fileElement = event.target.closest('.p-3')
if (fileElement) {
this.invalidFiles = this.invalidFiles.filter(item => item.file.name !== fileName)
fileElement.remove()
this.showFeedback("Invalid file removed")
}
}
clearErrors() {
// Remove any previous error messages from the feedback area
if (this.hasFeedbackTarget) {
this.feedbackTarget.classList.add('hidden')
}
}
showFeedback(message) {
if (this.hasFeedbackTarget) {
this.feedbackTarget.textContent = message
this.feedbackTarget.className = 'text-sm text-green-600 bg-green-50 border border-green-200 rounded-lg px-3 py-2'
this.feedbackTarget.classList.remove("hidden")
setTimeout(() => {
this.feedbackTarget.classList.add("hidden")
}, 4000)
}
}
showError(message) {
if (this.hasFeedbackTarget) {
this.feedbackTarget.textContent = message
this.feedbackTarget.className = 'text-sm text-red-600 bg-red-50 border border-red-200 rounded-lg px-3 py-2'
this.feedbackTarget.classList.remove("hidden")
setTimeout(() => {
this.feedbackTarget.classList.add("hidden")
}, 5000)
}
}
formatFileSize(bytes) {
if (bytes === 0) return "0 Bytes"
const k = 1024
const sizes = ["Bytes", "KB", "MB", "GB"]
const i = Math.floor(Math.log(bytes) / Math.log(k))
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i]
}
triggerFileSelect() {
this.inputTarget.click()
}
clearAll() {
this.validFiles = []
this.invalidFiles = []
this.previewTarget.innerHTML = ''
this.showFeedback("All files cleared")
}
}
Installation & Usage
Copy the ERB template
Copy the ERB code from the template tab above and paste it into your Rails view file.
Add the Stimulus controller
Create the Stimulus controller file in your JavaScript controllers directory.