ATOM
Components (registry)Actions

FeatureCard

Card de feature con media, titulo y texto para tiras horizontales. Un solo componente con variantes de color (green/purple/orange/blue) que por dentro apuntan a intents del sistema.

Preview

Anatomía CSS

<div class="feature-card">
  <span class="feature-card__body"></span>
  <span class="feature-card__media"></span>
  <span class="feature-card__media--fill"></span>
  <span class="feature-card__media-fill"></span>
  <span class="feature-card__text"></span>
  <span class="feature-card__title"></span>
  <span class="feature-card__value"></span>
</div>
ClasePropósito
feature-cardroot
feature-card--bluemodifier
feature-card--fillmodifier
feature-card--greenmodifier
feature-card--mediamodifier
feature-card--orangemodifier
feature-card--purplemodifier
feature-card__bodyelement
feature-card__mediaelement
feature-card__media--fillelement
feature-card__media-fillelement
feature-card__textelement
feature-card__titleelement
feature-card__valueelement

Tokens resueltos

Valores finales tras seguir la cadena de tokens. Derivados del source: si un token cambia, esta tabla cambia sola.

VariantePropValorToken
allbgvar(--feature-card-accent)--feature-card-accent
allfgvar(--feature-card-accent-display)--feature-card-accent-display

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).

/* -------------------------------------------------------------------------
   Feature Card

   Card de feature: media + titulo + texto. Pensada para tiras horizontales
   (marquee, carruseles) donde cada card se lee de un vistazo.

   No confundir con `feature` (atoms/feature.css): aquel es una FILA de
   "icono + texto + valor" para listas de beneficios dentro de un pricing-card.
   Esto es una card por derecho propio.

   Estructura:  .feature-card > .feature-card__media
                              > .feature-card__body > .feature-card__title
                                                    > .feature-card__text

   El media y el cuerpo son SLOTS: la card no decide si dentro va un <img>, un
   <svg> o un embed — solo la caja, el radio y el recorte.

   Variantes de color: --green --purple --orange --blue

   Los nombres son de COLOR a proposito (asi los nombra diseno) pero por dentro
   cada uno apunta a un intent del sistema, nunca a un hex. Si el DS reafina
   --success o --brand, la card sigue sin tocarla. Y es UN componente con
   variantes, no cuatro componentes de colores distintos.
   ------------------------------------------------------------------------- */

.feature-card {
  /* Un solo mando para la intensidad del tinte; el tono lo pone el intent.
     4% reproduce los tintes de diseno con desvio maximo de 5/255 por canal. */
  --feature-card-tint: 4%;

  /* Color del valor: el intent EMPUJADO hacia el foreground.

     El intent crudo no sirve como texto (--brand sobre su tinte da 2.71:1) y el
     par *-text del DS sobra por el otro lado: esta calibrado a 4.5:1 para texto
     normal, y un numero de 36px bold solo pide 3:1. Con --success-text el valor
     salia a 14.57:1 — legible, pero tan oscuro que dejaba de leerse como verde.

     Al 65% los cuatro acentos pasan de 3:1 conservando su tono: violeta 8.86,
     naranja 5.40, verde 3.94, azul 3.66. Y mezclar contra --foreground es
     direccionalmente correcto solo: oscurece en claro y ACLARA en oscuro,
     porque --foreground se invierte con el tema. Medido en los dos: en oscuro
     va de 5.92 a 12.35. */
  --feature-card-accent-display: color-mix(
    in srgb,
    var(--foreground) 65%,
    #0a0a0a
  );

  display: flex;
  flex-direction: column;
  gap: 16px;
  width: 20rem;
  /* En una tira, la altura la marca la card con mas texto y las demas la
     igualan. Sin esto cada card mide su contenido y el carril las centra: se
     leen como cajas sueltas a distinta altura, no como una fila.
     No fuerza nada fuera de flex — con un ancho explicito, stretch en una
     columna o en un grid no tiene efecto. */
  align-self: stretch;
  padding: 32px;
  border-radius: 16px;
  background-color: color-mix(
    in srgb,
    var(--muted) var(--feature-card-tint),
    #fafafa
  );
  color: #0a0a0a;
  font-family: 'inter tight', -apple-system, blinkmacsystemfont, 'segoe ui', roboto, helvetica, arial, sans-serif, ui-sans-serif, system-ui, sans-serif;
  /* .marquee__item va en nowrap para tiras de texto; aqui el texto es parrafo. */
  white-space: normal;
  text-align: start;
}

/* ---- Media (slot) ---- */

/* La caja del media la fija la CARD, no el asset: 5/4 es la proporcion de la
   remesa actual (751x597), asi que hoy no recorta nada, y manana una imagen con
   otra proporcion se encuadra en vez de descuadrar la fila entera. */
