css.joedoe.dev

Recetas

Lo que copio una y otra vez de proyecto en proyecto. Cada receta tiene la demo funcionando al lado: si en tu navegador no se ve, es que aún no lo soporta.

Maquetación

Centrar cualquier cosa

Funciona en todos

Dos líneas y centrado en los dos ejes. Sirve igual para un icono que para un modal.

.centro {
  display: grid;
  place-items: center;
}

Rejilla que se adapta sola

Funciona en todos

Tantas columnas como quepan, sin media queries. El min() evita que se salga en móviles estrechos.

.rejilla {
  display: grid;
  grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
  gap: 1rem;
}

Pie siempre abajo

Funciona en todos

Con poco contenido el pie no se queda flotando a media pantalla. dvh tiene en cuenta la barra del navegador del móvil.

body {
  min-height: 100dvh;
  display: grid;
  grid-template-rows: auto 1fr auto;
}

Proporción fija

Funciona en todos

Se acabó el truco del padding-top: 56.25%. La imagen es cuadrada y se ve en 16:9: object-fit: cover la recorta en vez de deformarla (va en img o video, en un div no hace nada).

.portada {
  aspect-ratio: 16 / 9;
  width: 100%;
  object-fit: cover;
}

Container queries

Funciona en todos

El componente cambia según el sitio que tiene, no según la pantalla. Arrastra la esquina de la caja para verlo.

.tarjeta-wrap { container-type: inline-size; }

.tarjeta { display: grid; gap: 1rem; }

@container (min-width: 22rem) {
  .tarjeta { grid-template-columns: 6rem 1fr; }
}

Tarjetas alineadas con subgrid

Funciona en todos

Títulos de distinta longitud y aun así todos los botones a la misma altura.

.tarjetas {
  display: grid;
  grid-template-columns: repeat(auto-fit, minmax(9rem, 1fr));
  gap: 1rem;
}

.tarjetas > * {
  display: grid;
  grid-row: span 3;
  grid-template-rows: subgrid;
}

Carrusel sin JavaScript

Funciona en todos

scroll-snap hace que cada tarjeta encaje al soltar. Con el dedo o con el trackpad.

.carrusel {
  display: flex;
  gap: 1rem;
  overflow-x: auto;
  scroll-snap-type: x mandatory;
  overscroll-behavior-x: contain;
}

.carrusel > * {
  flex: 0 0 80%;
  scroll-snap-align: center;
}

Anclas que no se esconden bajo la cabecera

Funciona en todos

Con una cabecera fija, el título al que saltas queda tapado. scroll-margin lo baja lo justo.

html { scroll-behavior: smooth; }

:target,
h2[id] {
  scroll-margin-top: 5rem;
}

@media (prefers-reduced-motion: reduce) {
  html { scroll-behavior: auto; }
}

Texto

Cortar el texto a N líneas

Funciona en todos

Lleva prefijo -webkit- pero funciona en todos. Para una sola línea, text-overflow: ellipsis.

.tres-lineas {
  display: -webkit-box;
  -webkit-box-orient: vertical;
  -webkit-line-clamp: 3;
  overflow: hidden;
}

Títulos equilibrados

Funciona en todos desde hace poco

Sin esa última línea con una palabra suelta. pretty hace lo mismo para párrafos, donde aún no llega, simplemente no hace nada.

h1, h2, h3 {
  text-wrap: balance;
}

p {
  text-wrap: pretty;
}

Texto con degradado

Funciona en todos

Con color: transparent a secas, un navegador que no sepa recortar el fondo dejaría el texto invisible. Dentro de @supports solo se aplica donde funciona; en el resto se ve en morado liso.

.degradado {
  color: #663399;
}

