Skip to main content

Coral UI

Coral UI is Adobe's web component library that powers the entire AEM Touch UI -- dialogs, the Sites console, the DAM console, the page editor toolbar, and every admin interface. When you build custom dialog fields, admin consoles, or author-facing UI in AEM, you use Coral UI components.

Coral UI 3 is built on standard Web Components (Custom Elements v1). Components are used as HTML elements with the is="coral-*" attribute or as standalone tags like <coral-dialog>.

Granite UI vs Coral UI

These two terms are often confused. They work together but at different layers:

LayerFrameworkWhere it runsWhat it does
Granite UIServer-side (Sling)AEM serverReads dialog XML, renders HTML markup with Coral classes and attributes
Coral UI 3Client-side (JS/CSS)BrowserProvides interactive web components (buttons, dialogs, selects, etc.)

When you write dialog XML like sling:resourceType="granite/ui/components/coral/foundation/form/textfield", Granite UI renders the server-side HTML, and Coral UI's JavaScript enhances it into an interactive text field in the browser.

tip

You interact with Granite UI when writing dialog XML (server-side). You interact with Coral UI when writing client-side JavaScript for dialog customisation, custom fields, or admin pages.


Component Library Reference

AEM ships with Coral UI 3. The full API is browsable on your local instance:

ResourceURL
Coral UI component exampleshttp://localhost:4502/libs/granite/ui/components/coral/foundation/form/field.html
Coral UI 3 API docshttp://localhost:4502/libs/granite/ui/references/coral-ui/coralui3/
Granite UI server-side docshttp://localhost:4502/libs/granite/ui/content/dumplibs.html

Commonly used Coral UI components

ComponentHTMLPurpose
coral-button<button is="coral-button">Buttons with icons and variants
coral-textfield<input is="coral-textfield">Text input fields
coral-textarea<textarea is="coral-textarea">Multi-line text input
coral-select<coral-select>Dropdown select
coral-checkbox<coral-checkbox>Checkbox toggle
coral-radio<coral-radio>Radio button
coral-switch<coral-switch>Toggle switch
coral-dialog<coral-dialog>Modal dialog
coral-alert<coral-alert>Alert / notification banner
coral-table<coral-table>Data table
coral-tabs<coral-tablist> + <coral-panelstack>Tabbed interface
coral-accordion<coral-accordion>Collapsible sections
coral-tag<coral-tag>Tag / chip
coral-tooltip<coral-tooltip>Tooltip on hover
coral-popover<coral-popover>Popover content
coral-progress<coral-progress>Progress bar
coral-wait<coral-wait>Loading spinner
coral-icon<coral-icon>Icon (from the Coral icon set)
coral-masonry<coral-masonry>Masonry grid layout (card view)
coral-columnview<coral-columnview>Column / Miller columns view
coral-shell<coral-shell>AEM shell (global navigation)

Setup and Integration

To use Coral UI components in a custom page or template (outside of standard dialogs), load the Coral UI libraries via a clientlib dependency.

1. Create a clientlib with Coral dependencies

ui.apps/.../clientlibs/clientlib-coral-custom/.content.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
allowProxy="{Boolean}true"
dependencies="[coralui3,granite.ui.coral.foundation]"
categories="[myproject.coral-custom]"/>
DependencyProvides
coralui3Core Coral UI 3 library (all components, JS, CSS)
granite.ui.coral.foundationGranite UI foundation components (form fields, containers)
granite.ui.shellAEM shell framework (only needed if building full admin pages)

2. Load the clientlib in your HTL template

body.html
<sly data-sly-use.clientlib="/libs/granite/sightly/templates/clientlib.html">
<sly data-sly-call="${clientlib.all @ categories='myproject.coral-custom'}"/>
</sly>

<div class="myproject-custom-ui">
<button is="coral-button" variant="primary" icon="add">Add Item</button>
</div>

The dependencies in the clientlib XML ensures Coral UI 3 is loaded before your custom code runs.


Using Coral UI Components

Buttons

<!-- Primary action button -->
<button is="coral-button" variant="primary" icon="check">Save</button>

