Author: Me. Blame me if it breaks.
Here’s how I built a custom ServiceNow widget that transforms ordinary links into vibrant, center-aligned cards.
Stylish? Absolutely.
Overengineered? Maybe.
Steps to Create this Custom Quick Link Widget
<!-- Quick Link Card -->
<div class="all-card-container">
<a class="quick-links card"
aria-labelledby="{{data.instanceId}}-quick-link"
id="{{data.instanceId}}-quick-link"
ng-href="{{data.json_data[0].url}}"
ng-click="clicked(data.json_data[0])"
ng-attr-target="{{data.json_data[0].target}}"
tabindex="0">
<div class="circle">
<div class="icon"
ng-if="data.json_data[0].icon && data.json_data[0].icon.length"
style="background-image: url({{data.json_data[0].icon}});"></div>
</div>
<div class="overlay"></div>
<!-- Title -->
<p class="card-title">{{data.title}}</p>
<!-- Short description -->
<p class="card-description">{{data.short_description}}</p>
</a>
</div>
.quick-links {
--bg-color: #DCE9FF;
--bg-color-light: #f1f7ff;
--text-color-hover: #4C5656;
--box-shadow-color: rgba(220, 233, 255, 0.48);
}
.all-card-container {
display: flex;
justify-content: center;
width: 260px;
margin: auto;
margin-top: -8rem;
}
.quick-links.card {
width: 250px;
height: 321px;
background: #fff;
border: none;
overflow: hidden;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
position: relative;
transition: all 0.3s ease-out;
text-decoration: none;
border-radius: 5px;
}
.quick-links.card:hover {
transform: translateY(-25px) scale(1.005) translateZ(0);
box-shadow: 0 24px 36px rgba(0,0,0,0.11),
0 24px 46px var(--box-shadow-color);
}
.quick-links.card:hover .overlay {
transform: scale(30);
}
.quick-links.card:hover .circle {
border-color: var(--bg-color-light);
background: var(--bg-color);
}
.quick-links.card:hover .circle:after {
background: var(--bg-color-light);
}
.quick-links.card:hover p {
color: var(--text-color-hover);
}
.quick-links.card p {
font-size: 17px;
color: #4C5656;
margin-top: 30px;
z-index: 1;
transition: color 0.3s ease-out;
}
.quick-links .circle {
width: 131px;
height: 131px;
border-radius: 50%;
background: #fff;
border: 3px solid var(--bg-color);
display: flex;
justify-content: center;
align-items: center;
position: relative;
z-index: 1;
transition: all 0.3s ease-out;
}
.quick-links .circle:after {
content: "";
width: 118px;
height: 118px;
display: block;
position: absolute;
background: var(--bg-color);
border-radius: 50%;
transition: opacity 0.3s ease-out;
}
.quick-links .circle .icon {
width: 64px;
height: 64px;
background-size: cover;
z-index: 10000;
position: relative;
}
.quick-links .overlay {
width: 20px;
height: 20px;
border-radius: 50%;
background: var(--bg-color);
position: absolute;
top: 100px;
left: 100px;
z-index: 0;
transition: transform 0.3s ease-out;
}
.card-title {
text-align: center;
font-weight: 600;
font-size: 18px;
margin: 10px 0 0;
}
.card-description {
text-align: center;
font-size: 14px;
margin: 6px 0 0;
color: #666;
}
.col-md-3 {
width: 24%;
}
(function () {
data.instanceId = $sp.getDisplayValue("sys_id");
var quickLinkId = $sp.getParameter("quick_link_id");
var pageId = $sp.getParameter("id");
var quickLinkUtil = new sn_ex_sp.QuickLinkUtil();
// Options
data.title = (options && options.title) ? options.title : "";
data.short_description = (options && options.short_description) ? options.short_description : "";
var selectedQuickLink = (options && options.select_a_quick_link) ? options.select_a_quick_link : "";
// Fetch JSON data
if (quickLinkId && pageId === "quick_link_preview") {
data.json_data = quickLinkUtil.fetchQuicklinksForHomePage(quickLinkId, data.instanceId);
} else if (selectedQuickLink) {
data.json_data = quickLinkUtil.fetchQuicklinksForHomePage(selectedQuickLink, data.instanceId);
} else {
// Default: fetch the first Quick Link available
var gr = new GlideRecord('sn_ex_sp_quick_link');
gr.orderBy('sys_created_on'); // or any other ordering
if (gr.query() && gr.next()) {
selectedQuickLink = gr.sys_id.toString();
data.json_data = quickLinkUtil.fetchQuicklinksForHomePage(selectedQuickLink, data.instanceId);
} else {
data.json_data = [];
}
}
// Set title: options.title or Quick Link name
if (!data.title && selectedQuickLink) {
var grTitle = new GlideRecord('sn_ex_sp_quick_link');
if (grTitle.get(selectedQuickLink)) {
data.title = grTitle.name.toString();
}
}
// Handle guided help click
if (input && input.action === "openGuidedhelp" && input.guidedHelpId) {
var portal = $sp.getPortalRecord();
data.pageId = $sp.getParameter("id") ? $sp.getParameter("id") : (portal && portal.homepage.getDisplayValue());
data.guidedHelpResponse = quickLinkUtil.handleGuidedHelpClick(input.guidedHelpId, data.pageId, "quick_link");
}
})();
api.controller = function($scope, $timeout, $window) {
var c = this;
$scope.isLoading = false;
$scope.clicked = function(item) {
if (item.type === "guided_help") {
c.server.get({
action: "openGuidedhelp",
guidedHelpId: item.guided_help
}).then(function(resp) {
$timeout(function() {
$window.open(resp.data.guidedHelpResponse.url, "_self");
});
});
}
};
c.asyncGet = function() {
$scope.isLoading = true;
c.data.action = "loadData";
c.server.update().then(function() {
$scope.isLoading = false;
});
};
if (c.data.load_config === "async") {
setTimeout(c.asyncGet);
}
};
You’ll get a responsive set of colorful cards centered on the page. Each card links to a different ServiceNow page or resource. This widget works great on Employee Center portals.