
Image upload is one of the most common features in modern web applications today. In fact, there’s a high chance, you’ve uploaded a photo or document in a web app recently. For developers, understanding how to implement image uploads is essential. This involves efficiently and securely handling everything from client-side validation and progress indicators to server-side storage and API integration, all to deliver interactive and user-friendly experiences.
In this article, we’ll take a step-by-step approach to break down how to implement image uploads in your Angular apps, starting from understanding how the image upload process works in web apps, and building up to handling multiple uploads and integrating with services like Cloudinary for better media management.
In this article:
- Prerequisites
- How to Upload Images With Angular
- How to Handle Multiple Image Uploads
- How to Streamline Image Upload with Angular and Cloudinary
Prerequisites
Before you begin, you must be familiar with JavaScript, CSS, and HTML. This tutorial uses Firebase Studio to set up a demo application using Angular CLI, a command-line interface tool for scaffolding and developing angular applications locally.
If you have your own application already set up, you can also follow along.
How to Upload Images With Angular
In many web applications, image uploading typically involves the user using the client-side component (the browser) for file selection and then a backend server for processing and storing the image in a database or doing some other things with it. Since Angular is a framework for building user interfaces, we’ll focus on how to implement file upload on the client-side.
Uploading images in Angular usually involves three key steps:
- Users select an image from their device using the HTML input element with a file input field.
- Optionally implement features such as file preview, drag-and-drop and progress indicator, for enhanced user experience.
- The selected image file is sent/uploaded to a backend server using the Fetch API or XMLHttpRequest.
Now let’s see how all these pieces work together in practice using code.
Step 1: Create Upload Component
For simplicity, we’ll create a dedicated component to handle file uploads. Run the following command in your project root directory to add a new folder called upload under src/app:
ng generate component upload
In the upload.html file, let’s add an input element for file upload:
<div class="container">
<h2>Upload an Image</h2>
<label class="upload-box">
<input type="file" (change)="onFileSelected($event)" hidden />
<p>Click to upload or drag & drop</p>
</label>
<button (click)="onUpload()" class="upload-button" [disabled]="!selectedFile()">
Upload
</button>
@if (selectedFile(); as file) {
<div class="file-info">
Selected: {{ file.name }}
</div>
}
</div>
In the above code, we added the hidden attribute on the input field to hide the default input element so we can style the input using our own custom styling. The functions: onFileSelected() and onUpload() will be in the component’s TypeScript logic (we’ll create these soon), while we are also showing the selected file’s name using selectedFile().
Next, let’s add some styling in upload.css:
.container {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
min-height: 60vh;
padding: 24px;
max-width: 400px;
margin: 0 auto;
}
.upload-box {
width: 100%;
height: 160px;
border: 2px dashed #d1d5db;
border-radius: 1rem;
background-color: white;
cursor: pointer;
text-align: center;
padding: 20px;
transition: all 0.2s ease;
}
.upload-box:hover {
background-color: #f3f4f6;
}
.upload-button {
width: 100%;
background-color: #2563eb;
color: white;
font-weight: 500;
padding: 12px;
border: none;
border-radius: 0.5rem;
margin-top: 16px;
transition: background-color 0.2s ease;
}
.upload-button:hover {
background-color: #1e40af;
}
.upload-button:disabled {
background-color: #9ca3af;
cursor: not-allowed;
}
.file-info {
margin-top: 12px;
font-size: 0.9rem;
color: #4b5563;
}
Step 2: Implement File Upload Logic
In upload.ts, paste the following code to implement file selection and upload logic:
import { ChangeDetectionStrategy, Component, inject, signal } from '@angular/core';
import { HttpClient } from '@angular/common/http';
@Component({
selector: 'app-upload',
templateUrl: './upload.html',
styleUrl: './upload.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class Upload {
selectedFile = signal<File | null>(null);
private http = inject(HttpClient);
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files && input.files.length > 0) {
this.selectedFile.set(input.files[0]);
}
}
onUpload() {
const file = this.selectedFile();
if (!file) return;
const formData = new FormData();
formData.append('file', file);
// Example request to send the image to a server. We'll change this later
this.http.post('http://localhost:3000/upload', formData)
.subscribe(response => {
console.log('Upload success!', response);
});
}
}
Here, we’re using the FormData Web API interface to construct the file input, allowing us to send the file data in an HTTP POST request. We do this by adding an event listener to the input element to retrieve the selected file by the user.
Next, we’ll add provideHttpClient() as a provider to app.config.ts to allow our app to make HTTP requests:
import { ApplicationConfig, provideBrowserGlobalErrorListeners, provideZoneChangeDetection } from '@angular/core';
import { provideRouter } from '@angular/router';
import { provideHttpClient } from '@angular/common/http';
import { routes } from './app.routes';
export const appConfig: ApplicationConfig = {
providers: [
provideBrowserGlobalErrorListeners(),
provideZoneChangeDetection({ eventCoalescing: true }),
provideRouter(routes),
provideHttpClient(),
]
};
Step 3: Import the Upload Component in app.ts
Now to use the Upload component, we’ll import it in app.ts as follows:
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { RouterOutlet } from '@angular/router';
import { Upload } from './upload/upload';
@Component({
selector: 'app-root',
imports: [RouterOutlet, Upload],
templateUrl: './app.html',
styleUrl: './app.css',
changeDetection: ChangeDetectionStrategy.OnPush,
})
export class App {
protected readonly title = signal('myapp');
}
Then reference the upload component selector in app.html:
<app-upload></app-upload>
This is what the homepage now looks like:

