$ cat docs/file-upload.md
updated Recently · 16 min read · published
File Upload File upload components from basic click-to-browse zones to advanced drag-and-drop areas with multi-file support, image previews, progress tracking, and client-side validation.
Basic Upload Zone
A click-to-browse upload zone with a dashed border and icon. Clicking the zone opens the native file picker. The <input type="file"> is visually hidden but remains accessible. The zone highlights on drag over for visual feedback.
basic-upload HTML CSS JS Live
Copy 1 <div class="upload-zone" id="upload-zone-1"> 2 <input type="file" class="file-input" id="file-1" multiple/> 3 <label for="file-1" class="upload-label"> 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="32" height="32"> 5 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> 6 <polyline points="17 8 12 3 7 8"/> 7 <line x1="12" y1="3" x2="12" y2="15"/> 8 </svg> 9 <span class="upload-title"> 10 Drop files here 11 </span> 12 <span class="upload-sub"> 13 or click to browse · Max 10MB per file 14 </span> 15 </label> 16 </div>
Copy 1 .upload-zone { 2 max-width:380px; 3 margin:16px auto; 4 font-family:system-ui, 5 sans-serif 6 } 7 .file-input { 8 position:absolute; 9 width:1px; 10 height:1px; 11 padding:0; 12 margin:-1px; 13 overflow:hidden; 14 clip:rect(0, 15 0, 16 0, 17 0); 18 border:0 19 } 20 .upload-label { 21 display:flex; 22 flex-direction:column; 23 align-items:center; 24 gap:10px; 25 padding:32px 24px; 26 border:2px dashed #2A2A3E; 27 border-radius:12px; 28 cursor:pointer; 29 transition:all .2s; 30 background:#0A0A0A 31 } 32 .upload-label:hover { 33 border-color:#00FF41; 34 background:rgba(0, 35 255, 36 65, 37 .03) 38 } 39 .upload-label:hover svg { 40 stroke:#00FF41 41 } 42 .upload-label svg { 43 stroke:#555; 44 transition:stroke .2s 45 } 46 .upload-title { 47 font-size:13px; 48 font-weight:600; 49 color:#E0E0E0 50 } 51 .upload-sub { 52 font-size:11px; 53 color:#666 54 } 55 .upload-zone.dragover .upload-label { 56 border-color:#00FF41; 57 background:rgba(0, 58 255, 59 65, 60 .05); 61 transform:scale(1.01) 62 } 63 .upload-zone.dragover .upload-label svg { 64 stroke:#00FF41 65 }
untitled.js wrap JavaScript
Copy 1 const zone=document.getElementById('upload-zone-1');const input=document.getElementById('file-1');['dragenter','dragover'].forEach(e=>zone.addEventListener(e,ev=>{ev.preventDefault();zone.classList.add('dragover')}));['dragleave','drop'].forEach(e=>zone.addEventListener(e,ev=>{ev.preventDefault();zone.classList.remove('dragover')}));zone.addEventListener('drop',ev=>{const files=ev.dataTransfer.files;if(files.length){alert('Selected '+files.length+' file(s): '+Array.from(files).map(f=>f.name).join(', '))}});input.addEventListener('change',()=>{if(input.files.length){alert('Selected '+input.files.length+' file(s): '+Array.from(input.files).map(f=>f.name).join(', '))}})
preview
Drag & Drop Events
The drag-and-drop API fires a sequence of events on the drop zone:dragenter , dragover , dragleave , and drop . Both dragenter and dragover must call preventDefault() to allow dropping.
drag-drop-events.js wrap JavaScript
Copy 1 // Highlight on drag enter/over 2 ['dragenter', 'dragover'].forEach(evt => { 3 zone.addEventListener(evt, (e) => { 4 e.preventDefault(); 5 zone.classList.add('dragover'); 6 }); 7 }); 8 9 // Remove highlight on drag leave/drop 10 ['dragleave', 'drop'].forEach(evt => { 11 zone.addEventListener(evt, (e) => { 12 e.preventDefault(); 13 zone.classList.remove('dragover'); 14 }); 15 }); 16 17 // Handle dropped files 18 zone.addEventListener('drop', (e) => { 19 const files = e.dataTransfer.files; 20 handleFiles(files); 21 });
Multi-File with Previews
A multi-file upload zone that shows thumbnails for images and file type icons for other files. Each file displays its name, size, a progress bar, and a remove button. The footer shows the total file count and combined size.
multi-preview HTML CSS JS Live
Copy 1 <div class="mp-upload"> 2 <div class="mp-zone" id="mp-zone"> 3 <input type="file" class="file-input" id="mp-input" multiple/> 4 <label for="mp-input" class="mp-label"> 5 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="24" height="24"> 6 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> 7 <polyline points="17 8 12 3 7 8"/> 8 <line x1="12" y1="3" x2="12" y2="15"/> 9 </svg> 10 <span> 11 Add files or drop here 12 </span> 13 </label> 14 </div> 15 <div class="mp-list"> 16 <div class="mp-item"> 17 <div class="mp-thumb img" style="background:linear-gradient(135deg,#1a1a2e,#2d1b69)"> 18 <span> 19 IMG 20 </span> 21 </div> 22 <div class="mp-info"> 23 <span class="mp-name"> 24 hero-banner.png 25 </span> 26 <span class="mp-size"> 27 2.4 MB 28 </span> 29 </div> 30 <div class="mp-progress"> 31 <div class="mp-bar" style="width:100%"> 32 </div> 33 </div> 34 <button class="mp-remove" aria-label="Remove file"> 35 ✕ 36 </button> 37 </div> 38 <div class="mp-item"> 39 <div class="mp-thumb img" style="background:linear-gradient(135deg,#0f2027,#203a43)"> 40 <span> 41 IMG 42 </span> 43 </div> 44 <div class="mp-info"> 45 <span class="mp-name"> 46 team-photo.jpg 47 </span> 48 <span class="mp-size"> 49 1.8 MB 50 </span> 51 </div> 52 <div class="mp-progress"> 53 <div class="mp-bar" style="width:100%"> 54 </div> 55 </div> 56 <button class="mp-remove" aria-label="Remove file"> 57 ✕ 58 </button> 59 </div> 60 <div class="mp-item"> 61 <div class="mp-thumb doc"> 62 <span> 63 PDF 64 </span> 65 </div> 66 <div class="mp-info"> 67 <span class="mp-name"> 68 project-spec.pdf 69 </span> 70 <span class="mp-size"> 71 840 KB 72 </span> 73 </div> 74 <div class="mp-progress"> 75 <div class="mp-bar" style="width:65%"> 76 </div> 77 </div> 78 <button class="mp-remove" aria-label="Remove file"> 79 ✕ 80 </button> 81 </div> 82 </div> 83 <div class="mp-footer"> 84 <span class="mp-count"> 85 3 files selected 86 </span> 87 <span class="mp-total"> 88 Total: 5.0 MB 89 </span> 90 </div> 91 </div>
Copy 1 .mp-upload { 2 max-width:380px; 3 margin:16px auto; 4 font-family:system-ui, 5 sans-serif 6 } 7 .mp-zone { 8 margin-bottom:12px 9 } 10 .file-input { 11 position:absolute; 12 width:1px; 13 height:1px; 14 padding:0; 15 margin:-1px; 16 overflow:hidden; 17 clip:rect(0, 18 0, 19 0, 20 0); 21 border:0 22 } 23 .mp-label { 24 display:flex; 25 align-items:center; 26 gap:8px; 27 justify-content:center; 28 padding:14px; 29 border:2px dashed #2A2A3E; 30 border-radius:10px; 31 cursor:pointer; 32 transition:all .2s; 33 font-size:12px; 34 color:#666; 35 background:#0A0A0A 36 } 37 .mp-label:hover { 38 border-color:#00FF41; 39 color:#00FF41; 40 background:rgba(0, 41 255, 42 65, 43 .03) 44 } 45 .mp-list { 46 display:flex; 47 flex-direction:column; 48 gap:6px 49 } 50 .mp-item { 51 display:flex; 52 align-items:center; 53 gap:10px; 54 padding:10px 12px; 55 background:#111; 56 border:1px solid #222; 57 border-radius:8px; 58 position:relative 59 } 60 .mp-thumb { 61 width:36px; 62 height:36px; 63 border-radius:6px; 64 display:flex; 65 align-items:center; 66 justify-content:center; 67 font-size:8px; 68 font-weight:800; 69 color:rgba(255, 70 255, 71 255, 72 .5); 73 flex-shrink:0 74 } 75 .mp-thumb.doc { 76 background:#1a1a2e; 77 color:#EF4444 78 } 79 .mp-info { 80 flex:1; 81 min-width:0; 82 display:flex; 83 flex-direction:column; 84 gap:2px 85 } 86 .mp-name { 87 font-size:12px; 88 color:#E0E0E0; 89 font-weight:500; 90 white-space:nowrap; 91 overflow:hidden; 92 text-overflow:ellipsis 93 } 94 .mp-size { 95 font-size:10px; 96 color:#666 97 } 98 .mp-progress { 99 position:absolute; 100 bottom:0; 101 left:12px; 102 right:40px; 103 height:2px; 104 background:#1A1A2E; 105 border-radius:1px 106 } 107 .mp-bar { 108 height:100%; 109 background:linear-gradient(90deg, 110 #00FF41, 111 #00CC33); 112 border-radius:1px; 113 transition:width .3s ease 114 } 115 .mp-remove { 116 width:24px; 117 height:24px; 118 border-radius:6px; 119 border:none; 120 background:transparent; 121 color:#555; 122 cursor:pointer; 123 display:flex; 124 align-items:center; 125 justify-content:center; 126 font-size:11px; 127 transition:all .2s; 128 flex-shrink:0 129 } 130 .mp-remove:hover { 131 background:rgba(239, 132 68, 133 68, 134 .1); 135 color:#EF4444 136 } 137 .mp-footer { 138 display:flex; 139 justify-content:space-between; 140 margin-top:10px; 141 padding:0 4px 142 } 143 .mp-count, 144 .mp-total { 145 font-size:10px; 146 color:#666 147 } 148 .mp-total { 149 color:#00FF41; 150 font-weight:600 151 }
untitled.js wrap JavaScript
Copy 1 document.querySelectorAll('.mp-remove').forEach(btn=>{btn.addEventListener('click',()=>{const item=btn.closest('.mp-item');item.style.opacity='0';item.style.transform='translateX(10px)';item.style.transition='all .2s';setTimeout(()=>item.remove(),200)})})
preview
Upload Progress
Files with animated progress bars showing upload status. Each bar displays the percentage complete with a shimmer animation during upload. Completed files show a checkmark with a green background, while queued files display a clock icon.
upload-progress HTML CSS Live
Copy 1 <div class="prog-upload"> 2 <div class="pu-item done"> 3 <div class="pu-icon done"> 4 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="14" height="14"> 5 <polyline points="20 6 9 17 4 12"/> 6 </svg> 7 </div> 8 <div class="pu-details"> 9 <div class="pu-row"> 10 <span class="pu-name"> 11 design-system.zip 12 </span> 13 <span class="pu-status done"> 14 Complete 15 </span> 16 </div> 17 <div class="pu-bar-wrap"> 18 <div class="pu-bar done" style="width:100%"> 19 </div> 20 </div> 21 <span class="pu-meta"> 22 4.2 MB · 12s 23 </span> 24 </div> 25 </div> 26 <div class="pu-item active"> 27 <div class="pu-icon active"> 28 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"> 29 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> 30 <polyline points="17 8 12 3 7 8"/> 31 <line x1="12" y1="3" x2="12" y2="15"/> 32 </svg> 33 </div> 34 <div class="pu-details"> 35 <div class="pu-row"> 36 <span class="pu-name"> 37 assets-bundle.tar.gz 38 </span> 39 <span class="pu-status active"> 40 72% 41 </span> 42 </div> 43 <div class="pu-bar-wrap"> 44 <div class="pu-bar active" style="width:72%"> 45 </div> 46 </div> 47 <span class="pu-meta"> 48 18.6 MB · Uploading... 49 </span> 50 </div> 51 </div> 52 <div class="pu-item queued"> 53 <div class="pu-icon"> 54 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" width="14" height="14"> 55 <circle cx="12" cy="12" r="10"/> 56 <polyline points="12 6 12 12 16 14"/> 57 </svg> 58 </div> 59 <div class="pu-details"> 60 <div class="pu-row"> 61 <span class="pu-name"> 62 video-recording.mp4 63 </span> 64 <span class="pu-status"> 65 Queued 66 </span> 67 </div> 68 <div class="pu-bar-wrap"> 69 <div class="pu-bar" style="width:0%"> 70 </div> 71 </div> 72 <span class="pu-meta"> 73 142 MB · Waiting 74 </span> 75 </div> 76 </div> 77 </div>
Copy 1 .prog-upload { 2 max-width:380px; 3 margin:16px auto; 4 display:flex; 5 flex-direction:column; 6 gap:8px; 7 padding:0 4px; 8 font-family:system-ui, 9 sans-serif 10 } 11 .pu-item { 12 display:flex; 13 gap:12px; 14 padding:12px; 15 background:#111; 16 border:1px solid #222; 17 border-radius:8px; 18 align-items:flex-start 19 } 20 .pu-icon { 21 width:28px; 22 height:28px; 23 border-radius:6px; 24 display:flex; 25 align-items:center; 26 justify-content:center; 27 border:1px solid #333; 28 color:#555; 29 background:#0A0A0A; 30 flex-shrink:0 31 } 32 .pu-icon.done { 33 background:#00FF41; 34 border-color:#00FF41; 35 color:#0A0A0A 36 } 37 .pu-icon.active { 38 border-color:#00FF41; 39 color:#00FF41 40 } 41 .pu-details { 42 flex:1; 43 min-width:0; 44 display:flex; 45 flex-direction:column; 46 gap:6px 47 } 48 .pu-row { 49 display:flex; 50 justify-content:space-between; 51 align-items:center 52 } 53 .pu-name { 54 font-size:12px; 55 color:#E0E0E0; 56 font-weight:500; 57 white-space:nowrap; 58 overflow:hidden; 59 text-overflow:ellipsis 60 } 61 .pu-status { 62 font-size:10px; 63 color:#666; 64 font-weight:500 65 } 66 .pu-status.done { 67 color:#00FF41 68 } 69 .pu-status.active { 70 color:#00FF41; 71 font-weight:700; 72 font-variant-numeric:tabular-nums 73 } 74 .pu-bar-wrap { 75 height:3px; 76 background:#1A1A2E; 77 border-radius:2px; 78 overflow:hidden 79 } 80 .pu-bar { 81 height:100%; 82 border-radius:2px; 83 transition:width .5s ease; 84 background:#333 85 } 86 .pu-bar.done { 87 background:#00FF41 88 } 89 .pu-bar.active { 90 background:linear-gradient(90deg, 91 #00FF41, 92 #00CC33); 93 animation:shimmer 1.5s infinite 94 } 95 @keyframes shimmer { 96 0% { 97 opacity:1 98 } 99 50% { 100 opacity:.7 101 } 102 100% { 103 opacity:1 104 } 105 } 106 .pu-meta { 107 font-size:10px; 108 color:#555 109 }
preview
File Validation
Client-side validation that checks file type, size, and count before upload. Invalid files show inline error messages with the specific rejection reason, while valid files proceed to the upload queue with a green checkmark.
Copy 1 <div class="valid-upload"> 2 <div class="vu-zone" id="vu-zone"> 3 <input type="file" class="file-input" id="vu-input" accept=".jpg,.jpeg,.png,.gif,.pdf,.docx" multiple/> 4 <label for="vu-input" class="vu-label"> 5 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="28" height="28"> 6 <path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"/> 7 <polyline points="17 8 12 3 7 8"/> 8 <line x1="12" y1="3" x2="12" y2="15"/> 9 </svg> 10 <span> 11 Drop files here 12 </span> 13 <span class="vu-hint"> 14 JPG, PNG, GIF, PDF, DOCX · Max 5MB · Max 5 files 15 </span> 16 </label> 17 </div> 18 <div class="vu-list"> 19 <div class="vu-item valid"> 20 <div class="vi-icon valid"> 21 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.5" width="12" height="12"> 22 <polyline points="20 6 9 17 4 12"/> 23 </svg> 24 </div> 25 <div class="vi-info"> 26 <span class="vi-name"> 27 photo.jpg 28 </span> 29 <span class="vi-detail"> 30 1.2 MB · Image 31 </span> 32 </div> 33 </div> 34 <div class="vu-item invalid"> 35 <div class="vi-icon invalid"> 36 ✕ 37 </div> 38 <div class="vi-info"> 39 <span class="vi-name"> 40 video.mp4 41 </span> 42 <span class="vi-detail invalid"> 43 File type not allowed · MP4 44 </span> 45 </div> 46 </div> 47 <div class="vu-item invalid"> 48 <div class="vi-icon invalid"> 49 ✕ 50 </div> 51 <div class="vi-info"> 52 <span class="vi-name"> 53 large-file.zip 54 </span> 55 <span class="vi-detail invalid"> 56 Exceeds 5MB limit · 24 MB 57 </span> 58 </div> 59 </div> 60 </div> 61 </div>
Copy 1 .valid-upload { 2 max-width:380px; 3 margin:16px auto; 4 font-family:system-ui, 5 sans-serif 6 } 7 .vu-zone { 8 margin-bottom:12px 9 } 10 .file-input { 11 position:absolute; 12 width:1px; 13 height:1px; 14 padding:0; 15 margin:-1px; 16 overflow:hidden; 17 clip:rect(0, 18 0, 19 0, 20 0); 21 border:0 22 } 23 .vu-label { 24 display:flex; 25 flex-direction:column; 26 align-items:center; 27 gap:8px; 28 padding:24px; 29 border:2px dashed #2A2A3E; 30 border-radius:10px; 31 cursor:pointer; 32 transition:all .2s; 33 background:#0A0A0A 34 } 35 .vu-label:hover { 36 border-color:#00FF41; 37 background:rgba(0, 38 255, 39 65, 40 .03) 41 } 42 .vu-label svg { 43 stroke:#555; 44 transition:stroke .2s 45 } 46 .vu-label:hover svg { 47 stroke:#00FF41 48 } 49 .vu-label span { 50 font-size:12px; 51 color:#E0E0E0; 52 font-weight:500 53 } 54 .vu-hint { 55 font-size:10px!important; 56 color:#666!important; 57 font-weight:400!important 58 } 59 .vu-list { 60 display:flex; 61 flex-direction:column; 62 gap:6px 63 } 64 .vu-item { 65 display:flex; 66 gap:10px; 67 padding:10px 12px; 68 border-radius:8px; 69 border:1px solid #222; 70 background:#111; 71 align-items:center 72 } 73 .vu-item.invalid { 74 border-color:rgba(239, 75 68, 76 68, 77 .2); 78 background:rgba(239, 79 68, 80 68, 81 .03) 82 } 83 .vi-icon { 84 width:24px; 85 height:24px; 86 border-radius:50%; 87 display:flex; 88 align-items:center; 89 justify-content:center; 90 font-size:10px; 91 font-weight:700; 92 flex-shrink:0 93 } 94 .vi-icon.valid { 95 background:rgba(0, 96 255, 97 65, 98 .1); 99 color:#00FF41 100 } 101 .vi-icon.invalid { 102 background:rgba(239, 103 68, 104 68, 105 .1); 106 color:#EF4444; 107 font-size:11px 108 } 109 .vi-info { 110 display:flex; 111 flex-direction:column; 112 gap:2px 113 } 114 .vi-name { 115 font-size:12px; 116 color:#E0E0E0; 117 font-weight:500 118 } 119 .vi-detail { 120 font-size:10px; 121 color:#666 122 } 123 .vi-detail.invalid { 124 color:#EF4444 125 }
preview
Avatar Upload
A circular avatar upload with image preview. Clicking the avatar or button opens the file picker filtered to images. The selected image appears as a circular preview using FileReader.readAsDataURL() .
avatar-upload HTML CSS JS Live
Copy 1 <div class="avatar-upload"> 2 <div class="av-wrap"> 3 <div class="av-preview" id="av-preview"> 4 <div class="av-placeholder"> 5 <svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" width="28" height="28"> 6 <path d="M20 21v-2a4 4 0 0 0-4-4H8a4 4 0 0 0-4 4v2"/> 7 <circle cx="12" cy="7" r="4"/> 8 </svg> 9 </div> 10 </div> 11 <input type="file" class="file-input" id="av-input" accept="image/*"/> 12 <label for="av-input" class="av-btn"> 13 Change Photo 14 </label> 15 <span class="av-hint"> 16 JPG or PNG · Max 2MB 17 </span> 18 </div> 19 </div>
Copy 1 .avatar-upload { 2 display:flex; 3 flex-direction:column; 4 align-items:center; 5 padding:24px; 6 font-family:system-ui, 7 sans-serif 8 } 9 .av-wrap { 10 display:flex; 11 flex-direction:column; 12 align-items:center; 13 gap:12px 14 } 15 .av-preview { 16 width:80px; 17 height:80px; 18 border-radius:50%; 19 background:#1A1A2E; 20 border:2px dashed #333; 21 display:flex; 22 align-items:center; 23 justify-content:center; 24 overflow:hidden; 25 transition:border-color .2s; 26 cursor:pointer 27 } 28 .av-preview:hover { 29 border-color:#00FF41 30 } 31 .av-preview img { 32 width:100%; 33 height:100%; 34 object-fit:cover 35 } 36 .av-placeholder svg { 37 stroke:#555 38 } 39 .file-input { 40 position:absolute; 41 width:1px; 42 height:1px; 43 padding:0; 44 margin:-1px; 45 overflow:hidden; 46 clip:rect(0, 47 0, 48 0, 49 0); 50 border:0 51 } 52 .av-btn { 53 padding:8px 16px; 54 border-radius:6px; 55 font-size:11px; 56 font-weight:600; 57 background:transparent; 58 color:#00FF41; 59 border:1px solid #00FF41; 60 cursor:pointer; 61 transition:all .2s; 62 font-family:system-ui, 63 sans-serif 64 } 65 .av-btn:hover { 66 background:rgba(0, 67 255, 68 65, 69 .08) 70 } 71 .av-hint { 72 font-size:10px; 73 color:#666 74 }
untitled.js wrap JavaScript
Copy 1 const avInput=document.getElementById('av-input');const avPreview=document.getElementById('av-preview');avInput.addEventListener('change',()=>{const file=avInput.files[0];if(file&&file.type.startsWith('image/')){const reader=new FileReader();reader.onload=e=>{avPreview.innerHTML='<img src="'+e.target.result+'" alt="Avatar preview"/>';avPreview.style.borderStyle='solid';avPreview.style.borderColor='#00FF41'};reader.readAsDataURL(file)}});avPreview.addEventListener('click',()=>avInput.click())
preview
Image Preview Grid
A grid of image thumbnails generated from selected files using URL.createObjectURL() . Each thumbnail has an overlay with file name and a remove button. Click any thumbnail to see a larger preview.
Copy 1 <div class="img-grid-demo"> 2 <div class="igd-grid"> 3 <div class="igd-thumb"> 4 <div class="igd-img" style="background:linear-gradient(135deg,#1a1a2e,#2d1b69)"> 5 </div> 6 <div class="igd-overlay"> 7 <span class="igd-name"> 8 design-v1.png 9 </span> 10 <button class="igd-x" aria-label="Remove"> 11 ✕ 12 </button> 13 </div> 14 </div> 15 <div class="igd-thumb"> 16 <div class="igd-img" style="background:linear-gradient(135deg,#0f2027,#203a43)"> 17 </div> 18 <div class="igd-overlay"> 19 <span class="igd-name"> 20 hero-banner.jpg 21 </span> 22 <button class="igd-x" aria-label="Remove"> 23 ✕ 24 </button> 25 </div> 26 </div> 27 <div class="igd-thumb"> 28 <div class="igd-img" style="background:linear-gradient(135deg,#0c2340,#1e5a8a)"> 29 </div> 30 <div class="igd-overlay"> 31 <span class="igd-name"> 32 team-photo.png 33 </span> 34 <button class="igd-x" aria-label="Remove"> 35 ✕ 36 </button> 37 </div> 38 </div> 39 <div class="igd-thumb"> 40 <div class="igd-img" style="background:linear-gradient(135deg,#2d1810,#5a3a28)"> 41 </div> 42 <div class="igd-overlay"> 43 <span class="igd-name"> 44 screenshot.png 45 </span> 46 <button class="igd-x" aria-label="Remove"> 47 ✕ 48 </button> 49 </div> 50 </div> 51 </div> 52 </div>
Copy 1 .img-grid-demo { 2 max-width:320px; 3 margin:16px auto; 4 padding:0 16px; 5 font-family:system-ui, 6 sans-serif 7 } 8 .igd-grid { 9 display:grid; 10 grid-template-columns:1fr 1fr; 11 gap:8px 12 } 13 .igd-thumb { 14 position:relative; 15 border-radius:8px; 16 overflow:hidden; 17 aspect-ratio:1; 18 border:1px solid #222 19 } 20 .igd-img { 21 width:100%; 22 height:100% 23 } 24 .igd-overlay { 25 position:absolute; 26 inset:0; 27 background:linear-gradient(transparent 50%, 28 rgba(0, 29 0, 30 0, 31 .7)); 32 display:flex; 33 align-items:flex-end; 34 justify-content:space-between; 35 padding:8px; 36 opacity:0; 37 transition:opacity .2s 38 } 39 .igd-thumb:hover .igd-overlay { 40 opacity:1 41 } 42 .igd-name { 43 font-size:9px; 44 color:#E0E0E0; 45 font-weight:500; 46 white-space:nowrap; 47 overflow:hidden; 48 text-overflow:ellipsis; 49 flex:1 50 } 51 .igd-x { 52 width:20px; 53 height:20px; 54 border-radius:4px; 55 border:none; 56 background:rgba(239, 57 68, 58 68, 59 .8); 60 color:#fff; 61 font-size:9px; 62 cursor:pointer; 63 display:flex; 64 align-items:center; 65 justify-content:center; 66 flex-shrink:0; 67 transition:background .2s 68 } 69 .igd-x:hover { 70 background:#EF4444 71 }
untitled.js wrap JavaScript
Copy 1 document.querySelectorAll('.igd-x').forEach(btn=>{btn.addEventListener('click',e=>{e.stopPropagation();const thumb=btn.closest('.igd-thumb');thumb.style.opacity='0';thumb.style.transform='scale(.8)';thumb.style.transition='all .2s';setTimeout(()=>thumb.remove(),200)})})
preview
HTML Structure
The upload zone wraps a hidden <input type="file"> with a visible <label> for click-to-browse. Drag-and-drop events are handled on the container element. Always include aria-live="polite" on the file list.
upload-structure.html wrap HTML
Copy 1 <div class="upload-zone" role="button" tabindex="0"> 2 <input type="file" class="sr-only" id="file-upload" 3 multiple accept=".jpg,.png,.pdf" /> 4 5 <label for="file-upload" class="upload-label"> 6 <svg><!-- upload icon --></svg> 7 <span>Drop files here or click to browse</span> 8 <small>JPG, PNG, PDF · Max 10MB</small> 9 </label> 10 </div> 11 12 <!-- File list after selection --> 13 <ul class="file-list" aria-live="polite"> 14 <li class="file-item"> 15 <span class="file-icon">📄</span> 16 <span class="file-name">document.pdf</span> 17 <span class="file-size">1.2 MB</span> 18 <button aria-label="Remove document.pdf">✕</button> 19 </li> 20 </ul>
Best Practices
ℹ info
Always validate files client-side before uploading \u2014 check type, size, and count. Server-side validation is still required, but client-side checks provide instant feedback and reduce unnecessary network traffic.
⚠ warning
Set the accept attribute on the file input to filter the file picker, but never rely on it alone for validation \u2014 users can bypass it. Always validate on the server.
✓ best practice
Use aria-live="polite" on the file list container so screen readers announce new files and removals without interrupting the current task.
Show file size and type icons in the file list so users can quickly verify their selections before uploading. Support keyboard navigation: Enter/Space to open file picker, Delete to remove selected files, Tab to navigate the list. For drag-and-drop, highlight the drop zone on dragenter and reset on dragleave /drop . Show upload progress for files over 1MB \u2014 users need feedback for large uploads. Use a progress bar with percentage and file size. Generate image previews using URL.createObjectURL() or FileReader.readAsDataURL() before uploading.