<!-- Secondary / quiet button -->
<button is="coral-button" variant="quiet" icon="edit">Edit</button>

<!-- Warning button -->
<button is="coral-button" variant="warning" icon="delete">Delete</button>

<!-- Button with only an icon (no label) -->
<button is="coral-button" variant="quietaction" icon="more" iconsize="S"
title="More options"></button>

<!-- Disabled button -->
<button is="coral-button" variant="primary" disabled>Unavailable</button>

<!-- Button sizes -->
<button is="coral-button" variant="primary" size="M">Medium</button>
<button is="coral-button" variant="primary" size="L">Large</button>

Button variants: default, primary, secondary, warning, quiet, minimal, quietaction, action

Alerts

<!-- Info alert -->
<coral-alert variant="info">
<coral-alert-header>Information</coral-alert-header>
<coral-alert-content>This component has unsaved changes.</coral-alert-content>
</coral-alert>

<!-- Success alert -->
<coral-alert variant="success">
<coral-alert-header>Saved</coral-alert-header>
<coral-alert-content>Your changes have been saved successfully.</coral-alert-content>
</coral-alert>

<!-- Warning alert -->
<coral-alert variant="warning">
<coral-alert-header>Warning</coral-alert-header>
<coral-alert-content>This action cannot be undone.</coral-alert-content>
</coral-alert>

<!-- Error alert -->
<coral-alert variant="error">
<coral-alert-header>Error</coral-alert-header>
<coral-alert-content>Failed to save. Please try again.</coral-alert-content>
</coral-alert>

Alert variants: info, success, warning, error, help

Dialogs

<!-- Define a dialog -->
<coral-dialog id="myDialog" variant="default">
<coral-dialog-header>Confirm Action</coral-dialog-header>
<coral-dialog-content>
<p>Are you sure you want to delete this item?</p>
</coral-dialog-content>
<coral-dialog-footer>
<button is="coral-button" variant="default" coral-close>Cancel</button>
<button is="coral-button" variant="primary" id="confirmBtn">Delete</button>
</coral-dialog-footer>
</coral-dialog>
Opening and handling dialogs
// Open the dialog
var dialog = document.getElementById('myDialog');
dialog.show();

// Handle the confirm button
document.getElementById('confirmBtn').addEventListener('click', function() {
// Perform the action
dialog.hide();
});

// Listen for dialog close
dialog.on('coral-overlay:close', function() {
console.log('Dialog closed');
});

Dialog variants: default, error, warning, success, help, info

Select / Dropdown

<coral-select name="category" placeholder="Select a category">
<coral-select-item value="news">News</coral-select-item>
<coral-select-item value="blog">Blog</coral-select-item>
<coral-select-item value="product" selected>Product</coral-select-item>
<coral-select-item value="event">Event</coral-select-item>
</coral-select>
Reading the selected value
var select = document.querySelector('coral-select[name="category"]');
select.on('change', function() {
console.log('Selected:', select.value);
});

Tables

<coral-table selectable>
<coral-table-head>
<coral-table-row>
<coral-table-headercell>Name</coral-table-headercell>
<coral-table-headercell>Status</coral-table-headercell>
<coral-table-headercell>Modified</coral-table-headercell>
</coral-table-row>
</coral-table-head>
<coral-table-body>
<coral-table-row>
<coral-table-cell>Homepage</coral-table-cell>
<coral-table-cell><coral-status variant="success">Published</coral-status></coral-table-cell>
<coral-table-cell>2025-01-15</coral-table-cell>
</coral-table-row>
<coral-table-row>
<coral-table-cell>About Us</coral-table-cell>
<coral-table-cell><coral-status variant="warning">Draft</coral-status></coral-table-cell>
<coral-table-cell>2025-01-20</coral-table-cell>
</coral-table-row>
</coral-table-body>
</coral-table>

Tabs

