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.
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>
| Clase | Propósito |
|---|---|
feature-card | root |
feature-card--blue | modifier |
feature-card--fill | modifier |
feature-card--green | modifier |
feature-card--media | modifier |
feature-card--orange | modifier |
feature-card--purple | modifier |
feature-card__body | element |
feature-card__media | element |
feature-card__media--fill | element |
feature-card__media-fill | element |
feature-card__text | element |
feature-card__title | element |
feature-card__value | element |
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 | var(--feature-card-accent) | --feature-card-accent |
| all | fg | var(--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); }