Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
62 commits
Select commit Hold shift + click to select a range
fdaae27
The CSS Selection - 2026 Edition
bartveneman Jan 22, 2026
f94b9b8
add scroll spy for nav
bartveneman Jan 25, 2026
3ff6c15
most of atrules done
bartveneman Jan 25, 2026
c5c2547
more details
bartveneman Jan 25, 2026
31cbe6d
add links from nav and index page
bartveneman Jan 26, 2026
9899d68
add most metrics
bartveneman Jan 26, 2026
6067782
tweaks
bartveneman Jan 26, 2026
0132676
add content to selector chapter
bartveneman Jan 27, 2026
2b670a0
add content to values chapter
bartveneman Jan 27, 2026
8529c34
add more values + specificity
bartveneman Jan 28, 2026
af0766c
more selectors stats
bartveneman Jan 29, 2026
3c53436
add rulesets content
bartveneman Jan 29, 2026
7c6db5a
ssr ToC
bartveneman Jan 29, 2026
324bad9
improve scroll highlight nav
bartveneman Jan 29, 2026
607f731
more content
bartveneman Jan 29, 2026
6ff275f
spelling corrections
bartveneman Jan 29, 2026
524d891
rework some files
bartveneman Jan 30, 2026
3bcff10
design tweaks
bartveneman Jan 30, 2026
101795e
fix y-axis labels
bartveneman Jan 30, 2026
fd7301c
improve chart a11y
bartveneman Jan 31, 2026
388308a
better content for atrules chapter
bartveneman Jan 31, 2026
f8d344e
add source data files
bartveneman Jan 31, 2026
0e595d2
add links to raw data to table charts
bartveneman Jan 31, 2026
accf8e9
add supports hacks and unique queries
bartveneman Jan 31, 2026
c9e845e
rename Raw data to Source data
bartveneman Jan 31, 2026
3c571e3
some conclusions
bartveneman Jan 31, 2026
1bbc4c1
improve a11y of bar charts with alt texts
bartveneman Jan 31, 2026
d210859
another round of editing, disclaimers, etc
bartveneman Feb 1, 2026
586dc2a
more editing
bartveneman Feb 1, 2026
cff9400
add OG image
bartveneman Feb 1, 2026
94adfc3
fix linting
bartveneman Feb 1, 2026
4eb6ba4
update aspect-ratio of OG image
bartveneman Feb 1, 2026
eeb8736
add note for vendor prefixed values
bartveneman Feb 1, 2026
f4b5522
add links to vendor prefixed props including blog posts
bartveneman Feb 1, 2026
1181776
add index page with single card link to 2026 edition
bartveneman Feb 1, 2026
b28812f
add SEO fields to index page
bartveneman Feb 1, 2026
a8dc7f0
add links to css-selection from hompage, footer, cmd-k
bartveneman Feb 1, 2026
d8cbc84
fix contrast issues in light mode
bartveneman Feb 1, 2026
56bc106
first review round
bartveneman Feb 2, 2026
e59b482
more editorial
bartveneman Feb 3, 2026
1f54fd0
add main Polypane banner to intro
bartveneman Feb 3, 2026
b1f72f0
add several polypane callouts in the content
bartveneman Feb 3, 2026
0a5e04f
finish Declans feedback
bartveneman Feb 3, 2026
dda180e
typo
bartveneman Feb 3, 2026
19f7bb8
open polypane in new tabs; better stacking context content
bartveneman Feb 4, 2026
e262946
add year to utm codes
bartveneman Feb 4, 2026
f651e9a
fix sticky-ness of nav
bartveneman Feb 4, 2026
5a91095
fix sticky-ness of nav
bartveneman Feb 4, 2026
8902a0a
fix chart label sizes
bartveneman Feb 4, 2026
0ba4d12
polypane callout design tweaks
bartveneman Feb 4, 2026
ba9ad3b
fix: nav margin on small screen
bartveneman Feb 4, 2026
090abc4
spacing + chart label size tweaks
bartveneman Feb 4, 2026
8a9a303
spacing tweaks for header
bartveneman Feb 4, 2026
07af103
finish conclusion
bartveneman Feb 4, 2026
fa116b5
add playwright tests
bartveneman Feb 4, 2026
5062dc5
adjust sponsor callouts + add one more to conclusion
bartveneman Feb 4, 2026
5358e53
improve text wrapping for polypane + math
bartveneman Feb 4, 2026
fbeed4e
rm robots noindex,nofollow
bartveneman Feb 4, 2026
f31d93c
more editorial feedback processed
bartveneman Feb 4, 2026
2dce32e
add announcement blog post
bartveneman Feb 4, 2026
7074f98
rm unused prism css to increase css coverage
bartveneman Feb 4, 2026
a329b37
select different blog post
bartveneman Feb 4, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions content/blog/2026-02-06-introducing-the-css-selection.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
---
title: Announcing The CSS Selection
excerpt: The most comprehensive large-scale CSS analysis ever documented is now online.
---

