This document explains the modern CSS features used in Fizzy's stylesheets. It's written for developers familiar with HTML who want to understand contemporary CSS techniques.
- CSS Layers
- CSS Custom Properties (Variables)
- CSS Nesting
- Modern Selectors
- Logical Properties
- Modern Units
- CSS Functions
- Container Queries
- Modern Color Formats
- CSS Grid
- Flexbox and Gap
- Aspect Ratio
- Media Queries
- Feature Queries (@supports)
- Animations and Transitions
- Scroll Behavior
- Modern Pseudo-Classes
- Modern Pseudo-Elements
- Form Styling
- Filters and Blend Modes
- CSS Containment
- Text Wrapping
What it does: Organizes CSS rules into named groups that control which styles take priority.
@layer reset, base, components, modules, utilities, native, platform;Layers listed later win over earlier ones. So platform styles beat utilities, which beat components, and so on.
@layer base {
body {
font-family: sans-serif;
}
}
@layer components {
.btn {
padding: 1rem;
}
}Why use it: Without layers, CSS specificity can be unpredictable. Layers give you explicit control over which styles should override others, regardless of selector complexity or source order.
Files: _global.css, base.css, buttons.css, and most other stylesheets
What it does: Lets you define reusable values that can be referenced throughout your CSS.
:root {
--inline-space: 1ch;
--block-space: 1rem;
--color-link: oklch(57% 0.19 260);
}
.button {
padding: var(--block-space) var(--inline-space);
color: var(--color-link);
}Key concepts:
| Syntax | Meaning |
|---|---|
--name |
Declares a variable |
var(--name) |
Uses a variable |
var(--name, fallback) |
Uses a variable with a fallback value |
:root |
The <html> element - variables here are global |
Math with variables:
--inline-space-half: calc(var(--inline-space) / 2);
--inline-space-double: calc(var(--inline-space) * 2);Scoped variables: Variables can be redefined in any selector:
:root {
--text-color: black;
}
.dark-mode {
--text-color: white; /* Overrides within .dark-mode */
}Files: _global.css defines 100+ variables used throughout the project
What it does: Lets you write child selectors inside parent selectors, similar to Sass/SCSS but native to CSS.
/* Without nesting */
.card { ... }
.card:hover { ... }
.card .title { ... }
.card.featured { ... }
/* With nesting */
.card {
...
&:hover { ... }
.title { ... }
&.featured { ... }
}The & symbol represents the parent selector:
| Nested Selector | Compiles To |
|---|---|
&:hover |
.card:hover |
&.active |
.card.active |
& .child |
.card .child |
.parent & |
.parent .card |
Real example from buttons.css:
.btn {
padding: 0.5em 1em;
&[disabled] {
opacity: 0.5;
cursor: not-allowed;
}
&:has(input:checked) {
background: var(--color-selected);
}
}Files: Used extensively in all component stylesheets
What it does: Matches any element that matches one of the selectors in its list. Reduces repetition.
/* Without :is() */
.card h1,
.card h2,
.card h3 {
margin: 0;
}
/* With :is() */
.card :is(h1, h2, h3) {
margin: 0;
}Specificity note: Takes the specificity of its most specific argument.
What it does: Same as :is() but with zero specificity, making it easy to override.
/* Easy to override later */
:where(#main) {
padding: 1rem;
}
/* This will win even though #main is an ID */
.custom-main {
padding: 2rem;
}What it does: Selects elements that contain specific children. This is the "parent selector" CSS developers wanted for decades.
/* Select cards that contain an image */
.card:has(img) {
padding: 0;
}
/* Select inputs that have a checked checkbox */
.btn:has(input:checked) {
background: blue;
}
/* Select nav when popup is open */
.nav:has(.popup[open]) {
z-index: 100;
}What it does: Matches elements that don't match the given selector.
/* All paragraphs except those with a class */
p:not([class]) {
margin-bottom: 1rem;
}
/* Links that aren't buttons */
a:not(.btn) {
text-decoration: underline;
}What it does: Matches elements that are focused AND the browser determines focus should be visible (typically keyboard navigation, not mouse clicks).
.btn:focus-visible {
outline: 2px solid blue;
outline-offset: 2px;
}Why use it: Shows focus rings for keyboard users (accessibility) without showing them for mouse users (cleaner UI).
Files: base.css, buttons.css, inputs.css, cards.css
What it does: Replaces direction-specific properties (left, right, top, bottom) with logical ones that automatically adapt to text direction (LTR or RTL languages).
| Physical Property | Logical Property | Direction |
|---|---|---|
width |
inline-size |
Horizontal in LTR |
height |
block-size |
Vertical in LTR |
margin-left |
margin-inline-start |
Start of text |
margin-right |
margin-inline-end |
End of text |
margin-top |
margin-block-start |
Start of block |
margin-bottom |
margin-block-end |
End of block |
padding-left/right |
padding-inline |
Both horizontal |
padding-top/bottom |
padding-block |
Both vertical |
left |
inset-inline-start |
Start position |
right |
inset-inline-end |
End position |
top |
inset-block-start |
Block start |
bottom |
inset-block-end |
Block end |
text-align: left |
text-align: start |
Start of text |
text-align: right |
text-align: end |
End of text |
.card {
inline-size: 100%; /* width: 100% */
max-inline-size: 800px; /* max-width: 800px */
padding-inline: 1rem; /* padding-left + padding-right */
padding-block: 0.5rem; /* padding-top + padding-bottom */
margin-inline: auto; /* margin-left: auto + margin-right: auto */
}Why use it: Code automatically works for right-to-left languages (Arabic, Hebrew) without any changes.
Files: Used throughout the entire codebase
| Unit | Meaning | Example |
|---|---|---|
rem |
Relative to root font size (usually 16px) | 1rem = 16px |
em |
Relative to current element's font size | If element is 20px, 2em = 40px |
ch |
Width of the "0" character | Useful for text containers |
ex |
Height of the "x" character | Rarely used |
| Unit | Meaning |
|---|---|
vw |
1% of viewport width |
vh |
1% of viewport height |
dvw |
1% of dynamic viewport width |
dvh |
1% of dynamic viewport height |
svw |
1% of small viewport width |
svh |
1% of small viewport height |
lvw |
1% of large viewport width |
lvh |
1% of large viewport height |
Dynamic vs Static viewport: On mobile, browser UI (address bar) appears and disappears. dvw/dvh adjust to this, while vw/vh don't.
.hero {
min-block-size: 100dvh; /* Full viewport, adjusts for mobile browser UI */
}| Unit | Meaning |
|---|---|
cqi |
1% of container's inline size |
cqb |
1% of container's block size |
.card-container {
container-type: inline-size;
}
.card-title {
font-size: clamp(1rem, 5cqi, 2rem); /* Scales with container width */
}Files: _global.css, layout.css, bubble.css, card-columns.css
What it does: Performs math with CSS values, even mixing units.
.sidebar {
inline-size: calc(100% - 300px);
padding: calc(var(--space) / 2);
margin: calc(-1 * var(--bleed));
}What it does: Creates fluid values with a minimum, preferred, and maximum.
/* clamp(minimum, preferred, maximum) */
.container {
inline-size: clamp(300px, 80%, 1200px);
}
.title {
font-size: clamp(1rem, 4vw, 3rem);
}The value will:
- Never go below the minimum (300px / 1rem)
- Never exceed the maximum (1200px / 3rem)
- Use the preferred value (80% / 4vw) when it's between min and max
.card {
inline-size: min(100%, 600px); /* Whichever is smaller */
font-size: max(16px, 1em); /* Whichever is larger */
}Files: _global.css, cards.css, inputs.css, utilities.css
What it does: Lets you style elements based on their container's size, not the viewport.
/* Define a container */
.card-grid {
container-type: inline-size;
}
/* Query the container */
@container (min-width: 400px) {
.card {
display: flex;
flex-direction: row;
}
}
@container (max-width: 399px) {
.card {
display: block;
}
}Container types:
| Value | Meaning |
|---|---|
inline-size |
Query based on width |
size |
Query based on width AND height |
normal |
No containment (default) |
Why use it: Components become truly self-contained. A card can adapt to its container width whether it's in a sidebar, main content, or modal.
Files: card-columns.css, bubble.css, nav.css
What it does: Defines colors using Lightness, Chroma (saturation), and Hue in a perceptually uniform way.
/* oklch(lightness chroma hue) */
--blue: oklch(57% 0.19 260);
--red: oklch(59% 0.19 38);| Component | Range | Meaning |
|---|---|---|
| Lightness | 0% - 100% | 0% = black, 100% = white |
| Chroma | 0 - ~0.4 | 0 = gray, higher = more vivid |
| Hue | 0 - 360 | Color wheel angle |
With transparency:
background: oklch(var(--lch-black) / 50%); /* 50% opacity */Why use it: A 50% lightness blue looks as bright as a 50% lightness red. Colors are perceptually consistent.
What it does: Blends two colors together.
/* color-mix(in colorspace, color1 percentage, color2) */
background: color-mix(in srgb, var(--card-color) 10%, white);
border: color-mix(in oklch, currentColor 50%, transparent);Color spaces: srgb, hsl, oklch, lab, etc.
Files: _global.css, cards.css, rich-text-content.css, golden-effect.css
What it does: Creates two-dimensional layouts with rows and columns.
.container {
display: grid;
grid-template-columns: 200px 1fr 200px; /* 3 columns */
grid-template-rows: auto 1fr auto; /* 3 rows */
gap: 1rem; /* Space between cells */
}The fr unit represents a fraction of available space:
grid-template-columns: 1fr 2fr 1fr; /* Middle column is 2x wider */Name areas and place items by name:
.layout {
display: grid;
grid-template-areas:
"header header header"
"nav main aside"
"footer footer footer";
grid-template-columns: 200px 1fr 200px;
}
.header { grid-area: header; }
.nav { grid-area: nav; }
.main { grid-area: main; }
.aside { grid-area: aside; }
.footer { grid-area: footer; }/* As many 200px columns as will fit */
grid-template-columns: repeat(auto-fill, minmax(200px, 1fr));Files: layout.css, card-columns.css, header.css, nav.css
.container {
display: flex;
flex-direction: row; /* or column */
justify-content: center; /* main axis alignment */
align-items: center; /* cross axis alignment */
flex-wrap: wrap; /* allow wrapping */
}What it does: Adds space between flex/grid children without affecting the outer edges.
.flex-container {
display: flex;
gap: 1rem; /* Equal horizontal and vertical */
gap: 1rem 2rem; /* row-gap column-gap */
row-gap: 1rem; /* Vertical only */
column-gap: 2rem; /* Horizontal only */
}Why use it: Replaces margin-based spacing hacks. No need to remove margins from first/last children.
Files: utilities.css, buttons.css, cards.css, nav.css
What it does: Maintains a specific width-to-height ratio.
.avatar {
aspect-ratio: 1; /* Square (1:1) */
}
.video {
aspect-ratio: 16 / 9; /* Widescreen */
}
.card-image {
aspect-ratio: 4 / 3; /* Standard photo */
}The element will maintain this ratio regardless of its width.
Files: buttons.css, cards.css, bubble.css, nav.css
/* Mobile-first approach */
.card { padding: 1rem; }
@media (min-width: 640px) {
.card { padding: 2rem; }
}
@media (min-width: 960px) {
.card { padding: 3rem; }
}/* Dark mode */
@media (prefers-color-scheme: dark) {
:root {
--background: black;
--text: white;
}
}
/* Reduced motion */
@media (prefers-reduced-motion: reduce) {
* {
animation-duration: 0.01ms !important;
transition-duration: 0.01ms !important;
}
}/* Touch devices */
@media (pointer: coarse) {
.btn { min-height: 44px; } /* Larger touch targets */
}
/* Devices with hover capability */
@media (hover: hover) {
.card:hover { transform: scale(1.02); }
}
/* PWA standalone mode */
@media (display-mode: standalone) {
.install-prompt { display: none; }
}
/* Print */
@media print {
.nav, .footer { display: none; }
}Files: Used throughout; _global.css for theme, reset.css for reduced motion
What it does: Applies styles only if the browser supports a specific CSS feature.
/* Use new feature if supported */
@supports (field-sizing: content) {
textarea {
field-sizing: content; /* Auto-sizing textareas */
}
}
/* Fallback for browsers without support */
@supports not (gap: 1px) {
.flex-container > * + * {
margin-left: 1rem;
}
}Files: inputs.css, rich-text-content.css, base.css
.btn {
background: blue;
transition: background 200ms ease-out;
}
.btn:hover {
background: darkblue;
}Transition shorthand: transition: property duration timing-function delay;
:root {
--ease-out-expo: cubic-bezier(0.16, 1, 0.3, 1);
--ease-out-overshoot: cubic-bezier(0.25, 1.75, 0.5, 1);
}
.modal {
transition: transform 300ms var(--ease-out-overshoot);
}@keyframes fade-in {
from {
opacity: 0;
transform: translateY(-10px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
.card {
animation: fade-in 300ms ease-out;
}What it does: Defines the starting state for elements that are newly rendered or changed to display: block.
dialog {
opacity: 1;
transform: scale(1);
transition: opacity 200ms, transform 200ms;
@starting-style {
opacity: 0;
transform: scale(0.9);
}
}What it does: Enables transitions on discrete properties like display.
dialog {
transition: opacity 200ms, display 200ms allow-discrete;
}Files: animation.css, dialog.css, popup.css, buttons.css
html {
scroll-behavior: smooth;
}.carousel {
display: flex;
overflow-x: auto;
scroll-snap-type: x mandatory;
}
.carousel-item {
scroll-snap-align: start;
}| Property | Values |
|---|---|
scroll-snap-type |
x mandatory, y proximity, both mandatory |
scroll-snap-align |
start, center, end |
.sidebar {
scrollbar-gutter: stable; /* Reserve space for scrollbar */
}.modal-content {
overscroll-behavior: contain; /* Don't scroll parent when hitting edges */
}Files: utilities.css, search.css, nav.css, card-columns.css
| Selector | Matches |
|---|---|
:hover |
Mouse over element |
:active |
Being clicked |
:focus |
Has focus |
:focus-visible |
Focus visible (keyboard) |
:focus-within |
Contains focused element |
:disabled |
Disabled form elements |
:checked |
Checked inputs |
:placeholder-shown |
Input showing placeholder |
:valid / :invalid |
Form validation state |
| Selector | Matches |
|---|---|
:first-child |
First child of parent |
:last-child |
Last child of parent |
:nth-child(n) |
nth child (1-indexed) |
:nth-child(odd) |
1st, 3rd, 5th... |
:nth-child(even) |
2nd, 4th, 6th... |
:nth-last-child(n) |
nth from end |
:only-child |
Only child of parent |
:empty |
No children |
/* Every 3rd element starting from the 2nd */
:nth-child(3n + 2)
/* First 3 elements */
:nth-child(-n + 3)
/* nth-child with selector filter (new!) */
:nth-child(1 of .comment-by-system) /* First element matching .comment-by-system */Files: base.css, cards.css, comments.css
| Selector | Creates |
|---|---|
::before |
Content before element |
::after |
Content after element |
::backdrop |
Backdrop behind dialogs/fullscreen |
::marker |
List item markers |
::selection |
Selected text |
::placeholder |
Input placeholder text |
::file-selector-button |
File input button |
dialog::backdrop {
background: rgba(0, 0, 0, 0.5);
opacity: 0;
transition: opacity 200ms;
}
dialog[open]::backdrop {
opacity: 1;
}Files: dialog.css, popup.css, inputs.css, base.css
What it does: Colors native form controls (checkboxes, radio buttons, progress bars).
input[type="checkbox"] {
accent-color: var(--color-primary);
}What it does: Makes textareas auto-size to their content.
@supports (field-sizing: content) {
textarea {
field-sizing: content;
}
}What it does: Removes default browser styling so you can style from scratch.
select {
appearance: none;
/* Now you can fully customize it */
}Files: inputs.css
.image {
filter: brightness(1.1);
filter: grayscale(100%);
filter: blur(4px);
filter: brightness(0.9) contrast(1.1); /* Multiple */
}| Function | Effect |
|---|---|
blur(px) |
Gaussian blur |
brightness(n) |
0 = black, 1 = normal, 2 = 2x bright |
contrast(n) |
0 = gray, 1 = normal |
grayscale(%) |
0% = color, 100% = gray |
saturate(n) |
0 = gray, 1 = normal, 2 = vivid |
hue-rotate(deg) |
Shift colors on color wheel |
drop-shadow() |
Shadow that follows alpha |
.overlay {
mix-blend-mode: multiply; /* Blends with content behind */
background-blend-mode: overlay; /* Blends background layers */
}Files: buttons.css, cards.css, golden-effect.css
What it does: Tells the browser that an element's internals are independent of the rest of the page, enabling performance optimizations.
.card {
contain: inline-size; /* Size containment on inline axis */
contain: layout; /* Layout containment */
contain: paint; /* Paint containment */
contain: style; /* Style containment */
contain: content; /* layout + paint */
contain: strict; /* All containment */
}container-type: inline-size implicitly adds contain: inline-size.
Files: cards.css, utilities.css
h1 {
text-wrap: balance; /* Even line lengths */
}
p {
text-wrap: pretty; /* Avoids orphans/widows */
}
.nowrap {
text-wrap: nowrap; /* No wrapping */
}| Value | Effect |
|---|---|
balance |
Balances line lengths (good for headlines) |
pretty |
Optimizes for aesthetics, avoiding orphans |
nowrap |
Prevents wrapping |
wrap |
Normal wrapping (default) |
Files: cards.css, rich-text-content.css
This codebase demonstrates modern CSS best practices:
- Layer-based architecture for predictable cascade
- Custom properties for maintainable theming
- Logical properties for internationalization
- Container queries for truly modular components
- OKLCH colors for perceptually consistent palettes
- Fluid typography/spacing with
clamp() - Modern selectors (
:has(),:is(),:where()) for cleaner code - Accessibility-first with
:focus-visibleand reduced motion - Progressive enhancement with
@supports
The patterns here represent the current state of CSS as of 2024-2025.