<coral-tablist>
<coral-tab>General</coral-tab>
<coral-tab selected>Advanced</coral-tab>
<coral-tab>Permissions</coral-tab>
</coral-tablist>
<coral-panelstack>
<coral-panel>
<p>General settings content</p>
</coral-panel>
<coral-panel selected>
<p>Advanced settings content</p>
</coral-panel>
<coral-panel>
<p>Permissions settings content</p>
</coral-panel>
</coral-panelstack>

Icons

Coral UI includes a comprehensive icon set. Use the icon attribute on buttons or the <coral-icon> element:

<!-- Standalone icon -->
<coral-icon icon="edit" size="S"></coral-icon>
<coral-icon icon="delete" size="M"></coral-icon>
<coral-icon icon="check" size="L"></coral-icon>

<!-- Common icons -->
<coral-icon icon="add"></coral-icon> <!-- + -->
<coral-icon icon="close"></coral-icon> <!-- × -->
<coral-icon icon="search"></coral-icon> <!-- 🔍 -->
<coral-icon icon="settings"></coral-icon> <!-- ⚙ -->
<coral-icon icon="infoCircle"></coral-icon> <!-- ℹ -->
<coral-icon icon="alert"></coral-icon> <!-- ⚠ -->
<coral-icon icon="folder"></coral-icon>
<coral-icon icon="image"></coral-icon>
<coral-icon icon="globe"></coral-icon>
<coral-icon icon="user"></coral-icon>
<coral-icon icon="link"></coral-icon>
<coral-icon icon="code"></coral-icon>

Browse all available icons at: http://localhost:4502/libs/granite/ui/references/coral-ui/coralui3/Coral.Icon.html


Coral UI in Dialog Clientlibs

The most common use of Coral UI in custom code is dialog clientlibs -- JavaScript that runs inside component dialogs to add custom behaviour (show/hide fields, validation, dynamic defaults).

Show/hide fields based on a dropdown value

ui.apps/.../clientlibs/clientlib-dialog/js/showhide.js
(function($, Coral) {
'use strict';

// Run when the dialog loads
$(document).on('dialog-ready', function() {
initShowHide();
});

function initShowHide() {
// Find the trigger select field
var $select = $('[name="./layoutType"]');
if (!$select.length) return;

// Get the Coral select component
Coral.commons.ready($select[0], function(selectEl) {
// Initial state
toggleFields(selectEl.value);

// Listen for changes
selectEl.on('change', function() {
toggleFields(selectEl.value);
});
});
}

function toggleFields(value) {
var $imageField = $('[name="./backgroundImage"]').closest('.coral-Form-fieldwrapper');
var $colorField = $('[name="./backgroundColor"]').closest('.coral-Form-fieldwrapper');

if (value === 'image') {
$imageField.show();
$colorField.hide();
} else if (value === 'color') {
$imageField.hide();
$colorField.show();
} else {
$imageField.show();
$colorField.show();
}
}

})(jQuery, Coral);

Custom validation

ui.apps/.../clientlibs/clientlib-dialog/js/validation.js
(function($, Coral) {
'use strict';

// Register a custom validator
$(window).adaptTo('foundation-registry').register(
'foundation.validation.validator',
{
// CSS selector to match the field
selector: '[data-validation="url-pattern"]',

// Validation function
validate: function(el) {
var value = el.value;
if (!value) return; // Skip empty (use 'required' for that)

var urlPattern = /^https?:\/\/.+/;
if (!urlPattern.test(value)) {
return 'Please enter a valid URL starting with http:// or https://';
}
}
}
);

})(jQuery, Coral);

Add the validator to a dialog field via granite:data:

<linkUrl
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
fieldLabel="Link URL"
name="./linkUrl"
validation="url-pattern"/>

Setting default values dynamically

Setting a default date to today
(function($, Coral) {
'use strict';

$(document).on('dialog-ready', function() {
var $dateField = $('[name="./publishDate"]');
if (!$dateField.length) return;

Coral.commons.ready($dateField[0], function(datePicker) {
// Only set if the field is empty (new component)
if (!datePicker.value) {
var today = new Date().toISOString().split('T')[0];
datePicker.value = today;
}
});
});

})(jQuery, Coral);

Loading the dialog clientlib