.feature-card__media {
  overflow: hidden;
  border-radius: 8px;
  aspect-ratio: 5 / 4;
}

/* El display va aparte y dentro de :where() a proposito. Es el default de un
   <div>, no una decision del componente: solo esta aqui por si el slot se monta
   sobre un elemento inline, donde aspect-ratio no aplicaria.

   A especificidad normal, esta declaracion EMPATA con cualquier regla de una
   clase que el consumidor escriba para ocultar el slot, y como el CSS del DS se
   carga despues del suyo, gana el DS y el slot no se puede esconder. Paso en
   atomchat.io: las variantes "sin imagen" del componente de Webflow declaraban
   display:none y el panel las emite con :where() —cero especificidad—, asi que
   este display:block las pisaba y la imagen seguia ahi.

   Un default pertenece a la capa de menor peso. Con :where() sigue cubriendo el
   caso inline y deja de pelear con quien lo consume. */
:where(.feature-card__media) {
  display: block;
}

/* Un slot es opcional por definicion, pero la caja del media se reserva sola:
   con aspect-ratio, un slot sin nada dentro sigue ocupando 5/4 de la columna y
   se lee como una imagen que no cargo. El consumidor no puede arreglarlo desde
   su lado sin tocar la clase del DS.

   Se usa :not(:has(*)) y no :empty porque :empty cuenta los nodos de texto: un
   salto de linea entre las etiquetas —lo normal en HTML escrito a mano— ya
   bastaria para que la regla no aplicara. Aqui lo que importa es si hay un
   elemento dentro, no si hay whitespace. Precedente de :has() en el repo:
   marquee.css (.marquee__item:has(> .review-card)). */
.feature-card__media:not(:has(*)) {
  display: none;
}