Click on the input area and you will see the name of the selected image file. This confirms that the file selection is working. Great!
How to Handle Multiple Image Uploads
Multiple image uploads is a common feature in many modern applications that allows users to upload multiple images at once. To do this, we’ll add the multiple attribute to the input field in upload.html:
<div class="container">
<h2>Upload Images</h2>
<label class="upload-box">
<input type="file" (change)="onFileSelected($event)" hidden multiple/>
<p>Click to upload or drag & drop</p>
</label>
<button (click)="onUpload()" class="upload-button" [disabled]="selectedFiles().length === 0">
Upload
</button>
@if (selectedFiles().length > 0) {
<div class="file-info">
<p>Selected files: {{ selectedFiles().length }}</p>
<ul>
@for (file of selectedFiles(); track file.name) {
<li>{{ file.name }}</li>
}
</ul>
</div>
}
</div>
Note we also added a view to show users the total number of selected files and the name of each file.
Next, let’s modify the logic in upload.ts to handle multiple file uploads:
export class Upload {
selectedFiles = signal<File[]>([]);
private http = inject(HttpClient);
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files) {
this.selectedFiles.set(Array.from(input.files));
}
}
onUpload() {
const files = this.selectedFiles();
if (files.length === 0) {
return;
}
const formData = new FormData();
for (const file of files) {
formData.append('files', file);
}
// Example request to send the selected images to a server
this.http.post('http://localhost:3000/upload-multiple', formData)
.subscribe(response => {
console.log('Multiple upload success!', response);
});
}
}
Now user can select multiple files at once for upload

Awesome! So far, we have successfully demonstrated the first and second key steps in implementing image uploads in Angular applications. To implement the last step, which is processing or storing the uploaded image for later use, you need to set up a server and a database to handle that logic. This backend infrastructure can be challenging and time-consuming to configure from scratch, especially for quick experiments or proof-of-concept projects.
In the next section, we’ll show you how Cloudinary simplifies the image upload process by replacing the need for complex, self-managed server logic, and providing a comprehensive, cloud-based service covering storage, image transformation, and optimized media delivery.
How to Streamline Image Upload with Angular and Cloudinary
Cloudinary is a cloud-based media management platform for images, videos, audio, and other media assets. Cloudinary handles asset uploads, storage, and even automatic optimizations like resizing or format conversion. It integrates easily with Angular applications through an SDK that includes several useful functions and APIs for interacting with the Cloudinary platform.
To upload images to Cloudinary from Angular, you have two options:
- Server-Side Upload: Your Angular app sends the file to your own secure backend server, and that server then sends the file to your Cloudinary cloud product environment using APIs and SDKs.
- Client-side Upload: Your Angular app uploads the file directly to Cloudinary’s REST API using an upload preset. This is simpler to implement but requires careful configuration to ensure security (e.g., restricted folders, allowed file types).
For this tutorial, we’ll go with the second option since we’re working in a client-side application. To get started, sign up for a free Cloudinary account. Next, you’ll need to retrieve your product environment cloud name. To get yours, log in to your dashboard. At the top of the page under Product Environment, find your Cloud name and copy it to your clipboard.
Next, you’ll create an unsigned upload request. You can do this directly in your Cloudinary dashboard. Remember to note the name of the preset you created.

