Card
Displays a card with a header, content and footer.
- v1.0.0
- SSR
- Zoneless
- 0.7 kB gzip
import { Component } from '@angular/core';
import { Button } from '@/ui/button/button';
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/ui/card/card';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';
/** A sign-in form in a card. */
@Component({
selector: 'example-card-demo',
imports: [
Button,
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
Input,
Label,
],
template: `
<section ndCard class="w-full max-w-sm" aria-labelledby="card-demo-title">
<div ndCardHeader>
<h2 ndCardTitle id="card-demo-title">Sign in to your account</h2>
<p ndCardDescription>Enter your email below to sign in.</p>
<div ndCardAction>
<a ndButton variant="link" href="#sign-up">Sign up</a>
</div>
</div>
<form ndCardContent id="card-demo-form" class="grid gap-6">
<div class="grid gap-2">
<label ndLabel for="card-demo-email">Email</label>
<input
ndInput
id="card-demo-email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<label ndLabel for="card-demo-password">Password</label>
<a
href="#forgot"
class="ms-auto text-sm underline-offset-4 hover:underline"
>Forgot your password?</a
>
</div>
<input ndInput id="card-demo-password" type="password" required />
</div>
</form>
<div ndCardFooter class="flex-col gap-2">
<button ndButton type="submit" form="card-demo-form" class="w-full">
Sign in
</button>
</div>
</section>
`,
})
export class CardDemoExample {}
A card groups related content and actions on a bordered surface: a
summary, a form, a setting. ndCard and its parts only style the
elements they're on, so you choose the semantics: a section with a
heading, an article in a feed, or a plain div.
Installation
npx @ndop/cli add cardpnpm dlx @ndop/cli add cardyarn dlx @ndop/cli add cardbunx @ndop/cli add cardInstall the dependencies
npm install @ndop/corepnpm add @ndop/coreyarn add @ndop/corebun add @ndop/coreCopy the code into your project
At the paths your
ndop.jsonaliases point to; these use the defaults.src/app/ui/card/card.tsimport { computed, Directive, input } from '@angular/core'; import { cn } from '@/lib/utils'; /** * A card: a bordered surface grouping related content, with optional * header, content and footer parts. * * @example * ```html * <div ndCard> * <div ndCardHeader> * <h3 ndCardTitle>Notifications</h3> * <p ndCardDescription>You have 3 unread messages.</p> * </div> * <div ndCardContent>…</div> * <div ndCardFooter>…</div> * </div> * ``` */ @Directive({ selector: '[ndCard]', host: { 'data-slot': 'card', '[class]': 'classes()' }, }) export class Card { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'flex flex-col gap-6 rounded-xl border bg-card py-6 text-card-foreground shadow-sm', this.class(), ), ); } /** * A card's header: its title, description and an optional action, laid * out in a grid. * * @example * ```html * <div ndCardHeader>…</div> * ``` */ @Directive({ selector: '[ndCardHeader]', host: { 'data-slot': 'card-header', '[class]': 'classes()' }, }) export class CardHeader { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( '@container/card-header grid auto-rows-min grid-rows-[auto_auto] items-start gap-2 px-6 has-data-[slot=card-action]:grid-cols-[1fr_auto] [.border-b]:pb-6', this.class(), ), ); } /** * A card's title. Put it on the heading element that fits the page's * outline. * * @example * ```html * <h3 ndCardTitle>Notifications</h3> * ``` */ @Directive({ selector: '[ndCardTitle]', host: { 'data-slot': 'card-title', '[class]': 'classes()' }, }) export class CardTitle { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('leading-none font-semibold', this.class()), ); } /** * Supporting text under a card's title. * * @example * ```html * <p ndCardDescription>You have 3 unread messages.</p> * ``` */ @Directive({ selector: '[ndCardDescription]', host: { 'data-slot': 'card-description', '[class]': 'classes()' }, }) export class CardDescription { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('text-sm text-muted-foreground', this.class()), ); } /** * An action in a card's header, at its end, such as a button or menu. * * @example * ```html * <div ndCardAction><button ndButton variant="link">Sign up</button></div> * ``` */ @Directive({ selector: '[ndCardAction]', host: { 'data-slot': 'card-action', '[class]': 'classes()' }, }) export class CardAction { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn( 'col-start-2 row-span-2 row-start-1 self-start justify-self-end', this.class(), ), ); } /** * A card's main content. * * @example * ```html * <div ndCardContent>…</div> * ``` */ @Directive({ selector: '[ndCardContent]', host: { 'data-slot': 'card-content', '[class]': 'classes()' }, }) export class CardContent { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('px-6', this.class())); } /** * A card's footer, usually its actions. * * @example * ```html * <div ndCardFooter><button ndButton>Save</button></div> * ``` */ @Directive({ selector: '[ndCardFooter]', host: { 'data-slot': 'card-footer', '[class]': 'classes()' }, }) export class CardFooter { /** Extra classes, merged last so they win. */ readonly class = input(''); protected readonly classes = computed(() => cn('flex items-center px-6 [.border-t]:pt-6', this.class()), ); }src/app/lib/utils.ts/** * Class helpers used by every styled item. They come from `@ndop/core`; * this file is yours, so it is the place to customize them, for example by * wrapping `cn` with an extended tailwind-merge configuration for your own * utilities. * * @packageDocumentation */ export { cn, variants, type ClassValue, type VariantOf } from '@ndop/core';Update the import paths to match your project
The files import each other through the
@/uiand@/libaliases.
Usage
import { Button } from '@/ui/button/button';
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/ui/card/card';
import { Input } from '@/ui/input/input';
import { Label } from '@/ui/label/label';<section ndCard class="w-full max-w-sm" aria-labelledby="card-demo-title">
<div ndCardHeader>
<h2 ndCardTitle id="card-demo-title">Sign in to your account</h2>
<p ndCardDescription>Enter your email below to sign in.</p>
<div ndCardAction>
<a ndButton variant="link" href="#sign-up">Sign up</a>
</div>
</div>
<form ndCardContent id="card-demo-form" class="grid gap-6">
<div class="grid gap-2">
<label ndLabel for="card-demo-email">Email</label>
<input
ndInput
id="card-demo-email"
type="email"
placeholder="m@example.com"
required
/>
</div>
<div class="grid gap-2">
<div class="flex items-center">
<label ndLabel for="card-demo-password">Password</label>
<a
href="#forgot"
class="ms-auto text-sm underline-offset-4 hover:underline"
>Forgot your password?</a
>
</div>
<input ndInput id="card-demo-password" type="password" required />
</div>
</form>
<div ndCardFooter class="flex-col gap-2">
<button ndButton type="submit" form="card-demo-form" class="w-full">
Sign in
</button>
</div>
</section>Examples
Card with action
A card with a header action, content and footer.
Pro plan
Billed monthly, renews on November 1.
$24 / month
- Unlimited projects
- 10 team members
- Priority support
import { Component } from '@angular/core';
import { Badge } from '@/ui/badge/badge';
import { Button } from '@/ui/button/button';
import {
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
} from '@/ui/card/card';
/** A card with a header action, content and a footer. */
@Component({
selector: 'example-card-with-action',
imports: [
Badge,
Button,
Card,
CardAction,
CardContent,
CardDescription,
CardFooter,
CardHeader,
CardTitle,
],
template: `
<section
ndCard
class="w-full max-w-sm"
aria-labelledby="card-with-action-title"
>
<div ndCardHeader>
<h2 ndCardTitle id="card-with-action-title">Pro plan</h2>
<p ndCardDescription>Billed monthly, renews on November 1.</p>
<div ndCardAction><span ndBadge variant="secondary">Current</span></div>
</div>
<div ndCardContent>
<p class="text-3xl font-semibold">
$24<span class="text-sm font-normal text-muted-foreground">
/ month</span
>
</p>
<ul class="mt-4 grid gap-2 text-sm text-muted-foreground">
<li>Unlimited projects</li>
<li>10 team members</li>
<li>Priority support</li>
</ul>
</div>
<div ndCardFooter class="gap-2 border-t">
<button ndButton variant="outline">Cancel plan</button>
<button ndButton class="ms-auto">Upgrade</button>
</div>
</section>
`,
})
export class CardWithActionExample {}
Accessibility
Card has no keyboard interaction or focus handling of its own; it keeps the semantics of the element it's on.
API reference
Card
[ndCard]A card: a bordered surface grouping related content, with optional header, content and footer parts.
| Card binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card |
CardHeader
[ndCardHeader]A card's header: its title, description and an optional action, laid out in a grid.
| CardHeader binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-header |
CardTitle
[ndCardTitle]A card's title. Put it on the heading element that fits the page's outline.
| CardTitle binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-title |
CardDescription
[ndCardDescription]Supporting text under a card's title.
| CardDescription binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-description |
CardAction
[ndCardAction]An action in a card's header, at its end, such as a button or menu.
| CardAction binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-action |
CardContent
[ndCardContent]A card's main content.
| CardContent binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-content |
CardFooter
[ndCardFooter]A card's footer, usually its actions.
| CardFooter binding | Type | Default |
|---|---|---|
[class]Extra classes, merged last so they win. | string | '' |
| Data attribute | Values |
|---|---|
data-slot | card-footer |
Agent notes
Do
- Give each card a heading with
ndCardTitle, at the right level for the page. - Use
sectionwitharia-labelledbypointing at the title when the card is a meaningful region, orarticlewhen it stands alone. - Put the card's main action in
ndCardFooterorndCardAction.
Don't
- Don't make a whole card clickable with a handler on the
div; put a link or button inside it. - Don't nest cards; use separators or headings inside one card.