@supports (background-clip: text) or (-webkit-background-clip: text) {
  .degradado {
    background: linear-gradient(90deg, #663399, #e86f9b);
    -webkit-background-clip: text;
    background-clip: text;
    color: transparent;
  }
}

Selectores y cascada

El padre según sus hijos: :has()

Funciona en todos

El selector que pedimos durante veinte años. Aquí el campo se marca en rojo cuando lo que escribes no es un email.

.campo:has(input:user-invalid) {
  border-color: crimson;
}

.campo:has(input:user-invalid) .error {
  display: block;
}

Ordenar la cascada con @layer

Funciona en todos

Lo de una capa posterior gana siempre, tenga la especificidad que tenga. Adiós a los !important para pisar un framework.

@layer reset, framework, mio;

@layer framework {
  #app .boton { background: gray; }
}

@layer mio {
  .boton { background: rebeccapurple; }
}

Anidar sin Sass

Funciona en todos

El CSS ya anida solo. El & funciona como en Sass, y las media queries pueden ir dentro.

.boton {
  background: rebeccapurple;
  color: white;

  &:hover { background: color-mix(in oklch, rebeccapurple, black 20%); }

  .icono { margin-inline-end: .4em; }

  @media (width < 30rem) { width: 100%; }
}

Movimiento

Animar la entrada de un popover

Funciona en todos desde hace poco

@starting-style da el punto de partida de algo que acaba de aparecer, y allow-discrete deja animar display. Todo sin JS.

[popover] {
  transition: opacity .25s, translate .25s,
    display .25s allow-discrete, overlay .25s allow-discrete;
  opacity: 0;
  translate: 0 1rem;
}

[popover]:popover-open {
  opacity: 1;
  translate: 0 0;

  @starting-style {
    opacity: 0;
    translate: 0 1rem;
  }
}

Aparecer al hacer scroll

Aún falta algún navegador

Una animación que avanza con el scroll en lugar de con el tiempo. Sin IntersectionObserver. Haz scroll dentro de la demo.

@supports (animation-timeline: view()) {
  @media (prefers-reduced-motion: no-preference) {
    .aparece {
      animation: aparece linear both;
      animation-timeline: view();
      animation-range: entry 0% cover 30%;
    }
  }
}

@keyframes aparece {
  from { opacity: 0; scale: .8; }
}

Respetar «reducir movimiento»

Funciona en todos

Hay gente a la que las animaciones marean de verdad. Esto las apaga casi del todo si lo han pedido en el sistema.

@media (prefers-reduced-motion: reduce) {
  *, *::before, *::after {
    animation-duration: .01ms !important;
    animation-iteration-count: 1 !important;
    transition-duration: .01ms !important;
    scroll-behavior: auto !important;
  }
}

Color

Claro y oscuro con light-dark()

Funciona en todos desde hace poco

Cada color dice sus dos versiones en una línea, y color-scheme hace que formularios y scrollbars acompañen.

:root {
  color-scheme: light dark;
  --fondo: light-dark(#fbfaf7, #17121f);
  --texto: light-dark(#221a2e, #ece6f5);
  --acento: light-dark(rebeccapurple, #c9a7f2);
}

body {
  background: var(--fondo);
  color: var(--texto);
}

Estados con color-mix()

Funciona en todos

Un solo color de marca y el hover, el activo y el fondo suave salen de él. Mezclar en oklch da tonos más limpios.

.boton {
  --c: rebeccapurple;
  background: var(--c);
}
.boton:hover  { background: color-mix(in oklch, var(--c), white 15%); }
.boton:active { background: color-mix(in oklch, var(--c), black 20%); }
.suave        { background: color-mix(in oklch, var(--c) 12%, transparent); }

Formularios del color de la marca

Funciona en todos

Checkboxes, radios, sliders y barras de progreso nativos, con tu color y sin rehacerlos.

:root {
  accent-color: rebeccapurple;
}

Accesibilidad

Foco visible solo con teclado

Funciona en todos

Nunca quites el outline sin más. Con :focus-visible el anillo sale al tabular y no al hacer clic.

:focus-visible {
  outline: 3px solid rebeccapurple;
  outline-offset: 3px;
  border-radius: 4px;
}

Oculto a la vista, no al lector de pantalla

Funciona en todos

Para el texto de un botón que solo es un icono. display: none lo escondería también para quien no ve.

.solo-lectores {
  position: absolute;
  width: 1px;
  height: 1px;
  margin: -1px;
  padding: 0;
  overflow: hidden;
  clip-path: inset(50%);
  white-space: nowrap;
  border: 0;
}

↑↓ moverse · ↵ abrir · esc cerrar