Card
<oge-card> is a content surface with an optional header, full-bleed media, an action row and a footer — one component, not a sub-component army. The sections are attribute slots ([ogeCardMedia], [ogeCardActions], [ogeCardFooter]…) and everything else projected is the content.
There is no WAI-ARIA card pattern, so the card renders no role and no interactive wrapper of its own; add role="article" or role="region" on the host where the context calls for it.
Basics
Simple titles come from the header / subheader inputs (the PrimeNG names — a title input would double as a native tooltip). The default projection is the content; no marker element to remember.
Four days above the tree line, one pass a day.
last action → —
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardActions } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardActions],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-card header="Mountains" subheader="Alps, 2026">
<p>Four days above the tree line, one pass a day.</p>
<div ogeCardActions align="end">
<button type="button" (click)="share()">Share</button>
</div>
</oge-card>
`,
})
export class Demo {
protected share(): void {}
}Chrome presets
stylingMode is the house word with the layout family's values plus Material's raised: outlined (default), raised, filled, flat. raised rests on the --oge-shadow-card token, so a theme re-tunes elevation without touching the component.
The outlined chrome preset.
The raised chrome preset.
The filled chrome preset.
The flat chrome preset.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- outlined is the default; raised rests on the --oge-shadow-card
token, filled sits on the header surface, flat is for a card
nested inside another surface. -->
<oge-card header="Outlined">…</oge-card>
<oge-card header="Raised" stylingMode="raised">…</oge-card>
<oge-card header="Filled" stylingMode="filled">…</oge-card>
<oge-card header="Flat" stylingMode="flat">…</oge-card>
`,
})
export class Demo {}Density
size is the family's density preset (sm / md / lg): it scales the section padding and type ramp together, and --oge-card-pad is the per-card escape hatch.
The sm density.
The md density.
The lg density.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- size scales the section padding and type ramp together;
--oge-card-pad is the per-card escape hatch. -->
<oge-card header="Compact" size="sm">…</oge-card>
<oge-card header="Default">…</oge-card>
<oge-card header="Comfortable" size="lg">…</oge-card>
`,
})
export class Demo {}Media
[ogeCardMedia] is full-bleed — it touches the card edges while the sections around it keep the padding. Size it with your own CSS (aspect-ratio, block-size); there is deliberately no aspectRatio input. The heading stays before the media in DOM order.
Media renders edge to edge, clipped by the card radius.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardMedia } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardMedia],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Media is full-bleed: it touches the card edges while the sections
around it keep the padding. Size it with your own CSS — there is
deliberately no aspectRatio input. -->
<oge-card header="Mountains" subheader="Alps, 2026">
<img
ogeCardMedia
src="https://picsum.photos/seed/oge-alps/640/360"
alt=""
style="aspect-ratio: 16 / 9"
/>
<p>The heading stays before the media in DOM order, so a screen
reader announces the card by its title first.</p>
</oge-card>
`,
})
export class Demo {}Horizontal
orientation="horizontal" turns the card into a two-column grid: the media spans the inline-start column and --oge-card-media-size sets its width. Kendo is the only reference with an orientation input at all.
The media column follows the writing mode, so it mirrors in RTL with no flag to set.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardMedia } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardMedia],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The media moves into an inline-start column spanning every other
section; --oge-card-media-size sets the column width. -->
<oge-card
header="Mountains"
orientation="horizontal"
style="--oge-card-media-size: 160px"
>
<img ogeCardMedia src="https://picsum.photos/seed/oge-lake/320/320" alt="" />
<p>Vertical is the default; Kendo is the only reference with an
orientation input at all.</p>
</oge-card>
`,
})
export class Demo {}Header slots
The header row renders only when it has something to show — titles, an [ogeCardAvatar] or [ogeCardHeaderActions]. Header actions are real controls in the Tab sequence; the card never wraps them in anything interactive.
Reached the ridge before the weather turned.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardAvatar, OgeCardHeaderActions } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardAvatar, OgeCardHeaderActions],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- The header row renders only when it has something to show:
titles, an avatar or header actions. The actions are real
controls in the Tab sequence — the card never wraps them in
anything interactive. -->
<oge-card header="R. Aydın" subheader="3 hours ago">
<img ogeCardAvatar src="https://i.pravatar.cc/80?img=12" alt="" />
<div ogeCardHeaderActions>
<button type="button" aria-label="More options" (click)="menu()">⋮</button>
</div>
<p>Reached the ridge before the weather turned.</p>
</oge-card>
`,
})
export class Demo {
protected menu(): void {}
}Actions alignment
align on the actions row is the Kendo superset: start (the Material/Kendo default), center, end, and stretched — every action takes equal width.
Unsaved changes.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardActions } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardActions],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- align follows the references: start (the Material/Kendo default),
center, end, and Kendo's stretched — every action takes equal width. -->
<oge-card header="Draft">
<p>Unsaved changes.</p>
<div ogeCardActions align="stretched">
<button type="button" (click)="discard()">Discard</button>
<button type="button" (click)="save()">Save</button>
</div>
</oge-card>
`,
})
export class Demo {
protected discard(): void {}
protected save(): void {}
}Footer & separator
[ogeCardFooter] is a divided strip on the header surface — metadata rather than commands. [ogeCardSeparator] on an <hr> draws a full-bleed hairline inside the padded content.
Generated from last week's data.
12 pages, 4 charts.
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, OgeCardFooter, OgeCardSeparator } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardFooter, OgeCardSeparator],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<oge-card header="Weekly report">
<p>Generated from last week's data.</p>
<hr ogeCardSeparator />
<p>12 pages, 4 charts.</p>
<div ogeCardFooter>Updated 2 hours ago</div>
</oge-card>
`,
})
export class Demo {}Status & loading
severity draws a status rail on the inline-start edge — the toast's rail idiom on a static surface. loading swaps the content and action row for a shimmer skeleton and marks the card aria-busy; header, media and footer keep the footprint while the data arrives.
The e2e stage timed out after 20 minutes.
import { ChangeDetectionStrategy, Component, signal } from '@angular/core';
import { OgeCard, OgeCardActions } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard, OgeCardActions],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- severity draws a status rail on the inline-start edge (the
toast's rail idiom); loading swaps the content and actions for a
shimmer skeleton and marks the card aria-busy — header, media and
footer keep the footprint while the data arrives. -->
<oge-card header="Deploy failed" subheader="build #412" severity="danger">
<p>The e2e stage timed out after 20 minutes.</p>
<div ogeCardActions align="end">
<button type="button" (click)="retry()">Retry</button>
</div>
</oge-card>
<oge-card header="Weekly report" [loading]="pending()" />
`,
})
export class Demo {
protected readonly pending = signal(true);
protected retry(): void {}
}Clickable cards, accessibly
There is no clickable input, on purpose: wrapping the whole card in a link or button is the nested-interactive axe violation the moment a second control appears, and a screen reader reads the entire card as one link name. The accessible pattern is one primary <a> in the content with a CSS-stretched hit area — and [interactive] is its visual half: a hover/focus-within lift with no role, tabindex or wrapper of its own.
Four days above the tree line.
Read the full reportlast action → —
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard } from '@oge-ui/layout';
@Component({
selector: 'demo-root',
imports: [OgeCard],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- There is no clickable input, on purpose: wrapping the card in a
link or button is the nested-interactive axe violation the moment
a second control appears, and a screen reader would read the whole
card as one link name. Instead: ONE primary <a> in the content and
a CSS stretched hit area over the positioned card —
.oge-card { position: relative; }
.card-link::after { content: ''; position: absolute; inset: 0; }
Other controls stay usable by raising them above the overlay
(position: relative + z-index). [interactive] is the visual half:
a hover/focus-within lift and ring, with no role, tabindex or
wrapper of its own. -->
<oge-card
header="Mountains"
subheader="Alps, 2026"
[interactive]="true"
style="position: relative"
>
<p>Four days above the tree line.</p>
<a class="card-link" href="/trips/alps-2026">Read the full report</a>
</oge-card>
`,
})
export class Demo {}Configuration
provideOgeCardConfig() carries stylingMode and orientation defaults; instance inputs win. There is no messages block, deliberately — the card renders no user-facing strings and no interactive chrome of its own.
What every card looks like under provideOgeCardConfig({ stylingMode: 'raised' }).
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { OgeCard, provideOgeCardConfig } from '@oge-ui/layout';
// Application- or component-scoped defaults. There is no messages
// block: the card renders no user-facing strings of its own.
export const cardProviders = [
provideOgeCardConfig({ stylingMode: 'raised' }),
];
@Component({
selector: 'demo-root',
imports: [OgeCard],
changeDetection: ChangeDetectionStrategy.OnPush,
template: `
<!-- Instance inputs win over the provided defaults. -->
<oge-card header="From config">…</oge-card>
<oge-card header="Overridden" stylingMode="outlined">…</oge-card>
`,
})
export class Demo {}