Image
A responsive frame for media from thumb to hero. Product shots and section visuals.
Sizes: xssmlherofull (default: xs)
Editorial
<!-- F12c editorial — non-derivable only. Review: Karen. -->
## Ejemplos
Product shot with ratio and cover fit:
```tsx import { Image } from '@/components/atoms/Image';
<Image src="/product/hero.jpg" alt="Atom UIKit components on a light surface" size="hero" ratio="16x9" fit="cover" radius="lg" /> ```
## Accesibilidad
- `alt` is mandatory for meaningful images; use empty `alt=""` only when the image is pure decoration next to adjacent text that already names it. - Default `loading="lazy"` — override to `eager` for LCP heroes above the fold.
## Cuándo no usar
- Avatars / people faces → `Avatar` (sizes, status, initials). - Icons and UI glyphs → icon components / SVG, not `Image` with a huge bitmap.
Uso
import { Image } from '@/components/atoms/Image';
<Image
src="/photos/hero.jpg"
alt="Team workshop"
size="l"
ratio="16x9"
fit="cover"
radius="md"
/>Props
| Prop | Tipo | Default | Rango / opciones | What | How |
|---|---|---|---|---|---|
| size | select | — | `xs`, `s`, `m`, `l`, `hero`, `full` | Intrinsic display size of the image frame. | m for cards; l/hero for marketing; full for edge-to-edge bands. Default: m. |
| ratio | select | — | `1x1`, `4x3`, `16x9`, `21x9` | Aspect ratio lock for the frame before load. | 16x9 for video thumbs; 1x1 for avatars/product tiles; 21x9 only for cinematic heroes. |
| fit | select | — | `cover`, `contain` | object-fit strategy inside the frame. | cover for crops/heroes; contain when the whole asset must stay visible (logos, diagrams). |
| radius | select | — | `none`, `sm`, `md`, `lg`, `xl`, `full` | Corner rounding of the image frame. | md for cards; full only for circular crops; none for full-bleed media. |
Gotchas
- a11y
Always pass meaningful alt (or alt="" only when purely decorative and adjacent text already names it).
- react
Forwards native img attributes; do not nest interactive buttons inside without a separate control pattern.
Anatomía CSS
<div class="image"> </div>
| Clase | Propósito |
|---|---|
image | root |
image--contain | modifier |
image--cover | modifier |
image--full | modifier |
image--hero | modifier |
image--l | modifier |
image--loaded | modifier |
image--loading | modifier |
image--m | modifier |
image--ratio-16x9 | modifier |
image--ratio-1x1 | modifier |
image--ratio-21x9 | modifier |
image--ratio-4x3 | modifier |
image--rounded-full | modifier |
image--rounded-lg | modifier |
image--rounded-md | modifier |
image--rounded-none | modifier |
image--rounded-sm | modifier |
image--rounded-xl | modifier |
image--s | modifier |
image--xs | modifier |
Tokens resueltos
Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.
| Variante | Prop | Valor | Token |
|---|---|---|---|
| all | bg | #f5f5f5 | muted |
Animaciones
| Propiedad | Duración | Easing |
|---|---|---|
opacity | var(--duration-300) | var(--easing-out) |
CSS mínimo funcional
Autocontenido: sin imports ni tokens. Para previews y prototipos — en producción se consume el CSS del DS (@atom-uikit/css/components.css</code>, <code>@atom-uikit/tokens/tokens.css).
/* ============================================================
Image — responsive image with sizing variants and aspect ratios
============================================================ */
.image {
display: block;
width: 100%;
max-width: 100%;
height: auto;
object-fit: cover;
border-radius: 8px;
background-color: #f5f5f5;
transition: opacity 300ms cubic-bezier(0.22, 1, 0.36, 1);
}
/* ---- Sizing variants ---- */
.image--xs {
width: 3rem; /* 48px */
height: 3rem;
border-radius: 4px;
}
.image--s {
width: 6rem; /* 96px */
height: 6rem;
border-radius: 8px;
}
.image--m {
width: 12.5rem; /* 200px */
height: auto;
}
.image--l {
width: 25rem; /* 400px */
height: auto;
}
.image--hero {
width: 100%;
max-height: 30rem; /* 480px */
object-fit: cover;
border-radius: 16px;
}
.image--full {
width: 100%;
height: 100%;
object-fit: cover;
border-radius: 0;
}
/* ---- Aspect ratios ---- */
.image--ratio-1x1 { aspect-ratio: 1 / 1; }
.image--ratio-4x3 { aspect-ratio: 4 / 3; }
.image--ratio-16x9 { aspect-ratio: 16 / 9; }
.image--ratio-21x9 { aspect-ratio: 21 / 9; }
/* ---- Object fit variants ---- */
.image--contain { object-fit: contain; }
.image--cover { object-fit: cover; }
/* ---- Border radius variants ---- */
.image--rounded-none { border-radius: 0; }
.image--rounded-sm { border-radius: 4px; }
.image--rounded-md { border-radius: 8px; }
.image--rounded-lg { border-radius: 12px; }
.image--rounded-xl { border-radius: 16px; }
.image--rounded-full { border-radius: 9999px; }
/* ---- Loading state (skeleton placeholder) ---- */
.image--loading {
opacity: 0;
}
.image--loaded {
opacity: 1;
}
/* ---- Reduced motion ---- */
@media (prefers-reduced-motion: reduce) {
.image {
transition-duration: 0ms;
}
}
Codigo fuente
import { forwardRef, useState, type ImgHTMLAttributes } from 'react'; export type ImageSize = 'xs' | 's' | 'm' | 'l' | 'hero' | 'full'; export type ImageRatio = '1x1' | '4x3' | '16x9' | '21x9'; export type ImageFit = 'cover' | 'contain'; export type ImageRadius = 'none' | 'sm' | 'md' | 'lg' | 'xl' | 'full'; export type ImageProps = { size?: ImageSize; ratio?: ImageRatio; fit?: ImageFit; radius?: ImageRadius; className?: string; } & Omit<ImgHTMLAttributes<HTMLImageElement>, 'className'>; function cn(...classes: (string | false | undefined | null)[]) { return classes.filter(Boolean).join(' '); } export const Image = forwardRef<HTMLImageElement, ImageProps>( ({ size, ratio, fit, radius, className, loading = 'lazy', onLoad, ...props }, ref) => { const [loaded, setLoaded] = useState(false); const classes = cn( 'image', size && `image--${size}`, ratio && `image--ratio-${ratio}`, fit && `image--${fit}`, radius && `image--rounded-${radius}`, !loaded && 'image--loading', loaded && 'image--loaded', className, ); return ( <img ref={ref} className={classes} loading={loading} onLoad={(e) => { setLoaded(true); onLoad?.(e); }} {...props} /> ); }, ); Image.displayName = 'Image';
Webflow
Pega en el Designer como application/json, luego convierte a Component (Atom / Image) y publica. Formato interno no documentado de Webflow — regenerable, no dependencia de runtime.
Setup del sitio (una vez)
Custom Code → Head:
<link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/tokens.css"> <link rel="stylesheet" href="https://atom-web-ds.vercel.app/v1/components.css">
Unsupported (no silencioso)
@mediaon(prefers-reduced-motion: reduce)— not a Designer breakpoint — moved to head Custom Code block
Tras pegar: Create component → nombre Atom / Image → Publish.