Skip to content

React Recipes

Copy-paste ready React components using @airframeui. You own the code, we provide the styling.

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>
);
}
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>
);
}
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>
);
}
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>;
}
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>;
}
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>;
}
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>

);
}
  1. You own the code - Copy, modify, extend as needed
  2. Just classnames - No runtime, just CSS classes
  3. Composable - Mix and match with your own components
  4. Type-safe - Full TypeScript support
  5. Accessible - Semantic HTML by default