Engineering 6 min read
Building modern SaaS interfaces
Reusable UI with scoped CSS variables, modular containers and a minimal JavaScript registry.
A structural surface with named regions — media, head, subhead, body, sections, subfoot and a three-way foot. SCSS-only: compose the regions you need, skip the rest.
Every region is optional and order-free in authoring, but renders in the structural order: ng-card-media → ng-card-head (-left/-right) → ng-card-subhead → ng-card-body (with ng-card-section blocks) → ng-card-subfoot → ng-card-foot (-left/-center/-right). Other components nest naturally inside: badges, buttons, progress, dropdowns, grids.
Head with icon and badge, one body section, foot with meta and action:
Design system rollout for the customer portal: tokens, dark mode and the component inventory, shipped in three milestones.
<div class="ng-card">
<div class="ng-card-head">
<div class="ng-card-head-left">
<i class="ph ph-rocket-launch text-purple fs-2xl"></i>
<h3>Project Aurora</h3>
</div>
<div class="ng-card-head-right">
<span class="ng-badge ng-badge-success-soft">Active</span>
</div>
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<p>Design system rollout for the customer portal: tokens, dark mode
and the component inventory, shipped in three milestones.</p>
</div>
</div>
<div class="ng-card-foot">
<div class="ng-card-foot-left">
<span class="fs-sm text-mute">Updated 2 hours ago</span>
</div>
<div class="ng-card-foot-right">
<a href="#" class="ng-btn ng-btn-primary sm">Open</a>
</div>
</div>
</div>
All regions together — subhead and subfoot draw their dashed separator lines, the body stacks sections with a consistent gap. Real components inside: a flex-grid stats row and a progress bar.
Traffic and conversions across all active campaigns.
Total views
+12.3%
Conversions
+8.1%
Data refreshes every 5 minutes.
<div class="ng-card">
<div class="ng-card-head">
<div class="ng-card-head-left">
<i class="ph ph-chart-line-up text-info fs-2xl"></i>
<h3>Performance Analytics</h3>
</div>
<div class="ng-card-head-right">
<span class="ng-badge ng-badge-indigo">LIVE</span>
</div>
</div>
<div class="ng-card-subhead">
<p class="fs-sm text-mute">Traffic and conversions across all active campaigns.</p>
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<div class="ng-row">
<div class="ng-col-6">
<p class="fs-sm text-mute">Total views</p>
<h2>24.5K</h2>
<p class="fs-sm text-aguagreen">+12.3%</p>
</div>
<div class="ng-col-6">
<p class="fs-sm text-mute">Conversions</p>
<h2>1.2K</h2>
<p class="fs-sm text-info">+8.1%</p>
</div>
</div>
</div>
<div class="ng-card-section">
<div class="ng-progress ng-progress-info" style="--ng-progress-value: 68%">
<div class="ng-progress-bar"></div>
</div>
</div>
</div>
<div class="ng-card-subfoot">
<p class="fs-sm">Data refreshes every 5 minutes.</p>
</div>
<div class="ng-card-foot">
<div class="ng-card-foot-left">
<a href="#" class="text-info">View full report</a>
</div>
<div class="ng-card-foot-right">
<i class="ph-duotone ph-share-network"></i>
<i class="ph-duotone ph-paper-plane-tilt"></i>
</div>
</div>
</div>
New in v1.2: .ng-card-head-titles stacks a .ng-card-title over a .ng-card-subtitle inside head-left, without forcing the whole head-left into a column.
Title above, subtitle below — without forcing head-left into a column.
<div class="ng-card">
<div class="ng-card-head">
<div class="ng-card-head-left">
<i class="ph ph-user-circle text-purple fs-2xl"></i>
<div class="ng-card-head-titles">
<div class="ng-card-title">Ada Lovelace</div>
<div class="ng-card-subtitle">Lead engineer</div>
</div>
</div>
<div class="ng-card-head-right">
<span class="ng-badge ng-badge-success-soft">Online</span>
</div>
</div>
<div class="ng-card-body">
<div class="ng-card-section"><p>Title above, subtitle below — without forcing head-left into a column.</p></div>
</div>
</div>
ng-card-media bleeds to the card edges and rounds the top corners; hover lifts the card on mouse-over. Editorial composition with NG parts only: soft badge + icon meta in the body, tag chips in the subfoot, avatar and arrow link in the foot. Two cards in a lanes grid spaced with ng-gap-lg:
Engineering 6 min read
Reusable UI with scoped CSS variables, modular containers and a minimal JavaScript registry.
Release 02 Jun 2026
A stable component contract, two grid systems and a fully brandable token layer.
<div class="ng-grid ng-lanes-md-2 ng-gap-lg">
<article class="ng-card hover">
<div class="ng-card-media">
<img src="https://picsum.photos/seed/nexigrid-post/900/500" alt="Post cover">
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<p class="fs-sm">
<span class="ng-badge ng-badge-info-soft">Engineering</span>
<span class="text-mute ml-8"><i class="ph ph-clock"></i> 6 min read</span>
</p>
<h3>Building modern SaaS interfaces</h3>
<p>Reusable UI with scoped CSS variables, modular containers
and a minimal JavaScript registry.</p>
</div>
</div>
<div class="ng-card-foot">
<div class="ng-card-foot-left">
<span class="ng-avatar sm circle"><img src="https://i.pravatar.cc/120?img=12" alt="Author"></span>
<span class="fs-sm">Claudio Pistidda · 12 Mar 2026</span>
</div>
<div class="ng-card-foot-right">
<a href="#" class="text-info fs-sm">Read <i class="ph ph-arrow-right"></i></a>
</div>
</div>
</article>
<article class="ng-card hover">
<div class="ng-card-media">
<img src="https://picsum.photos/seed/nexigrid-release/900/500" alt="Release cover">
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<p class="fs-sm">
<span class="ng-badge ng-badge-purple-soft">Release</span>
<span class="text-mute ml-8"><i class="ph ph-calendar-blank"></i> 02 Jun 2026</span>
</p>
<h3>Nexigrid 1.0 is here</h3>
<p>A stable component contract, two grid systems and a
fully brandable token layer.</p>
</div>
</div>
<div class="ng-card-subfoot">
<span class="ng-chip mr-8">tokens</span>
<span class="ng-chip mr-8">dark-mode</span>
<span class="ng-chip">grid</span>
</div>
<div class="ng-card-foot">
<div class="ng-card-foot-left">
<span class="ng-avatar sm circle"><img src="https://i.pravatar.cc/120?img=32" alt="Author"></span>
<span class="fs-sm">NexiGrid Team</span>
</div>
<div class="ng-card-foot-right">
<a href="#" class="text-purple fs-sm">Changelog <i class="ph ph-arrow-right"></i></a>
</div>
</div>
</article>
</div>
Add horizontal: the media takes a 42% column and the image covers it. Collapses back to vertical under the md breakpoint.
One token override file, zero component rewrites: how the L1/L2 variable model carried a full visual refresh.
Read the story<div class="ng-card horizontal">
<div class="ng-card-media">
<img src="https://picsum.photos/seed/nexigrid-travel/900/700" alt="Cover">
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<span class="ng-badge ng-badge-aguagreen-soft">Case study</span>
<h3 class="mt-16">Rebranding a booking platform in 6 weeks</h3>
<p>One token override file, zero component rewrites: how the
L1/L2 variable model carried a full visual refresh.</p>
<a href="#" class="ng-btn ng-btn-primary sm mt-16">Read the story</a>
</div>
</div>
</div>
compact re-scopes the padding and gap variables for denser layouts:
Standard padding and gaps from the scoped variables.
Tighter padding and gaps — denser dashboards.
<div class="ng-grid ng-lanes-md-2 ng-gap-lg">
<div class="ng-card">
<div class="ng-card-head">
<div class="ng-card-head-left"><h3>Default density</h3></div>
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<p>Standard padding and gaps from the scoped variables.</p>
</div>
</div>
</div>
<div class="ng-card compact">
<div class="ng-card-head">
<div class="ng-card-head-left"><h3>Compact</h3></div>
</div>
<div class="ng-card-body">
<div class="ng-card-section">
<p>Tighter padding and gaps — denser dashboards.</p>
</div>
</div>
</div>
</div>
The three foot areas share the row equally and justify their content left, center and right; under md they stack full-width:
The foot splits into three flexible areas.
<div class="ng-card">
<div class="ng-card-body">
<div class="ng-card-section">
<p>The foot splits into three flexible areas.</p>
</div>
</div>
<div class="ng-card-foot">
<div class="ng-card-foot-left">
<span class="ng-badge ng-badge-clight">left</span>
</div>
<div class="ng-card-foot-center">
<span class="ng-badge ng-badge-clight">center</span>
</div>
<div class="ng-card-foot-right">
<span class="ng-badge ng-badge-clight">right</span>
</div>
</div>
</div>
Everything visual reads from scoped variables — override them on a wrapper instead of restyling the card:
.my-dashboard .ng-card {
--ng-card-radius: var(--ng-radius-lg);
--ng-card-shadow: none;
--ng-card-padding: 1.25rem;
}
--ng-card-bg / --ng-card-color | Surface and text colour (defaults follow the theme). |
|---|---|
--ng-card-radius | Corner radius — defaults to --ng-radius-md via fallback-at-use. |
--ng-card-border-width / --ng-card-border-color | Border (defaults from the global border tokens). |
--ng-card-shadow / --ng-card-shadow-hover | Resting and hover elevation. |
--ng-card-padding / --ng-card-body-padding / --ng-card-header-padding / --ng-card-footer-padding | Region padding. |
--ng-card-gap / --ng-card-section-gap / --ng-card-gap-foot | Stacking gaps for body, sections and foot. |
ng-card ng-card-media ng-card-head ng-card-head-left ng-card-head-right ng-card-subhead ng-card-body ng-card-section ng-card-subfoot ng-card-foot ng-card-foot-left ng-card-foot-center ng-card-foot-right hover compact horizontal Generated from the component source (_ng_card.scss).
| Name | Card |
|---|---|
| Type | css |
| Version | 1.4.0 |
| Status | css |
| CSS | /assets/css/components/_ng_card.scss |
.ng-btn .ng-card .ng-card-body .ng-card-foot .ng-card-foot-center .ng-card-foot-left .ng-card-foot-right .ng-card-head .ng-card-head-left .ng-card-head-right .ng-card-head-titles .ng-card-media .ng-card-section .ng-card-subfoot .ng-card-subhead .ng-card-subtitle .ng-card-title --ng-card-bg --ng-card-border-color --ng-card-border-topbot-line --ng-card-border-width --ng-card-color --ng-card-divider-color --ng-card-divider-style --ng-card-divider-width --ng-card-foot-border-top --ng-card-foot-gap-x --ng-card-foot-gap-y --ng-card-foot-pad-y --ng-card-gap --ng-card-gap-block --ng-card-head-border-bottom --ng-card-head-gap-x --ng-card-head-gap-y --ng-card-head-pad-y --ng-card-pad-x --ng-card-section-gap --ng-card-sub-color