Create a responsive image-based EspoCRM record card using Ebla List Pro, Layout Manager, and standard Custom files.
Before you begin
The fields and List Card layout can be prepared from the EspoCRM administration area. The custom HTML design requires file access to the EspoCRM installation. Complete and test the customization on a development or test installation before using it in production.
What you will create
- A large image at the top of each card.
- Status and price displayed over the image.
- Category, location, and record name.
- Three compact specification fields.
- A short description and featured-record indicator.
- A fallback image when a record does not have an uploaded image.
Requirements
- EspoCRM with Ebla List Pro installed and enabled.
- Administrator access to EspoCRM.
- File access to the EspoCRM installation.
Example names used in this article
The example entity is called
Test. ReplaceTest,test, and the field names with the internal names used by your entity. Internal names are case-sensitive.
Step 1: Prepare the fields
Open Administration → Entity Manager → Test → Fields and create the fields needed by your card. The following set is enough for this property-style example.
| Label | Internal name | Field type | Example |
|---|---|---|---|
| Main Image | mainImage |
Image | Uploaded property photo |
| Price | price |
Currency | $450,000 |
| Availability | availability |
Enum | Available |
| Listing Type | listingType |
Enum | For Sale |
| Category | category |
Enum | Apartment |
| Location | location |
Varchar | Istanbul |
| Bedrooms | bedroomCount |
Integer | 3 |
| Bathrooms | bathroomCount |
Integer | 2 |
| Area | area |
Float | 165 |
| Featured | featured |
Boolean | Yes |
The standard name, description, and
assignedUser fields are also used. Add all required fields to the Detail layout
so users can enter and update the information.
Step 2: Enable List Pro card view
- Open Administration → Entity Manager → Test → Edit.
- Enable Card List View.
- Optionally enable Default View Mode Is Card.
- Save the entity.

Enable Card List View. Enable the default card mode only when users should open this entity in card view automatically.
The listCard layout is the single place where you choose the fields used by the
card. You do not need to repeat the field list in JavaScript.
Step 3: Create the List Card layout
Open Administration → Layout Manager → Test. If
List Card is not shown in the layout menu, select Create and
create listCard as a List layout. Open it and move the fields from Step 1 into
the Enabled column.
Keep the layout in one place
List Pro passes the
listCardlayout to the card item automatically. Do not create a second field array in the JavaScript view.
The field order does not need to match the visual card because the template decides where each value is displayed.

