Funciones personalizadas en CSS: @function nativo vs Sass
Antes de que existieran las funciones personalizadas en CSS, todos teníamos un _functions.scss. Abre el de cualquier proyecto con cinco años encima y verás lo mismo: rem(), spacing(), negative(), un fluid-type() que alguien copió de un artículo de 2019. Funcionan. Hasta que alguien pide un modo compacto para un componente.
Entonces descubres el límite. Esa función de Sass ya se ejecutó. Se ejecutó en tu máquina, durante el build, y escupió un 16px fijo en el CSS final. El navegador nunca supo que ahí había una fórmula. Para el modo compacto toca duplicar clases o volver a escribir calc() a mano.
Las funciones personalizadas en CSS nativas cambian eso. Ya no compilas la fórmula: la envías al navegador y él la evalúa con los valores reales de la cascada, en cada elemento.
En corto: Las funciones personalizadas en CSS se definen con la regla nativa @function y, a diferencia de Sass, se evalúan en el navegador leyendo custom properties y media queries del elemento. Funcionan en Chrome y Edge 139+; Firefox no las soporta y Safari solo en Technology Preview, así que hoy van con fallback.
¿Qué son las funciones personalizadas en CSS?
Una función personalizada en CSS es una regla @function con nombre --algo que recibe argumentos y devuelve un valor a través del descriptor result, evaluado por el navegador en el elemento donde se llama.
Viene del CSS Functions and Mixins Module, un Editor's Draft que firman Miriam Suzanne y Tab Atkins-Bittner (versión del 8 de octubre de 2026). Esta es la mínima expresión:
@function --negativo(--valor) {
result: calc(-1 * var(--valor));
}
.card {
--gap: 1.5rem;
margin-top: --negativo(var(--gap)); /* -1.5rem */
}
Tres detalles de sintaxis que la spec impone. El nombre empieza siempre por --, igual que una custom property. Dentro del cuerpo, los parámetros se leen con var(). Y si la función no declara result, es válida pero devuelve el valor "guaranteed-invalid".
Sintaxis de @function: tipos, valores por defecto y returns
La gramática completa admite tipo y valor por defecto en cada parámetro (separado con dos puntos) y un tipo de retorno con returns:
@function --alpha(--color <color>, --opacidad <number>: 0.5) returns <color> {
result: oklch(from var(--color) l c h / var(--opacidad));
}
.badge {
background: --alpha(var(--brand)); /* 50 % de opacidad */
border-color: --alpha(var(--brand), 0.8); /* 80 % */
}
Si pasas un argumento que no encaja con el tipo y el parámetro tiene valor por defecto, MDN documenta que se ignora el inválido y se usa el defecto. Para aceptar varios tipos se usa type(): --x type(<number> | <percentage>).
El cuerpo también admite variables locales. Son custom properties que solo existen dentro de la función. Así queda la conversión de px a rem sin repetir el 16 por todo el código:
@function --rem(--px <number>) returns <length> {
--base: 16;
result: calc(var(--px) / var(--base) * 1rem);
}
h1 { font-size: --rem(32); } /* 2rem */
Lo que Sass no puede hacer: leer la cascada en runtime
Una función personalizada en CSS puede leer custom properties definidas en el sitio de la llamada, sin recibirlas como argumento. La spec lo dice de forma explícita en su sección 2.3, con un ejemplo donde la función usa un --z que solo existe en el div que la invoca.
Eso te permite escribir esto:
@function --espacio(--n <number>) returns <length> {
result: calc(var(--n) * var(--unidad, 0.25rem));
}
.card { padding: --espacio(4); } /* 1rem */
.card.compacta { --unidad: 0.2rem; } /* 0.8rem, misma regla */
La misma llamada devuelve valores distintos según el contexto del elemento. En Sass, spacing(4) se resolvió en el build y no hay forma de que reaccione a una clase añadida en runtime.
Pruébalo aquí mismo
Esta demo usa la función --espacio() de arriba, en vivo y sin JavaScript. Abre "Modo compacto": cambia solo --unidad en el contenedor y las tres tarjetas recalculan su padding y su gap con la misma llamada.
✅ Tu navegador soporta @function: la demo funciona en vivo.
⚠️ Tu navegador no soporta @function: ves el fallback de padding: 1rem y el modo compacto no cambia nada.
Modo compacto (–unidad: 0.125rem)
Mismo CSS, misma llamada –espacio(4). Solo ha cambiado una custom property en el contenedor.
¿Quieres tocarlo tú? Abre DevTools, inspecciona una tarjeta y cambia --unidad en .dc-fn-cards. El panel Computed te enseña el padding ya resuelto.
El cuerpo admite además reglas condicionales como @media. La spec incluye un ejemplo en el que un @media anidado sobrescribe una variable local:
@function --espacio-responsive(--n <number>) returns <length> {
--base: 0.25rem;
result: calc(var(--n) * var(--base));
@media (width >= 1024px) {
--base: 0.3rem;
}
}
Y la escala tipográfica fluida, el clásico clamp() que todo el mundo copia y pega, queda encapsulada en una línea por uso:
@function --fluido(--min <number>, --max <number>) returns <length> {
--vw-min: 360;
--vw-max: 1280;
--pendiente: calc((var(--max) - var(--min)) / (var(--vw-max) - var(--vw-min)));
result: clamp(
calc(var(--min) * 1px),
calc(var(--min) * 1px + (100vw - var(--vw-min) * 1px) * var(--pendiente)),
calc(var(--max) * 1px)
);
}
h1 { font-size: --fluido(28, 48); }
h2 { font-size: --fluido(22, 34); }
También puedes usar if() (en Chrome desde la 137) dentro de result. La guía de MDN reescribe un ejemplo con @media como result: if(media(width < 700px): var(--narrow) ; else: var(--wide));. Si ya usas light-dark(), el post sobre light-dark() con imágenes es un buen complemento: el blog de Chrome enseña precisamente una función --light-dark() que hace lo mismo con cualquier valor, no solo con colores.
Si trabajas con Angular, esto encaja muy bien con los estilos encapsulados por componente. Defines las funciones una vez en los estilos globales y cada componente ajusta sus custom properties. Es el mismo enfoque de tokens que usamos en el curso de Angular Moderno, ahora sin depender del preprocesador.
Sass @function vs CSS @function: comparativa
Si vienes de Sass y quieres repasar qué te aporta el preprocesador, aquí tienes las diferencias entre CSS y SCSS. Esta tabla compara solo las funciones:
| Criterio | Sass @function |
CSS @function |
|---|---|---|
| Cuándo se evalúa | Una vez, en el build | En el navegador, por elemento, con la cascada |
| Lee custom properties del contexto | No: solo ve valores de compilación | Sí, las del sitio de la llamada |
Reacciona a @media o al cambio de una variable |
No | Sí: @media en el cuerpo, if() en result |
| Control de flujo | @if, @each, @for, @while, recursión |
Solo condicionales; las llamadas cíclicas devuelven guaranteed-invalid |
| Dónde se puede usar | En cualquier parte del .scss |
Solo donde se permite var() (no en selectores ni en condiciones de media query) |
| Soporte | Todos los navegadores (sale CSS plano) | Chrome/Edge 139+, Opera 123+; Safari solo TP; Firefox no |
| Limitación / riesgo | El valor queda congelado; no hay temas ni densidades en runtime sin duplicar CSS | Un error de tipo no avisa: la propiedad queda inválida en computed-value time y vuelve a su valor inicial o heredado |
Los datos de soporte son de caniuse a 13 de octubre de 2026, que da un 70,46 % de uso global. Safari añadió @function en Safari Technology Preview 249 (29 de julio de 2026), pero ninguna versión estable lo incluye todavía.
Cómo se discutió: del @return al descriptor result
La propuesta pública es el issue #9350 del CSSWG, que abrió Miriam Suzanne el 13 de septiembre de 2023 con el título "Proposal: Custom CSS Functions & Mixins". La primera versión devolvía valores con una regla @return, igual que Sass. La spec actual usa el descriptor result.
El hilo también discutió si las funciones debían ver las variables del elemento. Tab Atkins propuso en febrero de 2024 un modelo léxico: la función solo vería sus argumentos y las custom properties que declarase en una lista using (--var1, --var2) (comentario). Roman Komarov se opuso: con sistemas de tokens, declarar decenas de variables por función las haría difíciles de mantener (comentario). La spec actual le da la razón a Komarov: las custom properties del sitio de la llamada están disponibles de forma implícita. El issue se cerró el 9 de julio de 2026 con la etiqueta "Accepted by CSSWG Resolution".
Chrome quería sacarla antes. Adam Argyle explicó en el blog de Chrome por qué retrasaron @function de Chrome 136 a 139: los revisores del Intent to Ship pidieron más tiempo de experimentación tras cambios importantes en la spec. Chrome 139 salió a estable el 5 de agosto de 2025.
Fallback de las funciones personalizadas en CSS sin soporte
El fallback más robusto para funciones personalizadas en CSS es la cascada de toda la vida: declara dos veces la propiedad.
.card {
padding: 1rem; /* Firefox, Safari estable */
padding: --espacio(4); /* Chrome/Edge 139+ */
}
Un navegador que no entiende --espacio() descarta la segunda declaración al parsear y se queda con la primera. MDN propone una variante para detectar soporte: una función sin parámetros --supports() que devuelve none y se usa en display.
¿Y @supports at-rule(@function)? La función at-rule() existe en CSS Conditional 5 y, según Bramus, está disponible en Chromium 148 y Firefox 157, así que ahí la condición debería dar verdadero. Pero Chrome 139 a 147 soporta @function y no at-rule(). En esas versiones la condición da falso y se aplica el fallback. No rompe nada, pero desaprovecha soporte real.
@supports at-rule(@function) {
.card { padding: --espacio(4); }
}
Mi recomendación: doble declaración para valores sueltos y @supports at-rule() solo cuando necesites aislar un bloque entero.
Cuándo NO usar @function
Si tu tráfico depende de Safari o Firefox y no puedes mantener dos versiones. Sin soporte estable en los dos, cada función necesita su fallback. Si el fallback acaba siendo el mismo calc() que querías evitar, de momento la función no te ahorra nada.
Si necesitas bucles o generar CSS. @function devuelve un valor, no reglas. Generar 12 clases de grid con @for, iterar un mapa de colores o crear utilidades sigue siendo trabajo de Sass. Los mixins (@mixin, @apply) están en la spec, pero MDN confirma que ningún navegador los soporta.
Si quieres usarla en una media query o en un selector. La spec solo la admite donde se puede usar var(), así que @media (width > --bp()) no funciona.
Si tu equipo depende de los errores de compilación. Sass avisa si llamas mal a una función. CSS no: un argumento de tipo incorrecto sin valor por defecto, o un result que no encaja con returns, deja la propiedad inválida en computed-value time. En ese caso no se aplica la declaración anterior, sino el valor inicial o heredado, y el error pasa en silencio. Ojo con esto en la doble declaración: el fallback solo protege a los navegadores sin soporte.
Cómo migrar funciones de Sass a funciones personalizadas en CSS
Elige una sola función de tu _functions.scss que dependa del contexto (espaciado por densidad, opacidad de marca, escala fluida) y reescríbela como @function con su fallback. No migres el archivo entero. Mide en DevTools de Chrome cómo resuelve el valor y deja Sass para lo que genera CSS.
Si quieres ver más CSS moderno aplicado a proyectos reales, lo explico en vídeo en YouTube Dominicode, y en Dominicode Labs compartimos los patrones de producción y los revisamos en comunidad.
Preguntas frecuentes
¿Qué navegadores soportan @function en CSS en octubre de 2026?
Chrome y Edge desde la versión 139 y Opera desde la 123, según caniuse. Safari lo añadió en Technology Preview 249 pero no en estable, y Firefox no lo soporta. MDN lo marca como "Limited availability" y no Baseline.
¿Las funciones personalizadas en CSS sustituyen a Sass?
No del todo. Sustituyen las funciones que calculan valores, y lo hacen mejor porque reaccionan en runtime. Sass sigue siendo necesario para bucles, mapas, generación de clases y mixins, porque @mixin y @apply todavía no están en ningún navegador.
¿Puede una @function leer custom properties que no le paso como argumento?
Sí. La spec permite leer las custom properties definidas en el elemento donde se llama la función. Por eso la misma llamada puede devolver valores distintos en un componente normal y en uno compacto.
¿Qué pasa si una @function devuelve un valor del tipo incorrecto?
Si result no encaja con el tipo de returns, la función devuelve un valor inválido y la propiedad se vuelve inválida en computed-value time: toma su valor inicial o heredado, no la declaración anterior. Lo mismo pasa si un argumento no encaja con su tipo y el parámetro no tiene valor por defecto; si lo tiene, se usa el defecto. No hay error en consola, así que revisa el valor computado en DevTools.
¿Funciona @supports at-rule(@function) para detectar soporte?
Sí en Chromium 148+ y Firefox 157+, pero da falso en Chrome 139 a 147, que sí soportan @function. Es seguro porque aplica el fallback, aunque conservador. Para valores sueltos, la doble declaración cubre más navegadores.
Por Bezael Pérez — Developer senior con más de 15 años de experiencia y fundador de Dominicode.
¿Te resultó útil este artículo?
Compártelo con tu comunidad y ayuda a otros desarrolladores.
