React Recipes
Copy-paste ready React components using @airframeui. You own the code, we provide the styling.
Card Component
Section titled “Card Component”import { clsx } from 'clsx';
interface CardProps {
title?: string;
actions?: React.ReactNode;
children: React.ReactNode;
className?: string;
variant?: 'default' | 'elevated';
}
export function Card({ title, actions, children, className, variant = 'default' }: CardProps) {
return (
<section className={clsx('af-card', variant === 'elevated' && 'af-is-elevated', className)}>
{title && (
<header className="af-card__header">
<h2 className="af-card__title">{title}</h2>
{actions && <div className="af-card__actions">{actions}</div>}
</header>
)}
<div className="af-card__body">{children}</div>
</section>
);
}Button Component
Section titled “Button Component”import { clsx } from 'clsx';
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: 'default' | 'primary' | 'success' | 'warning' | 'danger' | 'info';
size?: 'sm' | 'md' | 'lg';
loading?: boolean;
}
export function Button({
variant = 'default',
size = 'md',
loading,
className,
disabled,
children,
...props
}: ButtonProps) {
return (
<button
className={clsx(
'af-btn',
variant !== 'default' && `af-is-${variant}`,
size !== 'md' && `af-is-${size}`,
loading && 'af-is-loading',
className
)}
disabled={disabled || loading}
{...props}
>
{children}
</button>
);
}Form Field Component
Section titled “Form Field Component”import React from 'react';
import { clsx } from 'clsx';
interface FieldProps {
id: string; // Required for accessibility - used for label 'for' and error aria-describedby
label: string;
hint?: string;
error?: string;
required?: boolean;
children: React.ReactNode;
className?: string;
}
export function Field({ id, label, hint, error, required, children, className }: FieldProps) {
const errorId = `${id}-error`;
return (
<div className={clsx('af-field', className)}>
<label className="af-field__label" htmlFor={id}>
{label}
{required && <span aria-label="required">_</span>}
</label>
{/_ Clone children to add aria-describedby when error exists */}
{error
? React.cloneElement(children as React.ReactElement, {
id,
'aria-invalid': true,
'aria-describedby': errorId,
className: clsx(
(children as React.ReactElement).props?.className,
'af-is-error'
),
})
: React.cloneElement(children as React.ReactElement, { id })
}
{hint && <p className="af-field**hint" id={`${id}-hint`}>{hint}</p>}
{error && (
<p
className="af-field**error"
id={errorId} >
{error}
</p>
)}
</div>
);
}Stack Layout Component
Section titled “Stack Layout Component”import { clsx } from 'clsx';
interface StackProps {
gap?: 'sm' | 'md' | 'lg';
responsive?: {
sm?: 'stack' | 'inline';
md?: 'stack' | 'inline';
lg?: 'stack' | 'inline';
};
children: React.ReactNode;
className?: string;
}
export function Stack({ gap = 'md', responsive, children, className }: StackProps) {
const classes = clsx(
'af-stack',
gap !== 'md' && `af-gap-${gap}`,
responsive?.sm && `af-${responsive.sm}@sm`,
responsive?.md && `af-${responsive.md}@md`,
responsive?.lg && `af-${responsive.lg}@lg`,
className
);
return <div className={classes}>{children}</div>;
}Grid Layout Component
Section titled “Grid Layout Component”import { clsx } from 'clsx';
interface GridProps {
cols?: 1 | 2 | 3 | 4 | 5 | 6 | 12;
gap?: 'sm' | 'md' | 'lg';
responsive?: {
sm?: 1 | 2 | 3 | 4 | 5 | 6;
md?: 1 | 2 | 3 | 4 | 5 | 6;
lg?: 1 | 2 | 3 | 4 | 5 | 6;
};
children: React.ReactNode;
className?: string;
}
export function Grid({ cols = 1, gap = 'md', responsive, children, className }: GridProps) {
// Use af-grid for all layouts - it auto-detects 12-column when children have spans
// For equal-width columns, use af-grid (auto-adjusts) or af-grid-1 through af-grid-6
// For 12-column system, use af-grid with af-col-span-* children
const classes = clsx(
cols === 12 ? 'af-grid' :
cols === 1 ? 'af-grid' : `af-grid-${cols}`,
gap !== 'md' && `af-gap-${gap}`,
responsive?.sm && `af-grid-${responsive.sm}@sm`,
responsive?.md && `af-grid-${responsive.md}@md`,
responsive?.lg && `af-grid-${responsive.lg}@lg`,
className
);
return <div className={classes}>{children}</div>;
}Responsive Column Component
Section titled “Responsive Column Component”import { clsx } from 'clsx';
interface ColProps {
span?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
responsive?: {
sm?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
md?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
lg?: 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12;
};
children: React.ReactNode;
className?: string;
}
export function Col({ span = 12, responsive, children, className }: ColProps) {
const classes = clsx(
`af-col-span-${span}`,
responsive?.sm && `af-col-span-${responsive.sm}@sm`,
responsive?.md && `af-col-span-${responsive.md}@md`,
responsive?.lg && `af-col-span-${responsive.lg}@lg`,
className
);
return <div className={classes}>{children}</div>;
}Usage Example
Section titled “Usage Example”import { Card } from './Card';
import { Button } from './Button';
import { Stack } from './Stack';
import { Grid } from './Grid';
function Dashboard() {
return (
<div className="af-container">
<Stack gap="lg">
<h1>Dashboard</h1>
{/* Prefer af-grid with column counts for equal-width cards */}
<Grid cols={1} gap="md" responsive={{ md: 2, lg: 4 }}>
<Card title="Users" actions={<Button variant="primary">View</Button>}>
<p>1,234 active users</p>
</Card>
<Card title="Revenue">
<p>$12,345</p>
</Card>
<Card title="Orders">
<p>567 orders</p>
</Card>
</Grid>
</Stack>
</div>
);
}Key Principles
Section titled “Key Principles”- You own the code - Copy, modify, extend as needed
- Just classnames - No runtime, just CSS classes
- Composable - Mix and match with your own components
- Type-safe - Full TypeScript support
- Accessible - Semantic HTML by default