Move every field used by the template into the List Card layout.
Step 4: Connect List Pro to your card item
Use EspoCRM's standard Custom directories. A separate module and
Resources/module.json are not required.
First, point the Test card mode to your custom card-list view. If this file already exists,
merge the recordViews.card value into its existing JSON.
custom/Espo/Custom/Resources/metadata/clientDefs/Test.json
{
"recordViews": {
"card": "custom:views/test/card-list"
}
}
Register the card stylesheet in the standard Custom metadata.
custom/Espo/Custom/Resources/metadata/app/client.json
{
"cssList": [
"__APPEND__",
"client/custom/css/test-card.css"
]
}
Why
__APPEND__mattersIt adds your CSS file to the existing stylesheet list. Without it, other registered stylesheets could be replaced.
Now create the card-list view that tells Ebla List Pro which view should render each individual card.
client/custom/src/views/test/card-list.js
define(
'custom:views/test/card-list',
['ebla-list-pro:views/record/card-list'],
function (CardListView) {
return CardListView.extend({
itemViewName: 'custom:views/test/card-item',
});
}
);
Step 5: Create the card item view
The item view prepares the field views, record link, uploaded image URL, and fallback image.
If your image field has another internal name, change every use of
mainImage and mainImageId.
client/custom/src/views/test/card-item.js
define(
'custom:views/test/card-item',
['ebla-list-pro:views/record/card-item'],
function (CardItemView) {
return CardItemView.extend({
template: 'custom:test/card-item',
data: function () {
return Object.assign(
{},
CardItemView.prototype.data.call(this),
this.model.attributes,
{
imageUrl: this.getImageUrl(),
recordUrl: '#Test/view/' + this.model.id,
}
);
},
getImageUrl: function () {
var imageId = this.model.get('mainImageId');
if (!imageId) {
return this.getFallbackImageUrl();
}
return this.getBasePath() + '?entryPoint=image&id=' +
encodeURIComponent(imageId) + '&size=large';
},
getFallbackImageUrl: function () {
return this.getBasePath() +
'client/custom/images/test-placeholder.webp';
},
afterRender: function () {
CardItemView.prototype.afterRender.call(this);
this.$el.closest('.card-list-row').addClass('test-card-shell');
this.$el.find('.test-card-image').one(
'error',
function (event) {
event.currentTarget.src = this.getFallbackImageUrl();
}.bind(this)
);
},
});
}
);
Step 6: Create the card template
EspoCRM creates a field view for every field in the listCard layout. Place those
field views in the template with names such as priceField and
availabilityField. Triple braces are required because EspoCRM field views return
safe HTML.
client/custom/res/templates/test/card-item.tpl
<article class="test-card">
<a class="test-card-media" href="{{recordUrl}}" aria-label="{{name}}">
<img
class="test-card-image"
src="{{imageUrl}}"
alt="{{name}}"
loading="lazy"
>
<div class="test-card-status field" data-name="availability">
{{{availabilityField}}}
</div>
<div class="test-card-price field" data-name="price">
{{{priceField}}}
</div>
</a>
{{#unless rowActionsDisabled}}
<div class="item-menu-container test-card-menu">{{{itemMenu}}}</div>
{{/unless}}
<div class="test-card-content">
<div class="test-card-tags">
<div class="field" data-name="listingType">{{{listingTypeField}}}</div>
<div class="field" data-name="category">{{{categoryField}}}</div>
</div>
<h3 class="test-card-title field" data-name="name">{{{nameField}}}</h3>
<div class="test-card-location">
<span class="fas fa-map-marker-alt" aria-hidden="true"></span>
<div class="field" data-name="location">{{{locationField}}}</div>
</div>
{{#if description}}
<p class="test-card-description">{{description}}</p>
{{/if}}
<div class="test-card-specs">
<div>
<span class="fas fa-bed" aria-hidden="true"></span>
<span class="field" data-name="bedroomCount">{{{bedroomCountField}}}</span>
</div>
<div>
<span class="fas fa-bath" aria-hidden="true"></span>
<span class="field" data-name="bathroomCount">{{{bathroomCountField}}}</span>
</div>
<div>
<span class="fas fa-ruler-combined" aria-hidden="true"></span>
<span class="field" data-name="area">{{{areaField}}}</span>
<span>m²</span>
</div>
</div>
<div class="test-card-footer">
{{#if featured}}
<strong><span class="fas fa-star"></span> Featured</strong>
{{else}}
<span>Test</span>
{{/if}}
{{#if assignedUserName}}
<div class="field" data-name="assignedUser">{{{assignedUserField}}}</div>
{{/if}}
</div>
</div>
</article>
Step 7: Add the card styling
This CSS creates a responsive image card and stays scoped to the custom Test card. You can safely change its radius, shadow, spacing, and colors.
client/custom/css/test-card.css
.card-list-inner > .card-list-row.test-card-shell {
overflow: hidden;
padding: 0;
border: 1px solid rgba(127, 127, 127, .16);
border-radius: 14px;
background: var(--panel-bg, #fff);
box-shadow: 0 8px 24px rgba(28, 39, 49, .1);
}
.test-card {
position: relative;
min-width: 0;
}
.test-card .inline-edit-link {
display: none;
}
.test-card-media {
position: relative;
display: block;
overflow: hidden;
aspect-ratio: 16 / 9;
background: rgba(127, 127, 127, .12);
}
.test-card-media::after {
position: absolute;
right: 0;
bottom: 0;
left: 0;
height: 55%;
background: linear-gradient(to top, rgba(10, 16, 22, .72), transparent);
content: "";
}
.test-card-image {
width: 100%;
height: 100%;
object-fit: cover;
}
.test-card-status {
position: absolute;
z-index: 2;
top: 12px;
left: 12px;
}
.test-card-price {
position: absolute;
z-index: 2;
right: 14px;
bottom: 12px;
color: #fff;
font-size: 21px;
font-weight: 700;
text-shadow: 0 1px 3px rgba(0, 0, 0, .45);
}
.test-card-menu {
position: absolute;
z-index: 4;
top: 9px !important;
right: 9px !important;
}
.test-card-content {
padding: 15px 16px 14px;
}
.test-card-tags,
.test-card-location,
.test-card-footer {
display: flex;
align-items: center;
gap: 7px;
}
.test-card-title {
overflow: hidden;
margin: 9px 0 6px;
font-size: 17px;
text-overflow: ellipsis;
white-space: nowrap;
}
.test-card-location,
.test-card-description,
.test-card-footer {
font-size: 12px;
opacity: .7;
}
.test-card-description {
display: -webkit-box;
overflow: hidden;
min-height: 38px;
margin: 11px 0 12px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
}
.test-card-specs {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
padding: 10px 0;
border-top: 1px solid rgba(127, 127, 127, .16);
border-bottom: 1px solid rgba(127, 127, 127, .16);
}
.test-card-specs > div {
display: flex;
align-items: center;
justify-content: center;
gap: 5px;
font-size: 12px;
font-weight: 600;
}
.test-card-footer {
justify-content: space-between;
padding-top: 11px;
}
@media (max-width: 480px) {
.test-card-price {
font-size: 18px;
}
}
Save a fallback image at
client/custom/images/test-placeholder.webp. A landscape image with a 16:9 ratio
works best. Keep the file small so cards load quickly.
Step 8: Apply the changes
Open Administration → Rebuild and run the rebuild from the EspoCRM interface. This makes EspoCRM discover the new Custom files and metadata.
When the rebuild finishes, clear the browser cache or perform a hard refresh. A normal page refresh may continue using an older JavaScript file.
Step 9: Test the result

The finished card view includes images, EspoCRM field formatting, record actions, and pagination. The highlighted button switches the list to card view.
- Open the Test list and select the card-view button.
- Confirm that records with images show the uploaded image.
- Confirm that records without images show the fallback image.
- Check that the price uses the configured currency symbol and number format.
- Open a card and confirm that it links to the correct record.
- Test desktop, tablet, and mobile widths.
- Test with a user who has limited field permissions.
You are finished when
The card view loads without errors, every card has an image, all values match the record, and the layout remains readable on a small screen.
Common problems
The card view keeps loading
Confirm that both JavaScript view names use the custom: namespace and that
recordViews.card points to custom:views/test/card-list. Then run
Administration → Rebuild and perform a hard refresh.
A value is empty
Add the field to the List Card layout in Layout Manager. Then confirm that the template uses
the correct field-view name, for example {{{priceField}}} for the
price field.
The image is broken
Check the image field's internal name. An image field called mainImage uses
mainImageId. Also confirm that the fallback image exists at the path used by
getFallbackImageUrl().
The CSS is not applied
Confirm that the CSS file is registered in
custom/Espo/Custom/Resources/metadata/app/client.json, that the list starts with
"__APPEND__", and that Administration → Rebuild was run
afterward.
The card shows old JavaScript
Run Administration → Rebuild, clear the browser cache, and perform a hard refresh.
Safe customization tips
- Keep all card CSS under a unique class such as
.test-card. - Use EspoCRM field views for currency, enums, links, and translated values.
- Use an image field instead of storing a public image URL in a text field.
- Always include a fallback image.
- Keep the customization under
custom/Espo/Customandclient/custom. - Do not edit Ebla List Pro's original files.
- Copy or package the Custom files when moving the card to another server.