Weekly report
Sent every Monday at 9:00 to 4 people.import { Card, Heading, Text } from '@vega-ui/react';
<Card aria-labelledby='preview-card-title'>
<Heading id='preview-card-title' as='h2' size={4} fontWeight={600}>Weekly report</Heading>
<Text size={2}>Sent every Monday at 9:00 to 4 people.</Text>
</Card>- Browser support
- Widely available
- Chrome 111
- Edge 111
- Firefox 113
- Safari 15.4
- Safari on iOS 15.4
- Bundle size
- Oklab and OkLCh — comes from the design tokens
- :where()
Summary
Card is a surface container for grouped content such as settings panels, dashboard blocks, and inline summaries. It is a layout and grouping surface, not a feedback or status primitive.
Imports
import {
Card,
type CardProps,
type CardSize,
type CardAppearance,
} from '@vega-ui/react';Exported Types
CardPropsCardSizeCardAppearance
Basic Usage
import { Button, Card, Heading, Text } from '@vega-ui/react';
const features = ['Up to 25 members', '100 GB of storage', 'Priority email support', 'SAML single sign-on'];
export default function BasicUsage() {
return (
<Card aria-labelledby='plan-team-title' style={{ width: '100%', maxWidth: 320, gap: 'var(--spacing-6)' }}>
<Heading id='plan-team-title' as='h3' size={4} fontWeight={600}>Team</Heading>
<Text size={3}>
<Text size={6} fontWeight={500}>$12</Text> per member / month
</Text>
<ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-9)', display: 'grid', gap: 'var(--spacing-2)' }}>Variants
- Sizes:
sm,md,lg - Appearances:
outline,transparent - Composition: default
articleorasChild
Edge Cases
Cardis a layout surface, not a semantic replacement for headings or sections.- Transparent cards work only when the parent surface already provides contrast.
- Internal spacing still belongs to child layout decisions.
- If the UI is trying to communicate status or urgency rather than grouping content,
AlertorBadgeis usually the clearer abstraction.
Common Mistakes
- Expecting
Cardto provide all internal layout spacing automatically. - Using transparent cards on visually noisy backgrounds.
- Encoding hierarchy only through card appearance.
Examples
Basic
Basic: pricing plan
Card renders an article, named by its heading through aria-labelledby. Content stacks in a column; spacing between children comes from gap on the card.
import { Button, Card, Heading, Text } from '@vega-ui/react';
const features = ['Up to 25 members', '100 GB of storage', 'Priority email support', 'SAML single sign-on'];
export default function BasicUsage() {
return (
<Card aria-labelledby='plan-team-title' style={{ width: '100%', maxWidth: 320, gap: 'var(--spacing-6)' }}>
<Heading id='plan-team-title' as='h3' size={4} fontWeight={600}>Team</Heading>
<Text size={3}>
<Text size={6} fontWeight={500}>$12</Text> per member / month
</Text>
<ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-9)', display: 'grid', gap: 'var(--spacing-2)' }}>Basic: whole card as a link
With asChild the card styles move onto an <a>, so the entire surface is one link with one accessible name. Keep the content of a link card non-interactive: no buttons or nested links inside.
import { Card, Heading, Text } from '@vega-ui/react';
import './examples.css';
const destinations = [
{ href: '#billing', title: 'Billing', description: 'Plan, invoices and payment methods.' },
{ href: '#members', title: 'Members', description: '18 members, 3 pending invitations.' },
{ href: '#security', title: 'Security', description: 'Two-factor authentication, sessions and SSO.' },
];
export default function LinkCards() {
return (
<nav aria-label='Workspace settings' style={{ width: '100%' }}>Controlled/Stateful
Controlled/Stateful: metric card with refresh
The value, the timestamp and the loading flag live in state. While refreshing, the card is aria-busy, the spinner has its own accessible name and the button is disabled.
import { useState } from 'react';
import { Button, Card, Heading, Spinner, Text } from '@vega-ui/react';
const formatter = new Intl.NumberFormat('en-US', { style: 'currency', currency: 'USD' });
export default function MetricsCard() {
const [revenue, setRevenue] = useState(48210.4);
const [updatedAt, setUpdatedAt] = useState('09:00');
const [loading, setLoading] = useState(false);
const refresh = () => {
setLoading(true);Form/Integration
Form/Integration: the card is the form
asChild renders the card as a <form>, named by its heading. Values are read with FormData on submit, native required and pattern validate the fields, and reset clears the status message.
import { FormEvent, useState } from 'react';
import { Button, Card, Heading, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
export default function SettingsFormCard() {
const [saved, setSaved] = useState('');
const submit = (event: FormEvent<HTMLFormElement>) => {
event.preventDefault();
const data = new FormData(event.currentTarget);
setSaved(`Saved “${data.get('workspace')}” at ${data.get('slug')}.vega.app`);
};
Layout/Overlay
Layout/Overlay: responsive grid with aligned actions
auto-fill with minmax(min(220px, 100%), 1fr) gives as many columns as fit without overflowing on phones. Grid rows stretch the cards to equal height, and margin-block-start: auto keeps every button at the bottom regardless of text length.
import { Button, Card, Heading, Text } from '@vega-ui/react';
const integrations = [
{ id: 'slack', name: 'Slack', description: 'Post deployment and incident updates to channels.' },
{ id: 'github', name: 'GitHub', description: 'Link pull requests to releases and show build status on every commit, including drafts and forks.' },
{ id: 'pagerduty', name: 'PagerDuty Incident Response Orchestration', description: 'Page the on-call engineer.' },
];
export default function ResponsiveGrid() {
return (
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(auto-fill, minmax(min(220px, 100%), 1fr))', gap: 'var(--spacing-6)', width: '100%' }}>
{integrations.map((integration) => (Error
Error: data failed to load
The card keeps its heading and explains the failure in a role='alert' region with a retry action. The retry shows a loading state before the data appears.
import { useState } from 'react';
import { Button, Card, Heading, Spinner, Text } from '@vega-ui/react';
type State = 'failed' | 'loading' | 'loaded';
export default function LoadErrorCard() {
const [state, setState] = useState<State>('failed');
const retry = () => {
setState('loading');
setTimeout(() => setState('loaded'), 900);
};Disabled
Disabled: read-only details for non-owners
Card has no disabled state of its own. A <fieldset disabled> inside it disables every field at once, and the explanation is linked with aria-describedby.
import { Card, Heading, Label, Text, TextField, TextFieldInput } from '@vega-ui/react';
export default function ReadOnlyBilling() {
return (
<Card aria-labelledby='billing-contact-title' style={{ width: '100%', maxWidth: 420, gap: 'var(--spacing-12)' }}>
<div style={{ display: 'grid', gap: 'var(--spacing-2)' }}>
<Heading id='billing-contact-title' as='h3' size={4} fontWeight={600}>Billing contact</Heading>
<Text id='billing-contact-note' size={2} style={{ color: 'var(--label-secondary)' }}>
Only workspace owners can change billing details. Ask Maria Chen to update them.
</Text>
</div>
<fieldset disabled aria-describedby='billing-contact-note' style={{ border: 0, margin: 0, padding: 0, display: 'grid', gap: 'var(--spacing-8)', alignSelf: 'stretch' }}>Edge
Edge: empty state and hundreds of items
The same card handles zero and 240 items. The empty state explains what to do next; the long list scrolls inside the card, and the scroll area is focusable and named so keyboard users can scroll it.
import { useState } from 'react';
import { Button, Card, Heading, Text } from '@vega-ui/react';
const allKeys = Array.from({ length: 240 }, (_, index) => `sk_live_${String(index + 1).padStart(4, '0')}`);
export default function EmptyAndMany() {
const [keys, setKeys] = useState<string[]>([]);
const toggle = () => {
if (keys.length === 0) {
setKeys(allKeys);
return;Anatomy
Overview
Card is a single grouped surface container with no public child parts. Its anatomy is one root surface plus whatever heading, body, metrics, or actions the consumer places inside.
Required Parts
Card
Required. Owns surface appearance, padding, and optional polymorphic rendering through asChild.
Optional Parts
Card does not expose public child parts.
Composition Order
Card- heading, text, actions, or layout children
Valid Composition Patterns
import { Button, Card, Heading, Text } from '@vega-ui/react';
const features = ['Up to 25 members', '100 GB of storage', 'Priority email support', 'SAML single sign-on'];
export default function BasicUsage() {
return (
<Card aria-labelledby='plan-team-title' style={{ width: '100%', maxWidth: 320, gap: 'var(--spacing-6)' }}>
<Heading id='plan-team-title' as='h3' size={4} fontWeight={600}>Team</Heading>
<Text size={3}>
<Text size={6} fontWeight={500}>$12</Text> per member / month
</Text>
<ul style={{ margin: 0, paddingInlineStart: 'var(--spacing-9)', display: 'grid', gap: 'var(--spacing-2)' }}>Invalid Composition Patterns
Card used as the only semantic structure
The surface should not replace headings, landmarks, or real content hierarchy.
Transparent card on a low-contrast background
The grouped content can lose visual separation entirely.