Register your dialog clientlib with the cq.authoring.dialog category or reference it as an extraClientlibs property on the component:

ui.apps/.../clientlibs/clientlib-dialog/.content.xml
<?xml version="1.0" encoding="UTF-8"?>
<jcr:root xmlns:jcr="http://www.jcp.org/jcr/1.0" xmlns:cq="http://www.day.com/jcr/cq/1.0"
jcr:primaryType="cq:ClientLibraryFolder"
allowProxy="{Boolean}true"
categories="[cq.authoring.dialog]"
dependencies="[coralui3]"/>
tip

The cq.authoring.dialog category is loaded for all dialogs. If your script should only run for a specific component, scope it by checking for a field unique to that dialog:

$(document).on('dialog-ready', function() {
var $myField = $('[name="./myUniqueFieldName"]');
if (!$myField.length) // Not our dialog
// ... custom logic ...
});

Coral UI JavaScript API Patterns

Coral.commons.ready()

Wait for a Coral component to be fully initialised before interacting with it:

var element = document.querySelector('coral-select');
Coral.commons.ready(element, function(readyElement) {
// Component is fully initialised and ready
console.log('Selected value:', readyElement.value);
});

Event handling with .on()

Coral components extend HTMLElement and support standard DOM events plus Coral-specific events:

// Coral event
dialog.on('coral-overlay:open', function() { /* dialog opened */ });
dialog.on('coral-overlay:close', function() { /* dialog closed */ });

// Standard DOM event on Coral element
select.on('change', function() { /* value changed */ });

// jQuery-style with delegation
$(document).on('change', 'coral-select[name="./category"]', function(e) {
console.log('Category changed:', e.target.value);
});

Common Coral events

EventComponentFires when
changeSelect, Checkbox, Radio, SwitchValue changes
coral-overlay:openDialog, PopoverOverlay opens
coral-overlay:closeDialog, PopoverOverlay closes
coral-overlay:beforeopenDialog, PopoverBefore overlay opens (cancellable)
coral-overlay:beforecloseDialog, PopoverBefore overlay closes (cancellable)
coral-collection:addMultifield, TagListItem added
coral-collection:removeMultifield, TagListItem removed
clickButtonButton clicked
inputTextfield, TextareaUser types

foundation-registry

The Granite UI registry is used for validators, adapters, and other extension points:

var registry = $(window).adaptTo('foundation-registry');

// Register a validator
registry.register('foundation.validation.validator', { ... });

// Register a condition (for show/hide)
registry.register('foundation.condition', { ... });

// Register an adapter
registry.register('foundation.adapters', { ... });

foundation-contentloaded

Fires when new content is loaded into the DOM (e.g., a dialog opens, a multifield item is added):

$(document).on('foundation-contentloaded', function(e) {
// New content was loaded into e.target
// Re-initialise any custom logic for dynamically added fields
var $newContent = $(e.target);
$newContent.find('[data-my-widget]').each(function() {
initMyWidget(this);
});
});

Building a Custom Dialog Field

For requirements that Coral UI's built-in fields don't cover, you can create a custom Granite UI field component that renders Coral UI markup:

Server-side (Granite UI field)

ui.apps/apps/myproject/components/fields/color-picker/color-picker.html
<sly data-sly-use.field="com.adobe.granite.ui.components.Field"/>
<div class="coral-Form-fieldwrapper"
data-sly-use.cfg="com.adobe.granite.ui.components.Config">

<label class="coral-Form-fieldlabel"
data-sly-test="${cfg.fieldLabel}">${cfg.fieldLabel}</label>

<div class="myproject-color-picker">
<input is="coral-textfield"
name="${field.name}"
value="${field.value}"
type="text"
class="myproject-color-picker__input"
placeholder="${cfg.emptyText || '#000000'}"/>
<div class="myproject-color-picker__preview"
style="background-color: ${field.value || '#000000'}"></div>
</div>

<coral-icon icon="infoCircle"
size="S"
class="coral-Form-fieldinfo"
data-sly-test="${cfg.fieldDescription}"
title="${cfg.fieldDescription}"></coral-icon>
</div>