After weeks of scraping and analyzing more than 100,000 websites I'm happy to announce: [The CSS Selection 2026](/the-css-selection/2026)! This is the biggest ever attempt (as far as I know) to look at CSS from a high level and draw conclusions from usage of new CSS features and anti-patterns. It features even more metrics than the [Web Almanac](https://almanac.httparchive.org/en/), although I hope it is obvious that the Web Almanac is the giant on whose shoulders this article stands. On top of that it also highlights some insane outliers, just because it's fun to do, and it may help you feel good about your own situation.

The CSS Selection is a new concept for Project Wallace that I've wanted to do for a long time. It has taken tremendous effort to get this across the finish line but now it's here and I couldn't be more proud of it. There is also a big list of improvements that I plan on incorporating in future editions so expect this to become even better.

The run up to publishing this has been so much fun. During analysis I've shared several statistics and screenshots on social media. People's emotions ranged from sheer laughter to outrage. The design aspect of the whole thing was also pretty nice to do. I even attempted so make a [card-like image](https://codepen.io/bartveneman/pen/azZYqLZ) for previews on social media. It took a little bit of fumbling, but I'm happy with how it turned out.

Another very cool aspect of The CSS Selection is that it's publicly sponsored by [Polypane](https://polypane.app/). That means that I had to work in a few pieces of sponsored content, but man, does it fit the rest of the content well! I had linked to at least two Polypane articles/pages already before Kilian came on board. It highlights how well Polypane fits this article and I'm proud to now have a second sponsor, next to [Netlify](https://www.netlify.com/), who kindly sponsor the hosting and deployment of this website.

[Read The CSS Selection now!](/the-css-selection/2026)
8 changes: 8 additions & 0 deletions src/app.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -30,3 +30,11 @@ declare module '*.md' {

export const metadata: Record<string, unknown>
}

declare module '*.svx' {
import type { SvelteComponent } from 'svelte'

export default class Comp extends SvelteComponent {}

export const metadata: Record<string, unknown>
}
58 changes: 58 additions & 0 deletions src/lib/components/Award.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
<script lang="ts">
let { children, size } = $props()
</script>

<aside class="award">
<span class="size" aria-hidden="true">{size}</span>
<p class="content">
{@render children?.()}
</p>
</aside>

<style>
.award {
margin-block: var(--space-12) var(--space-20);
width: auto;
max-width: min(36rem, 90%);
position: relative;
display: flow-root;

&::after {
content: '';
position: absolute;
z-index: -1;
inset: 1rem -1rem -1rem 1rem;
background-color: var(--bg-200);
}

&::before {
content: '';
position: absolute;
inset: 2rem -2rem -2rem 2rem;
z-index: -1;
border: 2px solid var(--bg-300);
}
}

.size {
font-size: var(--size-3xl);
font-weight: var(--font-ultrabold);
border: 4px solid var(--bg-500);
padding-block: var(--space-6);
padding-inline: var(--space-4);
float: left;
margin-inline-end: var(--space-8);
margin-block-end: var(--space-1);
background-color: light-dark(var(--bg-200), var(--bg-300));
position: relative;
}

.content {
padding-block-start: var(--space-8);
padding-inline-start: var(--space-8);
font-style: italic;
margin: 0 !important; /* reset markdown margin */
font-size: var(--size-base);
text-wrap: balance;
}
</style>
204 changes: 204 additions & 0 deletions src/lib/components/BarChart.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,204 @@
<script lang="ts">
import { format_number } from '$lib/format-number'

type Props = {
data: Record<string, number>
formatter?: (value: number) => string
title: string
alt: string
}

let { data, formatter = format_number, title, alt }: Props = $props()

// Chart dimensions
const width = 500
const height = 200
const margin = { top: 20, right: 30, bottom: 25, left: 60 }
const chart_width = width - margin.left - margin.right
const chart_height = height - margin.top - margin.bottom
const bar_width = 20
const num_y_ticks = 10

// Calculate scales - derived from data
const max_value = $derived(Math.max(...Object.values(data)))
const y_max = $derived.by(() => {
// Determine rounding magnitude based on max value
if (max_value === 0) {
return 1
} else if (max_value < 1) {
return Math.ceil(max_value * 10) / 10 // Round to nearest 0.1
} else if (max_value <= 100) {
return Math.ceil(max_value / 10) * 10 // Round to nearest 10
}
return Math.ceil(max_value / 100) * 100 // Round to nearest 100
})
const y_scale = $derived(chart_height / y_max)
const bar_spacing = $derived(chart_width / Object.values(data).length)

// Generate y-axis ticks with nice round numbers
const y_ticks = $derived.by(() => {
// When all values are 0, only show a 0 tick
if (max_value === 0) {
return [{ value: 0, y: chart_height }]
}

// Calculate ideal step size
const ideal_step = y_max / (num_y_ticks - 1)

// Find magnitude and round to nice increment (1, 2, 5, 10, 20, 50, 100, etc.)
const magnitude = Math.pow(10, Math.floor(Math.log10(ideal_step)))
const normalized = ideal_step / magnitude
const nice_step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10
const step = nice_step * magnitude

// Generate ticks from 0 to yMax using the nice step
const ticks = []
for (let value = 0; value <= y_max; value += step) {
const y = chart_height - value * y_scale
ticks.push({ value, y })
}
return ticks
})

// Generate bars
const bars = $derived(
Object.entries(data).map(([label, value], i) => {
const x = i * bar_spacing + bar_spacing / 2 - bar_width / 2
const bar_height = value * y_scale
const y = chart_height - bar_height
return {
x,
y,
width: bar_width,
height: bar_height,
label: label,
value: value,
center_x: i * bar_spacing + bar_spacing / 2
}
})
)

let uid = $props.id()
let title_id = `${uid}-title`
let description_id = `${uid}-description`
</script>

<div class="bar-chart">
<!-- Accessible title is in the SVG itself as title -->
<div id={title_id} class="[ title ] chart-title" aria-hidden="true">{title}</div>

<svg
xmlns="http://www.w3.org/2000/svg"
class="chart"
role="img"
aria-labelledby={`${title_id} ${description_id}`}
viewBox="0 0 {width} {height}"
>
<title id={title_id}>{title}</title>
<desc id={description_id}>{alt}</desc>
<g transform="translate({margin.left},{margin.top})">
<!-- Grid lines -->
<g class="grid" style="stroke: currentcolor; opacity: 0.2;" fill="none" text-anchor="end">
<path class="domain" stroke="currentColor" d="M{chart_width},{chart_height}H0V0H{chart_width}" />
{#each y_ticks as tick}
<g class="tick" opacity="1" transform="translate(0,{tick.y})">
<line stroke="currentColor" x2={chart_width} />
</g>
{/each}
</g>

<!-- Y-axis -->
<g fill="none" text-anchor="end">
<path stroke="currentColor" d="M-6,{chart_height}H0V0H-6" />
{#each y_ticks as tick}
<g class="tick" opacity="1" transform="translate(0,{tick.y})">
<line stroke="currentColor" x2="-6" />
<text fill="currentColor" x="-9" dy="0.32em" class="axis-label">
{formatter(tick.value)}
</text>
</g>
{/each}
</g>

<!-- X-axis -->
<g transform="translate(0,{chart_height})" fill="none" text-anchor="middle">
<path stroke="currentColor" d="M0,6V0H{chart_width}V6" />
{#each bars as bar}
<g class="tick" opacity="1" transform="translate({bar.center_x},0)">
<line stroke="currentColor" y2="6" />
<text fill="currentColor" y="9" dy="0.7em" text-anchor="middle" class="axis-label">
{bar.label}
</text>
</g>
{/each}
</g>

<!-- Bars -->
{#each bars as bar}
<rect x={bar.x} y={bar.y} width={bar.width} height={bar.height} fill="currentColor" class="bar" />
<text x={bar.center_x} y={bar.y - 10} text-anchor="middle" fill="currentColor" class="bar-label">
{formatter(bar.value)}
</text>
{/each}
</g>
</svg>

<details>
<summary>View chart as table</summary>
<table>
<caption>{title}</caption>
<thead>
<tr>
<th scope="col">Percentile</th>
<th scope="col">Value</th>
</tr>
</thead>
<tbody>
{#each bars as bar}
<tr>
<th scope="row">{bar.label}</th>
<td>{formatter(bar.value)}</td>
</tr>
{/each}
</tbody>
</table>
</details>
</div>

<style>
.chart-title {
font-size: var(--size-sm);
text-align: center;
}

.bar-label,
.axis-label {
font-size: 0.7rem;

@media (min-width: 60rem) {
font-size: 0.55rem;
}
}

.bar {
fill: var(--accent-400);
}

.bar-label {
fill: var(--fg-200);
}

svg {
color: var(--fg-300);
}

text {
fill: currentColor;
color: var(--fg-200);
}

summary {
text-align: center;
font-size: var(--size-base);
}
</style>
2 changes: 1 addition & 1 deletion src/lib/components/Button.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -39,10 +39,10 @@
<!-- We know this is either a <a> or a <button> -->
<!-- svelte-ignore a11y_no_static_element_interactions -->
<svelte:element this={element} onclick={on_click} class="btn {variant} {size} {classname}" {...rest}>
{@render children?.()}
{#if icon}
<Icon name={icon} size={14} />
{/if}
{@render children?.()}
</svelte:element>

<style>
Expand Down
5 changes: 3 additions & 2 deletions src/lib/components/Footer.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,13 +25,14 @@
},
{
title: 'CSS Games',
items: [{ title: 'CSS Units memory game', href: '/css-units-game' }]
items: [{ title: 'CSS Units memory', href: '/css-units-game' }]
},
{
title: 'Documentation',
items: [
{ href: '/docs', title: 'Docs' },
{ href: '/blog', title: 'Blog' }
{ href: '/blog', title: 'Blog' },
{ href: '/the-css-selection', title: 'The CSS Selection' }
]
},
{
Expand Down
14 changes: 14 additions & 0 deletions src/lib/components/Formula.svelte
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
<script lang="ts">
let { children } = $props()
</script>

<div class="formula">
{@render children?.()}
</div>

<style>
.formula {
font-size: var(--size-2xl);
font-family: cursive;
}
</style>
14 changes: 12 additions & 2 deletions src/lib/components/Heading.svelte
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
<script lang="ts">
import type { SvelteHTMLElements } from 'svelte/elements'
type HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'span' | 'div'
type HeadingSize = undefined | 1 | 2 | 3 | 4 | 5 | 6
type HeadingSize = undefined | 1 | 2 | 3 | 4 | 5 | 6 | 0

const SIZE_MAP = {
['h1' as HeadingElement]: 1,
Expand All @@ -19,7 +19,7 @@

let { element, size, children, class: className, ...rest }: SvelteHTMLElements['div'] & Props = $props()

let calculated_size = $derived(size || SIZE_MAP[element])
let calculated_size = $derived(size ?? SIZE_MAP[element])
</script>

<svelte:element this={element} class={[`heading heading-size-${calculated_size}`, className]} {...rest}>
Expand All @@ -36,6 +36,16 @@
text-wrap: pretty;
}

.heading-size-0 {
color: light-dark(var(--black), var(--white));
font-weight: var(--font-ultrabold);
font-size: var(--size-5xl);

@container (width > 44rem) {
font-size: var(--size-7xl);
}
}

h1,
.heading-size-1 {
color: light-dark(var(--black), var(--white));
Expand Down
Loading
Loading