Basically, an upload preset is a predefined set of upload options and transformations that can be applied to assets during the upload process instead of specifying them in each upload call. For example, you can use an upload preset to specify if you want to upload images to a specific folder in your product environment or if you want the images to be public or private.
Update upload.ts
Update the Upload class in upload.ts to the following:
export class Upload {
selectedFiles = signal<File[]>([]);
uploadedImageUrls = signal<string[]>([]);
// Replace with your Cloudinary cloud name and upload preset
cloudName = 'YOUR_CLOUD_NAME';
uploadPreset = 'YOUR_UPLOAD_PRESET';
cloudinaryURL = `https://api.cloudinary.com/v1_1/${this.cloudName}/image/upload`;
private http = inject(HttpClient);
onFileSelected(event: Event) {
const input = event.target as HTMLInputElement;
if (input.files) {
this.selectedFiles.set(Array.from(input.files));
this.uploadedImageUrls.set([]); // Clear previous URLs on new file selection
}
}
onUpload() {
const files = this.selectedFiles();
if (files.length === 0) {
return;
}
this.uploadedImageUrls.set([]);
for (const file of files) {
const formData = new FormData();
formData.append('file', file);
formData.append('upload_preset', this.uploadPreset);
this.http.post(this.cloudinaryURL, formData).subscribe({
next: (data: any) => {
console.log('Image uploaded:', data);
this.uploadedImageUrls.update(urls => [...urls, data.secure_url]);
},
error: (error) => {
console.error('Error uploading image:', error);
}
});
}
}
}
Update upload.html
Add the following to upload.html to display the URLs of the uploaded files:
<!-- Display the uploaded files -->
@if (uploadedImageUrls().length > 0) {
<div class="success-message">
<h3>Upload Successful!</h3>
<p>URLs for your uploaded images:</p>
<ul>
@for (url of uploadedImageUrls(); track url) {
<li>
<a [href]="url" target="_blank">{{ url }}</a>
</li>
}
</ul>
</div>
}
Now try uploading some files and you should see their URLs displayed as shown below:

Now you should be able to see the uploaded images in your Cloudinary media library as shown in the screenshot below:

With Cloudinary, your images are uploaded directly to the cloud, bypassing the need for your own server storage. Additionally, you can perform several image processing tasks such as basic editing using Cloudinary to advanced functionalities like object detection and more.
Final Thoughts
Image upload is an essential feature in modern web applications. In many real-world projects, implementing this typically requires setting up a backend server to handle file processing and a database to store image data or blobs. However, managing these tasks internally can add unnecessary complexity and overhead. By integrating a media management service like Cloudinary, you can offload the overhead of image storage, optimization, and delivery, allowing your Angular app to focus on delivering a smooth user interface and experience.
Transform and optimize your images and videos effortlessly with Cloudinary’s cloud-based solutions. Sign up for free today!
Frequently Asked Questions
How can I restrict the file types/size the user can upload?
You can restrict users to uploading certain file types by adding the accept attribute to the <input type="file"> element (for example, accept="image/png, image/jpeg"):
<input type="file" id="profile_pic" name="profile_pic" accept="image/png, image/jpeg"/>
To set upload limits for image file size, you can validate the file size in upload.ts before processing it. Here’s an example:
// In the OnFileSelected method, set file upload limit to 5 MB in bytes
const maxFileSize = 5 * 1024 * 1024;
for (let i = 0; i < this.selectedFiles.length; i++) {
if (this.selectedFiles[i].size > maxFileSize) {
alert(`File ${this.selectedFiles[i].name} is too large. Max size is 5MB.`);
this.selectedFiles = null;
return;
}
}
How can I optimize images before uploading to reduce file size?
You can optimize images to reduce their file size both on the client-side or server-side using different methods and tools. For browser-based/client-side image optimization, the ngx-image-compress library is a popular choice. For server-side image optimization, Sharp and node-imagemagick are popular and excellent choices. However, if you’re using Cloudinary, you can dynamically optimize images on-the-fly when delivering them to users using Cloudinary’s intelligent image optimization algorithms. You can read more about optimizing images with Cloudinary in the docs.