Add an Image component to your Retool canvas and set its Source to a URL string or a `{{ }}` expression (e.g., `{{ query1.data[0].photo_url }}`). For tables, add an Image-type column and set the cell value to `{{ currentRow.image_url }}`. Base64 images use `data:image/png;base64,{{ filepicker1.value[0] }}`. For private S3 images, generate pre-signed URLs in a JS query first.
| Fact | Value |
|---|---|
| Tool | Retool |
| Difficulty | Beginner |
| Time required | 10-15 min |
| Compatibility | Retool Cloud and Self-hosted |
| Last updated | March 2026 |
Working with Images in Retool
Retool's Image component displays images from any accessible URL — HTTP/HTTPS links, data URIs, base64-encoded images, and relative paths. The Source field accepts both static strings and `{{ }}` dynamic expressions, making it easy to display database-stored image URLs or dynamically selected images.
Beyond standalone Image components, Retool tables support Image-type columns that render an image in each row based on that row's URL data. This is the most common use case: a users table with profile photos, a product catalog with product images, or an inventory system with item photos.
This tutorial covers all four image source types you'll encounter: public HTTPS URLs, base64 data URIs, pre-signed S3 URLs, and Retool Storage URLs.
Prerequisites
- A Retool app with at least one query returning image URL data, or an Image component to configure
- Editor or Admin access to the Retool app
- URLs or base64 data for the images you want to display
Step-by-step guide
Add an Image component and set a static URL
In the Retool component panel, find Image under the Media section. Drag it onto the canvas. Select the Image component and look at the Inspector panel. In the General section, find the Source (or Image URL) field. Enter any public HTTPS image URL — for example, https://via.placeholder.com/300x200 for a test image. The Image component renders the image at the component's dimensions (adjust by dragging component edges). In the Inspector's Layout section, set Object fit to: Cover (fills the frame, may crop), Contain (shows full image, may add letterboxing), or Fill (stretches to fill). Cover is most common for thumbnails.
Expected result: The Image component displays the placeholder image in the specified frame dimensions.
Display a dynamic image from query data
To display an image that comes from a database query, bind the Image component's Source field to a `{{ }}` expression referencing your query data. First, run a query that returns image URLs (e.g., SELECT id, name, photo_url FROM users LIMIT 1). Then in the Image component's Source field, enter: `{{ query1.data[0].photo_url }}` For the currently selected table row, use: `{{ table1.selectedRow.photo_url }}` This enables a master-detail pattern: when a user clicks a table row, the Image component updates to show that row's image.
1// Image component → Source field examples:23// Show first record's image4{{ query1.data[0].photo_url }}56// Show selected table row's image7{{ table1.selectedRow.photo_url }}89// Show image with fallback for null/undefined URLs10{{ table1.selectedRow.photo_url || 'https://via.placeholder.com/100?text=No+Photo' }}1112// Show image from a state variable13{{ selectedImageUrl.value }}Expected result: The Image component displays the photo from the selected query row or table row selection.
Add an image column to a Retool Table
For tables where each row should show an image (product catalogs, user directories, etc.), add a custom Image column. Select the Table component → Inspector → Columns → click the + icon to add a custom column. Set the Column type to Image. In the Value field for that column, enter a `{{ }}` expression: `{{ currentRow.photo_url }}` Retool renders each row's image as a thumbnail inside the table cell. You can control thumbnail size in the column's Width setting. The images load lazily as rows become visible.
1// Table custom column → Image type → Value field:2{{ currentRow.photo_url }}34// With fallback for missing images:5{{ currentRow.photo_url || 'https://via.placeholder.com/40?text=?' }}67// For avatars (sometimes stored as user.avatar.url in nested objects):8{{ currentRow.avatar ? currentRow.avatar.url : '' }}Expected result: Each table row shows an image thumbnail in the column, loaded from the URL in that row's photo_url field.
Display base64-encoded images from the File Picker
When users upload images via Retool's File Picker component, the uploaded file is stored as a base64-encoded string accessible via `{{ filePicker1.value[0] }}` (the `[0]` because File Picker returns an array of files). To display an uploaded image immediately after selection (for preview), set an Image component's Source to a data URI:
1// Image component → Source field to preview File Picker upload:2data:{{ filePicker1.value[0].type }};base64,{{ filePicker1.value[0].base64Data }}34// More commonly written as:5{{ 'data:' + filePicker1.value[0].type + ';base64,' + filePicker1.value[0].base64Data }}67// Check if a file is selected before building the data URI:8{{ filePicker1.value.length > 0 9 ? 'data:' + filePicker1.value[0].type + ';base64,' + filePicker1.value[0].base64Data 10 : '' }}Expected result: After selecting a file in the File Picker, the Image component immediately shows a preview of the selected image.
Handle private images with pre-signed S3 URLs
If images are stored in a private S3 bucket, direct URLs won't work because they require authentication. Generate pre-signed URLs via a JS Query that calls your S3 resource. Pre-signed URLs are time-limited (typically 1-24 hours) and require the S3 bucket name and object key. Create a JS Query named getPresignedUrl that takes the object key and returns a temporary URL:
1// JS Query: getPresignedUrl2// Calls an API resource that generates pre-signed URLs3// First, create an API resource that wraps your S3 presign endpoint45// Alternative: use Retool's AWS S3 resource directly6// In a REST API query:7// GET https://s3.amazonaws.com/{{ s3BucketName }}/{{ objectKey }}8// With AWS Signature auth configured in the resource910// JS Query to build the S3 URL for display:11const bucketName = 'my-company-bucket';12const objectKey = table1.selectedRow.s3_key;13const region = 'us-east-1';1415// For public S3 buckets (not recommended for production):16return `https://${bucketName}.s3.${region}.amazonaws.com/${objectKey}`;1718// For private buckets, call your backend presign endpoint:19// return await fetch('/api/s3/presign?key=' + objectKey).then(r => r.json()).then(d => d.url);Expected result: The Image component displays the private S3 image using a temporary pre-signed URL generated by the JS query.
Add error handling for broken image URLs
When an image URL is null, undefined, or points to a non-existent resource, the Image component shows a broken image icon. Handle this gracefully by providing fallback URLs using JavaScript's || operator in the Source expression. For more control, use a Text component with HTML mode to implement the native onerror handler:
1// Simple fallback in Image component Source field:2{{ table1.selectedRow.photo_url || 'https://ui-avatars.com/api/?name=' + table1.selectedRow.name }}34// Text component with HTML mode enabled (more control):5// <img src="{{ table1.selectedRow.photo_url }}"6// onerror="this.src='https://via.placeholder.com/100?text=No+Image'"7// style="width:100%;height:100%;object-fit:cover;border-radius:8px;" />89// JS Query to validate URL before displaying:10const url = table1.selectedRow.photo_url;11if (!url || typeof url !== 'string' || !url.startsWith('http')) {12 return 'https://via.placeholder.com/100?text=No+Image';13}14return url;Expected result: When an image URL is missing or broken, a placeholder image appears instead of a broken icon.
Complete working example
1// JS Query: resolveImageUrl2// Resolves an image URL from various sources (URL, base64, S3 key)3// Usage: set Image component Source to {{ resolveImageUrl.data }}45const imageData = table1.selectedRow;67// Priority order: direct URL > S3 key > base64 > placeholder8if (imageData.photo_url && imageData.photo_url.startsWith('http')) {9 // Direct HTTPS URL10 return imageData.photo_url;11}1213if (imageData.s3_key) {14 // Build S3 URL (assumes public bucket or CDN in front)15 const cdnBase = 'https://cdn.yourcompany.com';16 return `${cdnBase}/${imageData.s3_key}`;17}1819if (imageData.photo_base64) {20 // Base64-encoded image stored in database21 const mimeType = imageData.photo_mime_type || 'image/jpeg';22 return `data:${mimeType};base64,${imageData.photo_base64}`;23}2425// Fallback: generated avatar from name26const name = encodeURIComponent(imageData.name || 'Unknown');27return `https://ui-avatars.com/api/?name=${name}&background=7C3AED&color=fff&size=100`;Common mistakes when displaying Images from URLs in Retool
Why it's a problem: Using table1.selectedRow.photo_url in the Image Source before any row is selected, causing the component to show a broken image on load
How to avoid: Add a fallback: {{ table1.selectedRow.photo_url || 'https://via.placeholder.com/100' }}. Also set the Image component's Hidden property to {{ !table1.selectedRow }} to hide it when no row is selected.
Why it's a problem: Trying to display HTTP (non-HTTPS) image URLs which browsers block as mixed content
How to avoid: Ensure all image URLs use HTTPS. If your image server only serves HTTP, you'll need to configure a reverse proxy or CDN with HTTPS in front of it, or upload images to a proper storage service like S3 or Retool Storage.
Why it's a problem: Storing large images as base64 in the database and experiencing slow query load times
How to avoid: Store only the image URL or S3 object key in the database. Upload actual image files to object storage (AWS S3, Retool Storage, Cloudflare R2) and save the resulting URL. Query response times drop dramatically when you're not transmitting base64 image data.
Best practices
- Always provide a fallback URL for Image components — null URLs show broken image icons that look unprofessional
- Use Object fit: Cover for thumbnail-style images in tables; use Contain for full images where the full content must be visible
- For user avatars with no photo, generate initials-based avatars using the ui-avatars.com API as a fallback URL
- Lazy-load large image sets by using server-side pagination in your query — don't load 1,000 image URLs at once
- Store image URLs (not base64) in your database for large images — base64 strings are ~33% larger and slow down query responses
- For private images, generate pre-signed URLs server-side with a short expiration (1-4 hours) rather than exposing permanent S3 credentials
- Test images in preview mode with the actual data — the editor canvas may show placeholder images that work differently from real data
Still stuck?
Copy one of these prompts to get a personalized, step-by-step explanation.
I'm building a Retool user directory app. My PostgreSQL query returns user rows with name, email, and photo_url fields. I want to: (1) show a profile image in an Image component when a user is selected from a table, (2) add a photo_url image column to the table itself, (3) show a placeholder avatar if photo_url is null. Write the Retool {{ }} expressions for each case, and show how to use the ui-avatars.com API as a fallback based on the user's name.
In this Retool app: (1) set the image1 Image component Source to {{ table1.selectedRow.photo_url || 'https://ui-avatars.com/api/?name=' + encodeURIComponent(table1.selectedRow.name || 'User') }}, (2) add a custom Image-type column to table1 with value {{ currentRow.photo_url }}, (3) hide image1 when no row is selected using the Hidden property: {{ !table1.selectedRow.id }}.
Frequently asked questions
Can Retool display images stored in Retool Storage?
Yes. Retool Storage generates HTTPS URLs for uploaded files. After uploading to Retool Storage via a query or File Picker, store the returned URL in your database. Then display it in an Image component the same way as any other HTTPS URL. For private Retool Storage files, you'll need to generate signed URLs via the Retool Storage API.
Why do images in my Retool table load slowly?
Slow image loading in tables usually means the images are too large (high-resolution photos displayed at thumbnail size), too many rows are being fetched at once, or the images aren't cached/CDN-served. Fix: use paginated queries to limit rows, serve images through a CDN that resizes them on-the-fly (Cloudinary, Imgix, or AWS CloudFront with Lambda@Edge), and ensure images are compressed before storage.
How do I display an image that's returned directly as binary data from an API?
If an API returns image binary data directly (Content-Type: image/jpeg), configure your Retool REST API resource query to return the response as Base64. Retool has an 'Encode response as base64' option in the query settings. Then combine with the MIME type to build the data URI: {{ 'data:image/jpeg;base64,' + imageQuery.data }}.
Talk to an Expert
Our team has built 600+ apps. Get personalized help with your project.
Book a free consultation