Client-side (JavaScript enhancement)

ui.apps/.../clientlibs/clientlib-dialog/js/color-picker.js
(function($, Coral) {
'use strict';

$(document).on('foundation-contentloaded', function() {
$('.myproject-color-picker').each(function() {
var $picker = $(this);
var $input = $picker.find('.myproject-color-picker__input');
var $preview = $picker.find('.myproject-color-picker__preview');

// Update preview on input change
$input.on('input', function() {
$preview.css('background-color', $input.val() || '#000000');
});
});
});

})(jQuery, Coral);

Using the custom field in a dialog

<backgroundColor
jcr:primaryType="nt:unstructured"
sling:resourceType="myproject/components/fields/color-picker"
fieldLabel="Background Color"
fieldDescription="Enter a hex color code"
emptyText="#ffffff"
name="./backgroundColor"/>

Theming and CSS Classes

Coral UI uses a set of CSS utility classes for layout and spacing:

Layout classes

ClassPurpose
coral-Form-fieldwrapperWraps a form field (label + input + description)
coral-Form-fieldlabelField label text
coral-Form-fieldinfoHelp icon / description
coral-Form-fieldThe input element itself
coral--darkDark theme (applied to a container)
coral--lightLight theme
coral--largestLargest text size
coral--largeLarge text size

Spacing with u-coral-*

<!-- Margin -->
<div class="u-coral-margin">Standard margin</div>
<div class="u-coral-marginTop">Top margin only</div>
<div class="u-coral-marginBottom">Bottom margin only</div>

<!-- Padding -->
<div class="u-coral-padding">Standard padding</div>
<div class="u-coral-noPadding">Remove padding</div>

<!-- Text alignment -->
<div class="u-coral-textAlignCenter">Centred text</div>
<div class="u-coral-textAlignRight">Right-aligned text</div>

<!-- Visibility -->
<div class="u-coral-screenReaderOnly">Hidden visually, accessible to screen readers</div>

Best Practices

Use Coral.commons.ready() before accessing components

Coral components initialise asynchronously. Always use Coral.commons.ready() or the foundation-contentloaded event before reading values or attaching listeners.

Scope dialog scripts to avoid conflicts

Dialog clientlibs with category cq.authoring.dialog run for every dialog. Always check for a unique field name before executing your logic.

Don't manipulate Coral DOM internals

Coral components manage their own internal DOM structure (shadow DOM). Don't rely on or manipulate internal elements -- use the component's JavaScript API instead:

// ❌ Bad: manipulating internal DOM
select.querySelector('.coral-Select-trigger').style.color = 'red';

// ✅ Good: using the component API
select.variant = 'warning';
select.disabled = true;

Use granite:data for custom data attributes

Pass custom data from dialog XML to your client-side JavaScript:

<myField
jcr:primaryType="nt:unstructured"
sling:resourceType="granite/ui/components/coral/foundation/form/textfield"
name="./title">
<granite:data
jcr:primaryType="nt:unstructured"
max-words="{Long}50"
show-counter="{Boolean}true"/>
</myField>
// Read in JS
var maxWords = $field.data('max-words'); // 50
var showCounter = $field.data('show-counter'); // true

Prefer built-in Granite UI fields

Before building a custom field, check if an existing Granite UI field type or Coral component covers your need. Custom fields add maintenance overhead and may break on AEM upgrades.


Common Pitfalls

PitfallSolution
Coral is not definedEnsure your clientlib has coralui3 as a dependency
Component API methods return undefinedUse Coral.commons.ready() -- the component isn't initialised yet
Dialog script runs for wrong componentScope your script by checking for a unique field name
Styles break after AEM upgradeDon't rely on internal Coral DOM structure; use the public API
foundation-contentloaded fires multiple timesNormal -- it fires for each new content load (dialog open, multifield add); make your init logic idempotent
Custom field not saving valuesEnsure the <input> has a name attribute matching ./propertyName
jQuery $ not availableUse (function($, Coral) { ... })(jQuery, Coral); wrapper

External Resources

See also