Centre anything
Works everywhereTwo lines, centred on both axes. Works the same for an icon as for a modal.
.centro {
display: grid;
place-items: center;
}What I copy again and again from project to project. Each recipe has its demo running alongside: if it doesn't show in your browser, your browser doesn't support it yet.
Two lines, centred on both axes. Works the same for an icon as for a modal.
.centro {
display: grid;
place-items: center;
}As many columns as fit, no media queries. The min() keeps it from overflowing on narrow phones.
.rejilla {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(min(100%, 12rem), 1fr));
gap: 1rem;
}With little content the footer doesn't float halfway up. dvh accounts for the mobile browser bar.
body {
min-height: 100dvh;
display: grid;
grid-template-rows: auto 1fr auto;
}No more padding-top: 56.25% hack. The image is square and shows at 16:9: object-fit: cover crops it instead of stretching it (it works on img or video; on a div it does nothing).
.portada {
aspect-ratio: 16 / 9;
width: 100%;
object-fit: cover;
}The component changes with the room it has, not with the screen. Drag the box's corner to see it.
.tarjeta-wrap { container-type: inline-size; }
.tarjeta { display: grid; gap: 1rem; }
@container (min-width: 22rem) {
.tarjeta { grid-template-columns: 6rem 1fr; }
}Titles of different lengths and still every button on the same line.
.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;
}scroll-snap makes each card settle into place when you let go. With a finger or a 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;
}With a sticky header, the heading you jump to ends up covered. scroll-margin moves it down just enough.
html { scroll-behavior: smooth; }
:target,
h2[id] {
scroll-margin-top: 5rem;
}
@media (prefers-reduced-motion: reduce) {
html { scroll-behavior: auto; }
}It carries a -webkit- prefix but works everywhere. For a single line, text-overflow: ellipsis.
.tres-lineas {
display: -webkit-box;
-webkit-box-orient: vertical;
-webkit-line-clamp: 3;
overflow: hidden;
}No more last line with one lonely word. pretty does the same for paragraphs; where it isn't supported yet, it just does nothing.
h1, h2, h3 {
text-wrap: balance;
}
p {
text-wrap: pretty;
}With a bare color: transparent, a browser that can't clip the background would leave the text invisible. Inside @supports it only applies where it works; elsewhere the text is plain purple.
.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;
}
}The selector we asked for for twenty years. Here the field turns red when what you type isn't an email.
.campo:has(input:user-invalid) {
border-color: crimson;
}
.campo:has(input:user-invalid) .error {
display: block;
}A later layer always wins, whatever the specificity. Goodbye to !important just to override a framework.
@layer reset, framework, mio;
@layer framework {
#app .boton { background: gray; }
}
@layer mio {
.boton { background: rebeccapurple; }
}CSS nests on its own now. & works as in Sass, and media queries can go inside.
.boton {
background: rebeccapurple;
color: white;
&:hover { background: color-mix(in oklch, rebeccapurple, black 20%); }
.icono { margin-inline-end: .4em; }
@media (width < 30rem) { width: 100%; }
}@starting-style gives the starting point for something that just appeared, and allow-discrete lets display animate. No JS at all.
[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;
}
}An animation that moves with scrolling instead of time. No IntersectionObserver. Scroll inside the 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; }
}Animations make some people genuinely dizzy. This all but switches them off if they asked for it in their system settings.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation-duration: .01ms !important;
animation-iteration-count: 1 !important;
transition-duration: .01ms !important;
scroll-behavior: auto !important;
}
}Each colour states both versions on one line, and color-scheme brings form controls and scrollbars along.
: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);
}One brand colour, and hover, active and the soft background all come from it. Mixing in oklch gives cleaner tones.
.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); }Native checkboxes, radios, sliders and progress bars in your colour, without rebuilding them.
:root {
accent-color: rebeccapurple;
}Never just remove the outline. With :focus-visible the ring shows when tabbing and not on click.
:focus-visible {
outline: 3px solid rebeccapurple;
outline-offset: 3px;
border-radius: 4px;
}For the label of an icon-only button. display: none would hide it from people who can't see, too.
.solo-lectores {
position: absolute;
width: 1px;
height: 1px;
margin: -1px;
padding: 0;
overflow: hidden;
clip-path: inset(50%);
white-space: nowrap;
border: 0;
}