Skip to content

Commit c6f638f

Browse files
Merge pull request #49 from WebDevSimplified/css-quantity-queries
Add Quantity Query Article
2 parents 6367d26 + 3589658 commit c6f638f

File tree

4 files changed

+370
-0
lines changed

4 files changed

+370
-0
lines changed
Lines changed: 229 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,229 @@
1+
import { useState } from "react"
2+
import styles from "./cssQuantityQuery.module.css"
3+
4+
const queryTypes = ["≥", "≤", "Between"] as const
5+
type QueryType = (typeof queryTypes)[number]
6+
7+
export default function CSSQuantityQuery({
8+
hideForm = false,
9+
showQuery = false,
10+
initialQueryType = "≥",
11+
initialAmount = 3,
12+
initialAmount2 = 5,
13+
initialBoxCount = 2,
14+
}: {
15+
hideForm?: boolean
16+
showQuery?: boolean
17+
initialQueryType?: QueryType
18+
initialAmount?: number
19+
initialAmount2?: number
20+
initialBoxCount?: number
21+
}) {
22+
const [queryType, setQueryType] = useState<QueryType>(initialQueryType)
23+
const [amount, setAmount] = useState(initialAmount)
24+
const [amount2, setAmount2] = useState(initialAmount2)
25+
const [boxCount, setBoxCount] = useState(initialBoxCount)
26+
27+
return (
28+
<div
29+
style={{
30+
margin: "1rem",
31+
padding: "1rem",
32+
border: "1px dashed var(--theme-text-lighter)",
33+
}}
34+
>
35+
<div
36+
style={{
37+
display: hideForm ? "none" : "flex",
38+
gap: "1rem",
39+
flexWrap: "wrap",
40+
marginBottom: showQuery ? "0.5rem" : "1.5rem",
41+
}}
42+
>
43+
<FormGroup label="Query Type" htmlFor="queryType">
44+
<select
45+
id="queryType"
46+
value={queryType}
47+
onChange={e => setQueryType(e.target.value as QueryType)}
48+
style={{ width: "100%", fontSize: "1em" }}
49+
>
50+
{queryTypes.map(type => (
51+
<option key={type}>{type}</option>
52+
))}
53+
</select>
54+
</FormGroup>
55+
<FormGroup
56+
htmlFor="amount"
57+
label={queryType === "Between" ? "Min" : "Amount"}
58+
>
59+
<input
60+
id="amount"
61+
type="number"
62+
min="0"
63+
step="1"
64+
value={amount}
65+
style={{ width: "100%", fontSize: "1em" }}
66+
onChange={e => setAmount(e.target.valueAsNumber)}
67+
/>
68+
</FormGroup>
69+
{queryType === "Between" && (
70+
<FormGroup htmlFor="amount2" label="Max">
71+
<input
72+
id="amount2"
73+
type="number"
74+
min="0"
75+
step="1"
76+
value={amount2}
77+
style={{ width: "100%", fontSize: "1em" }}
78+
onChange={e => setAmount2(e.target.valueAsNumber)}
79+
/>
80+
</FormGroup>
81+
)}
82+
</div>
83+
{showQuery && (
84+
<pre style={{ marginBottom: "1.5rem" }}>
85+
<code>{getCode({ amount, amount2, queryType })}</code>
86+
</pre>
87+
)}
88+
<div>
89+
<div style={{ display: "flex", gap: ".5rem" }}>
90+
<button
91+
className={styles.btn}
92+
onClick={() => setBoxCount(b => b + 1)}
93+
>
94+
Add Box
95+
</button>
96+
<button
97+
className={styles.btn}
98+
onClick={() => setBoxCount(b => b - 1)}
99+
>
100+
Remove Box
101+
</button>
102+
</div>
103+
<p
104+
style={{
105+
fontSize: ".8em",
106+
color: "var(--theme-text-lighter)",
107+
marginBottom: "1rem",
108+
}}
109+
>
110+
If the quantity query is satisfied the boxes will be orange while if
111+
the query is not satisfied they will be blue.
112+
</p>
113+
<div
114+
style={{
115+
display: "grid",
116+
gridTemplateColumns: "repeat(auto-fill, minmax(200px, 1fr))",
117+
gridGap: "1rem",
118+
gridAutoRows: "100px",
119+
alignItems: "stretch",
120+
justifyItems: "stretch",
121+
}}
122+
>
123+
{Array.from({ length: boxCount }, (_, i) => (
124+
<div
125+
key={i}
126+
style={{
127+
backgroundColor: getBoxColor({
128+
amount,
129+
amount2,
130+
queryType,
131+
boxCount,
132+
}),
133+
}}
134+
/>
135+
))}
136+
</div>
137+
</div>
138+
</div>
139+
)
140+
}
141+
142+
function getCode({
143+
queryType,
144+
amount,
145+
amount2,
146+
}: {
147+
queryType: QueryType
148+
amount: number
149+
amount2: number
150+
}) {
151+
switch (queryType) {
152+
case "≥":
153+
return `/* Elements */
154+
ul li:nth-last-child(n + ${amount}),
155+
ul li:nth-last-child(n + ${amount}) ~ li {}
156+
157+
/* Container */
158+
ul:has(li:nth-last-child(n + ${amount})) {}`
159+
case "≤":
160+
return `/* Elements */
161+
ul li:nth-last-child(-n + ${amount}):first-child,
162+
ul li:nth-last-child(-n + ${amount}):first-child ~ li {}
163+
164+
/* Container */
165+
ul:has(li:nth-last-child(-n + ${amount}):first-child) {}`
166+
case "Between":
167+
return `/* Elements */
168+
ul li:nth-last-child(n + ${amount}):nth-last-child(-n + ${amount2}):first-child,
169+
ul li:nth-last-child(n + ${amount}):nth-last-child(-n + ${amount2}):first-child ~ li {}
170+
171+
/* Container */
172+
ul:has(li:nth-last-child(n + ${amount}):nth-last-child(-n + ${amount2}):first-child) {}`
173+
}
174+
}
175+
176+
function FormGroup({
177+
label,
178+
htmlFor,
179+
children,
180+
}: {
181+
label: string
182+
htmlFor: string
183+
children: React.ReactNode
184+
}) {
185+
return (
186+
<div
187+
style={{
188+
display: "flex",
189+
flexDirection: "column",
190+
alignItems: "flex-start",
191+
maxWidth: "100px",
192+
}}
193+
>
194+
<label
195+
htmlFor={htmlFor}
196+
style={{
197+
fontWeight: "semibold",
198+
fontSize: ".8em",
199+
}}
200+
>
201+
{label}
202+
</label>
203+
{children}
204+
</div>
205+
)
206+
}
207+
208+
function getBoxColor({
209+
amount,
210+
amount2,
211+
queryType,
212+
boxCount,
213+
}: {
214+
amount: number
215+
amount2: number
216+
queryType: QueryType
217+
boxCount: number
218+
}) {
219+
switch (queryType) {
220+
case "≥":
221+
return boxCount >= amount ? "var(--theme-orange)" : "var(--theme-blue)"
222+
case "≤":
223+
return boxCount <= amount ? "var(--theme-orange)" : "var(--theme-blue)"
224+
case "Between":
225+
return boxCount >= amount && boxCount <= amount2
226+
? "var(--theme-orange)"
227+
: "var(--theme-blue)"
228+
}
229+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
.btn {
2+
border: none;
3+
border-radius: 0.25em;
4+
padding: 0.5em 0.75em;
5+
font-size: inherit;
6+
background: var(--theme-purple);
7+
cursor: pointer;
8+
}
9+
10+
.btn:hover {
11+
background: var(--theme-purple-hover);
12+
}
Lines changed: 116 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,116 @@
1+
---
2+
layout: "@layouts/BlogPost.astro"
3+
title: "CSS Quantity Queries Are Really Cool"
4+
date: "2024-10-21"
5+
description: "Quantity queries are an incredible CSS feature that allows you to style elements based on the number of elements in a container without using any JavaScript at all."
6+
tags: ["CSS"]
7+
---
8+
9+
import CSSQuantityQuery from "@blogComponents/cssQuantityQuery/CSSQuantityQuery"
10+
11+
## Introduction
12+
13+
Quantity queries have been around for over 15 years, yet almost no one knows about them. I had never even heard of them until a few months ago. They are an incredible CSS feature that allows you to style elements based on the number of elements in a container without using any JavaScript at all. This has a ton of different use cases and are surprisingly easy to write once you understand how they work.
14+
15+
_If you prefer to learn visually, check out the video version of this article._
16+
`youtube: yYKX72xjFx8`
17+
18+
## Quantity Queries In Action
19+
20+
Before I start explaining exactly how quantity queries work I think it is best to show you an interactive example of how they work. Just choose the type of query and the number of elements and as long as the condition you set is true the elements will change from blue to orange using only CSS.
21+
22+
_If you want to see the CSS output generated from this form use the [form at the bottom of the page](#calculator)_
23+
24+
<CSSQuantityQuery client:load />
25+
26+
I know it seems crazy but the color of these boxes is determined entirely by CSS.
27+
28+
## Understanding Quantity Queries
29+
30+
Now that you have an idea of what quantity queries can do let's take a look at how to write them. There are three main types of quantity queries that you can write (at least, at most, between), but they all follow the same basic structure.
31+
32+
### At Least Query
33+
34+
Here is what a quantity query for at most 3 elements looks like:
35+
36+
```css
37+
ul li:nth-last-child(n + 3),
38+
ul li:nth-last-child(n + 3) ~ li {
39+
background-color: orange;
40+
}
41+
```
42+
43+
<CSSQuantityQuery client:load hideForm initialQueryType="" initialAmount={3} />
44+
45+
This may look complicated but if we break down each part of this selector it actually becomes quite easy to understand.
46+
47+
- `ul li` - This is the simplest part of the selector since all it does is select all the `li` elements in a `ul`.
48+
- `:nth-last-child(n + 3)` - This selector just selects all the elements in a list except for the last 3. If we change the 3 to a 2 it would select all the elements except for the last 2.
49+
50+
This explains the entire first line of our CSS selector which essentially selects all the elements except for the last 3. The second line of our selector is exactly the same as the first line except it adds `~ li` to the end. This makes it so that all the `li` elements after the ones that were selected in the first line are also selected. Essentially, it selects all the elements except for the first element in the list as long as the list has at least 3 elements.
51+
52+
By combining these two selectors we can select all the elements in a list as long as their are at least 3 elements in the list.
53+
54+
### At Most Query
55+
56+
The at most query is actually quite similar to the at least query. Here is an example for at most 3 elements.
57+
58+
```css
59+
ul li:nth-last-child(-n + 3):first-child,
60+
ul li:nth-last-child(-n + 3):first-child ~ li {
61+
background-color: orange;
62+
}
63+
```
64+
65+
<CSSQuantityQuery client:load hideForm initialQueryType="" initialAmount={3} />
66+
67+
- `ul li` - This is identical to the at least query.
68+
- `:nth-last-child(-n + 3)` - This is the reverse of the at least query and selects only the last 3 elements in a list.
69+
- `:first-child` - This is used to select the first element in the list which when combined with `:nth-last-child(-n + 3)` makes sure it only selects the first element in a list if it also happens to be within the last 3 elements. This is important because without this the selector would select the first element in the list even if there were more than 3 elements in the list.
70+
71+
The first line of our selector selects the first element in the list as long as our list is at most 3 elements long. We then repeat that same selector and add `~ li` to the end to select all the elements after the first element in the list. This makes it so that all the elements in the list are selected as long as their are at most 3 elements in the list.
72+
73+
### Between Query
74+
75+
The between query looks the most complicated but it is simply just taking the at least and at most queries and combining them into one selector.
76+
77+
```css
78+
ul li:nth-last-child(n + 2):nth-last-child(-n + 4):first-child,
79+
ul li:nth-last-child(n + 2):nth-last-child(-n + 4):first-child ~ li {
80+
background-color: orange;
81+
}
82+
```
83+
84+
<CSSQuantityQuery
85+
client:load
86+
hideForm
87+
initialQueryType="Between"
88+
initialAmount={2}
89+
initialAmount2={4}
90+
/>
91+
92+
If you look closely you will notice that the `:nth-last-child(n + 2)` is just an at least 2 query and the `:nth-last-child(-n + 4):first-child` is just an at most 4 query. By combining these two selectors we can select all the elements in a list as long as the list has between 2 and 4 elements.
93+
94+
## Advanced Uses
95+
96+
Up until this point I have shown you how to change the styles of elements in a list based on the number of elements in a list, but oftentimes you will want to change the styles of the container itself based on the number of elements in the list. This is actually quite easy to do and only requires a slight modification to the selectors we have already written.
97+
98+
All you need to do is take the first selector and wrap everything after the `ul` in a `:has` selector. You can then remove the second line of the selector since we only need to select the container once. This new selector will select the container as long as the list meets the quantity query specified.
99+
100+
```css
101+
ul:has(li:nth-last-child(n + 3)) {
102+
background-color: orange;
103+
}
104+
```
105+
106+
The above example will select the `ul` container if there are at least 3 elements in it.
107+
108+
## Calculator
109+
110+
Here is a calculator you can use to generate your own query selectors. The only thing you will need to change is the `ul` and `li` elements since those should be whatever selector makes sense for your code.
111+
112+
<CSSQuantityQuery client:load showQuery />
113+
114+
## Conclusion
115+
116+
Quantity queries are an incredibly powerful CSS feature that can be used to style elements based on the number of elements in a container. They are surprisingly easy to write once you understand how they work and can be used in a variety of different ways. They can also be used with modern CSS to select and style the container based on the number of children in it which is really useful.

src/styles/blog.css

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -354,6 +354,19 @@ kbd {
354354
}
355355
}
356356

357+
pre:not(.shiki) {
358+
line-height: normal;
359+
overflow-x: auto;
360+
background: var(--theme-code-inline-bg);
361+
color: var(--theme-text);
362+
padding: 0.5em;
363+
}
364+
365+
pre:not(.shiki) > code {
366+
border-radius: 0;
367+
padding: 0;
368+
}
369+
357370
:not(.shiki) > code {
358371
padding: 0.1em 0.25em;
359372
text-shadow: none;

0 commit comments

Comments
 (0)