.feature-card__media > * {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Cuerpo (slot) ---- */

.feature-card__body {
  display: flex;
  flex-direction: column;
  gap: 8px;
}

.feature-card__title {
  margin: 0;
  font-size: 20px;
  font-weight: 600;
  line-height: 1.3;
  letter-spacing: -0.02em;
  text-wrap: balance;
}

.feature-card__text {
  margin: 0;
  font-size: 16px;
  font-weight: 400;
  line-height: 1.5;
  text-wrap: pretty;
}

/* ---- Valor (slot opcional) ----

   Una metrica en el sitio del gancho: la card no cambia, cambia que lo que
   engancha es un numero en vez de una imagen. Va ANTES del cuerpo y el <h3>
   se queda en la etiqueta: un numero no titula nada, y meterlo en un heading
   ensucia el esquema de encabezados de la pagina para un lector de pantalla.

   El color sale de --feature-card-accent-display, no del intent crudo ni del
   par *-text. El porque esta arriba, donde se declara. */

.feature-card__value {
  margin: 0;
  color: var(--foreground);
  font-size: 48.83px;
  font-weight: 700;
  line-height: 1;
  letter-spacing: -0.02em;
  /* Cifras de ancho fijo: sin esto, dos metricas apiladas bailan al alinearse. */
  font-variant-numeric: tabular-nums;
}

/* ---- Modificadores de rejilla ----

   La card fija su ancho porque nacio para tiras horizontales; en una rejilla
   manda la columna. Van como MODIFICADORES y no como selector descendente
   desde el layout porque el panel de Webflow no declara descendentes: alli un
   modo del contenedor no se puede reproducir y la rejilla quedaria con cards
   de 20rem sueltas. Misma razon que marquee__item--logo. */

.feature-card--fill {
  width: auto;
  height: 100%;
}

/* Tile de solo media: sin cuerpo y sin padding, la imagen sangra hasta el
   radio. Comparte radio y tinte con las cards de contenido para que la rejilla
   se lea como un sistema y no como dos cajas distintas puestas al lado. */
.feature-card--media {
  width: auto;
  height: 100%;
  padding: 0;
  overflow: hidden;
}

/* El tile de solo media llena la celda de la rejilla: ahi la proporcion la pone
   la celda, no la card, asi que la del media sobra. */
.feature-card__media--fill {
  flex-grow: 1;
  height: 100%;
  aspect-ratio: auto;
  border-radius: inherit;
}

/* Sobre la imagen, no sobre el contenedor: object-fit necesita ir en el propio
   reemplazado, y Webflow no puede escribir `.feature-card__media--fill > *`. */
.feature-card__media-fill {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Acentos ----

   Una sola variable por acento. La superficie y el valor salen ambos de ella:
   el fondo por tinte al 4%, el valor por empuje al 65% hacia el foreground.
   Anadir un acento nuevo es una linea, no dos que puedan quedar descuadradas. */

.feature-card--green {
  --feature-card-accent: #25d366;
}

.feature-card--purple {
  --feature-card-accent: #8023ff;
}

.feature-card--orange {
  --feature-card-accent: #ff6600;
}

.feature-card--blue {
  --feature-card-accent: #22d3ee;
}

Codigo fuente

styles/components/feature-card.css
/* -------------------------------------------------------------------------
   Feature Card

   Card de feature: media + titulo + texto. Pensada para tiras horizontales
   (marquee, carruseles) donde cada card se lee de un vistazo.

   No confundir con `feature` (atoms/feature.css): aquel es una FILA de
   "icono + texto + valor" para listas de beneficios dentro de un pricing-card.
   Esto es una card por derecho propio.

   Estructura:  .feature-card > .feature-card__media
                              > .feature-card__body > .feature-card__title
                                                    > .feature-card__text

   El media y el cuerpo son SLOTS: la card no decide si dentro va un <img>, un
   <svg> o un embed — solo la caja, el radio y el recorte.

   Variantes de color: --green --purple --orange --blue

   Los nombres son de COLOR a proposito (asi los nombra diseno) pero por dentro
   cada uno apunta a un intent del sistema, nunca a un hex. Si el DS reafina
   --success o --brand, la card sigue sin tocarla. Y es UN componente con
   variantes, no cuatro componentes de colores distintos.
   ------------------------------------------------------------------------- */

.feature-card {
  /* Un solo mando para la intensidad del tinte; el tono lo pone el intent.
     4% reproduce los tintes de diseno con desvio maximo de 5/255 por canal. */
  --feature-card-tint: 4%;

  /* Color del valor: el intent EMPUJADO hacia el foreground.

     El intent crudo no sirve como texto (--brand sobre su tinte da 2.71:1) y el
     par *-text del DS sobra por el otro lado: esta calibrado a 4.5:1 para texto
     normal, y un numero de 36px bold solo pide 3:1. Con --success-text el valor
     salia a 14.57:1 — legible, pero tan oscuro que dejaba de leerse como verde.

     Al 65% los cuatro acentos pasan de 3:1 conservando su tono: violeta 8.86,
     naranja 5.40, verde 3.94, azul 3.66. Y mezclar contra --foreground es
     direccionalmente correcto solo: oscurece en claro y ACLARA en oscuro,
     porque --foreground se invierte con el tema. Medido en los dos: en oscuro
     va de 5.92 a 12.35. */
  --feature-card-accent-display: color-mix(
    in srgb,
    var(--feature-card-accent, var(--foreground)) 65%,
    var(--foreground)
  );

  display: flex;
  flex-direction: column;
  gap: var(--spacing-4);
  width: var(--feature-card-width, 20rem);
  /* En una tira, la altura la marca la card con mas texto y las demas la
     igualan. Sin esto cada card mide su contenido y el carril las centra: se
     leen como cajas sueltas a distinta altura, no como una fila.
     No fuerza nada fuera de flex — con un ancho explicito, stretch en una
     columna o en un grid no tiene efecto. */
  align-self: stretch;
  padding: var(--spacing-8);
  border-radius: var(--radius-xl);
  background-color: color-mix(
    in srgb,
    var(--feature-card-accent, var(--muted)) var(--feature-card-tint),
    var(--background)
  );
  color: var(--card-foreground);
  font-family: var(--font-family-sans), ui-sans-serif, system-ui, sans-serif;
  /* .marquee__item va en nowrap para tiras de texto; aqui el texto es parrafo. */
  white-space: normal;
  text-align: start;
}

/* ---- Media (slot) ---- */

/* La caja del media la fija la CARD, no el asset: 5/4 es la proporcion de la
   remesa actual (751x597), asi que hoy no recorta nada, y manana una imagen con
   otra proporcion se encuadra en vez de descuadrar la fila entera. */
.feature-card__media {
  overflow: hidden;
  border-radius: var(--radius-md);
  aspect-ratio: var(--feature-card-media-ratio, 5 / 4);
}

/* El display va aparte y dentro de :where() a proposito. Es el default de un
   <div>, no una decision del componente: solo esta aqui por si el slot se monta
   sobre un elemento inline, donde aspect-ratio no aplicaria.

   A especificidad normal, esta declaracion EMPATA con cualquier regla de una
   clase que el consumidor escriba para ocultar el slot, y como el CSS del DS se
   carga despues del suyo, gana el DS y el slot no se puede esconder. Paso en
   atomchat.io: las variantes "sin imagen" del componente de Webflow declaraban
   display:none y el panel las emite con :where() —cero especificidad—, asi que
   este display:block las pisaba y la imagen seguia ahi.

   Un default pertenece a la capa de menor peso. Con :where() sigue cubriendo el
   caso inline y deja de pelear con quien lo consume. */
:where(.feature-card__media) {
  display: block;
}

/* Un slot es opcional por definicion, pero la caja del media se reserva sola:
   con aspect-ratio, un slot sin nada dentro sigue ocupando 5/4 de la columna y
   se lee como una imagen que no cargo. El consumidor no puede arreglarlo desde
   su lado sin tocar la clase del DS.

   Se usa :not(:has(*)) y no :empty porque :empty cuenta los nodos de texto: un
   salto de linea entre las etiquetas —lo normal en HTML escrito a mano— ya
   bastaria para que la regla no aplicara. Aqui lo que importa es si hay un
   elemento dentro, no si hay whitespace. Precedente de :has() en el repo:
   marquee.css (.marquee__item:has(> .review-card)). */
.feature-card__media:not(:has(*)) {
  display: none;
}

.feature-card__media > * {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Cuerpo (slot) ---- */

.feature-card__body {
  display: flex;
  flex-direction: column;
  gap: var(--spacing-2);
}

.feature-card__title {
  margin: 0;
  font-size: var(--font-size-lg);
  font-weight: var(--font-weight-semibold);
  line-height: 1.3;
  letter-spacing: var(--letter-spacing-tight);
  text-wrap: balance;
}

.feature-card__text {
  margin: 0;
  font-size: var(--font-size-base);
  font-weight: var(--font-weight-regular);
  line-height: 1.5;
  text-wrap: pretty;
}

/* ---- Valor (slot opcional) ----

   Una metrica en el sitio del gancho: la card no cambia, cambia que lo que
   engancha es un numero en vez de una imagen. Va ANTES del cuerpo y el <h3>
   se queda en la etiqueta: un numero no titula nada, y meterlo en un heading
   ensucia el esquema de encabezados de la pagina para un lector de pantalla.

   El color sale de --feature-card-accent-display, no del intent crudo ni del
   par *-text. El porque esta arriba, donde se declara. */

.feature-card__value {
  margin: 0;
  color: var(--feature-card-accent-display, var(--foreground));
  font-size: var(--font-size-4xl);
  font-weight: var(--font-weight-bold);
  line-height: 1;
  letter-spacing: var(--letter-spacing-tight);
  /* Cifras de ancho fijo: sin esto, dos metricas apiladas bailan al alinearse. */
  font-variant-numeric: tabular-nums;
}

/* ---- Modificadores de rejilla ----

   La card fija su ancho porque nacio para tiras horizontales; en una rejilla
   manda la columna. Van como MODIFICADORES y no como selector descendente
   desde el layout porque el panel de Webflow no declara descendentes: alli un
   modo del contenedor no se puede reproducir y la rejilla quedaria con cards
   de 20rem sueltas. Misma razon que marquee__item--logo. */

.feature-card--fill {
  width: auto;
  height: 100%;
}

/* Tile de solo media: sin cuerpo y sin padding, la imagen sangra hasta el
   radio. Comparte radio y tinte con las cards de contenido para que la rejilla
   se lea como un sistema y no como dos cajas distintas puestas al lado. */
.feature-card--media {
  width: auto;
  height: 100%;
  padding: 0;
  overflow: hidden;
}

/* El tile de solo media llena la celda de la rejilla: ahi la proporcion la pone
   la celda, no la card, asi que la del media sobra. */
.feature-card__media--fill {
  flex-grow: 1;
  height: 100%;
  aspect-ratio: auto;
  border-radius: inherit;
}

/* Sobre la imagen, no sobre el contenedor: object-fit necesita ir en el propio
   reemplazado, y Webflow no puede escribir `.feature-card__media--fill > *`. */
.feature-card__media-fill {
  display: block;
  width: 100%;
  height: 100%;
  object-fit: cover;
}

/* ---- Acentos ----

   Una sola variable por acento. La superficie y el valor salen ambos de ella:
   el fondo por tinte al 4%, el valor por empuje al 65% hacia el foreground.
   Anadir un acento nuevo es una linea, no dos que puedan quedar descuadradas. */

.feature-card--green {
  --feature-card-accent: var(--success);
}

.feature-card--purple {
  --feature-card-accent: var(--ai);
}

.feature-card--orange {
  --feature-card-accent: var(--brand);
}

.feature-card--blue {
  --feature-card-accent: var(--info);
}

On this page

Detalles

Publicado14 de mayo de 2026
Categoriaactions
Lectura...
Visitas...
Ayuda?Slack

Componente

FeatureCard

Source

components-react / cssDisponible via MCP: atom_uikit_source("feature-card")