Skip to content

Commit a8a4bb5

Browse files
authored
The CSS Selection - 2026 Edition (#119)
closes #105 closes #104
1 parent cae2795 commit a8a4bb5

123 files changed

Lines changed: 260192 additions & 125 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
---
2+
title: Announcing The CSS Selection
3+
excerpt: The most comprehensive large-scale CSS analysis ever documented is now online.
4+
---
5+
6+
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.
7+
8+
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.
9+
10+
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.
11+
12+
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.
13+
14+
[Read The CSS Selection now!](/the-css-selection/2026)

src/app.d.ts

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,3 +30,11 @@ declare module '*.md' {
3030

3131
export const metadata: Record<string, unknown>
3232
}
33+
34+
declare module '*.svx' {
35+
import type { SvelteComponent } from 'svelte'
36+
37+
export default class Comp extends SvelteComponent {}
38+
39+
export const metadata: Record<string, unknown>
40+
}

src/lib/components/Award.svelte

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
<script lang="ts">
2+
let { children, size } = $props()
3+
</script>
4+
5+
<aside class="award">
6+
<span class="size" aria-hidden="true">{size}</span>
7+
<p class="content">
8+
{@render children?.()}
9+
</p>
10+
</aside>
11+
12+
<style>
13+
.award {
14+
margin-block: var(--space-12) var(--space-20);
15+
width: auto;
16+
max-width: min(36rem, 90%);
17+
position: relative;
18+
display: flow-root;
19+
20+
&::after {
21+
content: '';
22+
position: absolute;
23+
z-index: -1;
24+
inset: 1rem -1rem -1rem 1rem;
25+
background-color: var(--bg-200);
26+
}
27+
28+
&::before {
29+
content: '';
30+
position: absolute;
31+
inset: 2rem -2rem -2rem 2rem;
32+
z-index: -1;
33+
border: 2px solid var(--bg-300);
34+
}
35+
}
36+
37+
.size {
38+
font-size: var(--size-3xl);
39+
font-weight: var(--font-ultrabold);
40+
border: 4px solid var(--bg-500);
41+
padding-block: var(--space-6);
42+
padding-inline: var(--space-4);
43+
float: left;
44+
margin-inline-end: var(--space-8);
45+
margin-block-end: var(--space-1);
46+
background-color: light-dark(var(--bg-200), var(--bg-300));
47+
position: relative;
48+
}
49+
50+
.content {
51+
padding-block-start: var(--space-8);
52+
padding-inline-start: var(--space-8);
53+
font-style: italic;
54+
margin: 0 !important; /* reset markdown margin */
55+
font-size: var(--size-base);
56+
text-wrap: balance;
57+
}
58+
</style>

src/lib/components/BarChart.svelte

Lines changed: 204 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,204 @@
1+
<script lang="ts">
2+
import { format_number } from '$lib/format-number'
3+
4+
type Props = {
5+
data: Record<string, number>
6+
formatter?: (value: number) => string
7+
title: string
8+
alt: string
9+
}
10+
11+
let { data, formatter = format_number, title, alt }: Props = $props()
12+
13+
// Chart dimensions
14+
const width = 500
15+
const height = 200
16+
const margin = { top: 20, right: 30, bottom: 25, left: 60 }
17+
const chart_width = width - margin.left - margin.right
18+
const chart_height = height - margin.top - margin.bottom
19+
const bar_width = 20
20+
const num_y_ticks = 10
21+
22+
// Calculate scales - derived from data
23+
const max_value = $derived(Math.max(...Object.values(data)))
24+
const y_max = $derived.by(() => {
25+
// Determine rounding magnitude based on max value
26+
if (max_value === 0) {
27+
return 1
28+
} else if (max_value < 1) {
29+
return Math.ceil(max_value * 10) / 10 // Round to nearest 0.1
30+
} else if (max_value <= 100) {
31+
return Math.ceil(max_value / 10) * 10 // Round to nearest 10
32+
}
33+
return Math.ceil(max_value / 100) * 100 // Round to nearest 100
34+
})
35+
const y_scale = $derived(chart_height / y_max)
36+
const bar_spacing = $derived(chart_width / Object.values(data).length)
37+
38+
// Generate y-axis ticks with nice round numbers
39+
const y_ticks = $derived.by(() => {
40+
// When all values are 0, only show a 0 tick
41+
if (max_value === 0) {
42+
return [{ value: 0, y: chart_height }]
43+
}
44+
45+
// Calculate ideal step size
46+
const ideal_step = y_max / (num_y_ticks - 1)
47+
48+
// Find magnitude and round to nice increment (1, 2, 5, 10, 20, 50, 100, etc.)
49+
const magnitude = Math.pow(10, Math.floor(Math.log10(ideal_step)))
50+
const normalized = ideal_step / magnitude
51+
const nice_step = normalized <= 1 ? 1 : normalized <= 2 ? 2 : normalized <= 5 ? 5 : 10
52+
const step = nice_step * magnitude
53+
54+
// Generate ticks from 0 to yMax using the nice step
55+
const ticks = []
56+
for (let value = 0; value <= y_max; value += step) {
57+
const y = chart_height - value * y_scale
58+
ticks.push({ value, y })
59+
}
60+
return ticks
61+
})
62+
63+
// Generate bars
64+
const bars = $derived(
65+
Object.entries(data).map(([label, value], i) => {
66+
const x = i * bar_spacing + bar_spacing / 2 - bar_width / 2
67+
const bar_height = value * y_scale
68+
const y = chart_height - bar_height
69+
return {
70+
x,
71+
y,
72+
width: bar_width,
73+
height: bar_height,
74+
label: label,
75+
value: value,
76+
center_x: i * bar_spacing + bar_spacing / 2
77+
}
78+
})
79+
)
80+
81+
let uid = $props.id()
82+
let title_id = `${uid}-title`
83+
let description_id = `${uid}-description`
84+
</script>
85+
86+
<div class="bar-chart">
87+
<!-- Accessible title is in the SVG itself as title -->
88+
<div id={title_id} class="[ title ] chart-title" aria-hidden="true">{title}</div>
89+
90+
<svg
91+
xmlns="http://www.w3.org/2000/svg"
92+
class="chart"
93+
role="img"
94+
aria-labelledby={`${title_id} ${description_id}`}
95+
viewBox="0 0 {width} {height}"
96+
>
97+
<title id={title_id}>{title}</title>
98+
<desc id={description_id}>{alt}</desc>
99+
<g transform="translate({margin.left},{margin.top})">
100+
<!-- Grid lines -->
101+
<g class="grid" style="stroke: currentcolor; opacity: 0.2;" fill="none" text-anchor="end">
102+
<path class="domain" stroke="currentColor" d="M{chart_width},{chart_height}H0V0H{chart_width}" />
103+
{#each y_ticks as tick}
104+
<g class="tick" opacity="1" transform="translate(0,{tick.y})">
105+
<line stroke="currentColor" x2={chart_width} />
106+
</g>
107+
{/each}
108+
</g>
109+
110+
<!-- Y-axis -->
111+
<g fill="none" text-anchor="end">
112+
<path stroke="currentColor" d="M-6,{chart_height}H0V0H-6" />
113+
{#each y_ticks as tick}
114+
<g class="tick" opacity="1" transform="translate(0,{tick.y})">
115+
<line stroke="currentColor" x2="-6" />
116+
<text fill="currentColor" x="-9" dy="0.32em" class="axis-label">
117+
{formatter(tick.value)}
118+
</text>
119+
</g>
120+
{/each}
121+
</g>
122+
123+
<!-- X-axis -->
124+
<g transform="translate(0,{chart_height})" fill="none" text-anchor="middle">
125+
<path stroke="currentColor" d="M0,6V0H{chart_width}V6" />
126+
{#each bars as bar}
127+
<g class="tick" opacity="1" transform="translate({bar.center_x},0)">
128+
<line stroke="currentColor" y2="6" />
129+
<text fill="currentColor" y="9" dy="0.7em" text-anchor="middle" class="axis-label">
130+
{bar.label}
131+
</text>
132+
</g>
133+
{/each}
134+
</g>
135+
136+
<!-- Bars -->
137+
{#each bars as bar}
138+
<rect x={bar.x} y={bar.y} width={bar.width} height={bar.height} fill="currentColor" class="bar" />
139+
<text x={bar.center_x} y={bar.y - 10} text-anchor="middle" fill="currentColor" class="bar-label">
140+
{formatter(bar.value)}
141+
</text>
142+
{/each}
143+
</g>
144+
</svg>
145+
146+
<details>
147+
<summary>View chart as table</summary>
148+
<table>
149+
<caption>{title}</caption>
150+
<thead>
151+
<tr>
152+
<th scope="col">Percentile</th>
153+
<th scope="col">Value</th>
154+
</tr>
155+
</thead>
156+
<tbody>
157+
{#each bars as bar}
158+
<tr>
159+
<th scope="row">{bar.label}</th>
160+
<td>{formatter(bar.value)}</td>
161+
</tr>
162+
{/each}
163+
</tbody>
164+
</table>
165+
</details>
166+
</div>
167+
168+
<style>
169+
.chart-title {
170+
font-size: var(--size-sm);
171+
text-align: center;
172+
}
173+
174+
.bar-label,
175+
.axis-label {
176+
font-size: 0.7rem;
177+
178+
@media (min-width: 60rem) {
179+
font-size: 0.55rem;
180+
}
181+
}
182+
183+
.bar {
184+
fill: var(--accent-400);
185+
}
186+
187+
.bar-label {
188+
fill: var(--fg-200);
189+
}
190+
191+
svg {
192+
color: var(--fg-300);
193+
}
194+
195+
text {
196+
fill: currentColor;
197+
color: var(--fg-200);
198+
}
199+
200+
summary {
201+
text-align: center;
202+
font-size: var(--size-base);
203+
}
204+
</style>

src/lib/components/Button.svelte

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,10 +39,10 @@
3939
<!-- We know this is either a <a> or a <button> -->
4040
<!-- svelte-ignore a11y_no_static_element_interactions -->
4141
<svelte:element this={element} onclick={on_click} class="btn {variant} {size} {classname}" {...rest}>
42+
{@render children?.()}
4243
{#if icon}
4344
<Icon name={icon} size={14} />
4445
{/if}
45-
{@render children?.()}
4646
</svelte:element>
4747

4848
<style>

src/lib/components/Footer.svelte

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,14 @@
2525
},
2626
{
2727
title: 'CSS Games',
28-
items: [{ title: 'CSS Units memory game', href: '/css-units-game' }]
28+
items: [{ title: 'CSS Units memory', href: '/css-units-game' }]
2929
},
3030
{
3131
title: 'Documentation',
3232
items: [
3333
{ href: '/docs', title: 'Docs' },
34-
{ href: '/blog', title: 'Blog' }
34+
{ href: '/blog', title: 'Blog' },
35+
{ href: '/the-css-selection', title: 'The CSS Selection' }
3536
]
3637
},
3738
{

src/lib/components/Formula.svelte

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
<script lang="ts">
2+
let { children } = $props()
3+
</script>
4+
5+
<div class="formula">
6+
{@render children?.()}
7+
</div>
8+
9+
<style>
10+
.formula {
11+
font-size: var(--size-2xl);
12+
font-family: cursive;
13+
}
14+
</style>

src/lib/components/Heading.svelte

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
<script lang="ts">
22
import type { SvelteHTMLElements } from 'svelte/elements'
33
type HeadingElement = 'h1' | 'h2' | 'h3' | 'h4' | 'span' | 'div'
4-
type HeadingSize = undefined | 1 | 2 | 3 | 4 | 5 | 6
4+
type HeadingSize = undefined | 1 | 2 | 3 | 4 | 5 | 6 | 0
55
66
const SIZE_MAP = {
77
['h1' as HeadingElement]: 1,
@@ -19,7 +19,7 @@
1919
2020
let { element, size, children, class: className, ...rest }: SvelteHTMLElements['div'] & Props = $props()
2121
22-
let calculated_size = $derived(size || SIZE_MAP[element])
22+
let calculated_size = $derived(size ?? SIZE_MAP[element])
2323
</script>
2424

2525
<svelte:element this={element} class={[`heading heading-size-${calculated_size}`, className]} {...rest}>
@@ -36,6 +36,16 @@
3636
text-wrap: pretty;
3737
}
3838
39+
.heading-size-0 {
40+
color: light-dark(var(--black), var(--white));
41+
font-weight: var(--font-ultrabold);
42+
font-size: var(--size-5xl);
43+
44+
@container (width > 44rem) {
45+
font-size: var(--size-7xl);
46+
}
47+
}
48+
3949
h1,
4050
.heading-size-1 {
4151
color: light-dark(var(--black), var(--white));

0 commit comments

Comments
 (0)