···11+---
22+title: CSS functions and mixins
33+bio: tl;dr and my 2 cents
44+banner: mixins.png
55+pub: 2000-01-01
66+---
77+88+The CSS `@function` rule is now available in chrome 139, with a definition for `@mixin` in the editors draft. (Referenced [First Public Working Draft](https://www.w3.org/TR/2025/WD-css-mixins-1-20250515/), Referenced [Editors Draft](https://drafts.csswg.org/css-mixins/))
99+I wanted an excuse to read into a spec, and this seems quite an interesting one so I thought why not write a post about it! (this is definitely not also because I need something to post when i deploy my site so it doesn't look empty lmao)
1010+1111+> _All ye who travel beyond this point, head this warning_:
1212+> This spec got hands and idk how to shut up so this is a long ish post. Sorry! Also sorry about any syntax highlighting bugs. This is an experimental spec and I'm using nesting too, which might cause visual bugs with my syntax highlighter. This should resolve with time though. I hope.
1313+1414+---
1515+1616+## 1. What's in a function anyway?
1717+1818+CSS has a bunch of functions (`rgb`, `anchor`, `blur`, and `var` to name a few), which are all free of side effects and consistent\*, but are fully defined in the spec, without any way for developers to specify their own functions.
1919+2020+\*CSS random, as discussed in [this webkit post](https://webkit.org/blog/17285/rolling-the-dice-with-css-random/), isn't always consistent (it's a random function after all) however it is 1) not in anything except safari TP so I'm not gonna count it and 2) I Cant Find Any Actual Spec. So. also not gonna count it.
2121+2222+Imagine this css:
2323+2424+```css
2525+h1 {
2626+ font-size: 3em;
2727+2828+ @media (max-width: 1200px) {
2929+ font-size: 2em;
3030+ }
3131+ @media (max-width: 600px) {
3232+ font-size: 1em;
3333+ }
3434+}
3535+3636+img {
3737+ width: 1000px;
3838+3939+ @media (max-width: 1200px) {
4040+ width: 800px;
4141+ }
4242+ @media (max-width: 600px) {
4343+ width: 450px;
4444+ }
4545+}
4646+4747+.screen-mode::after {
4848+ content: "desktop";
4949+5050+ @media (max-width: 1200px) {
5151+ content: "tablet";
5252+ }
5353+ @media (max-width: 600px) {
5454+ content: "mobile";
5555+ }
5656+}
5757+```
5858+5959+While this isn't the most realistic css (the font size probably doesn't need to change, the image could be more fluid with %/vw/vh/etc, and you would basically never need to do that with `::after`), it's useful for demonstration of a potentially useful function.
6060+6161+Below we're going to start defining a function which will take in 1 to 3 parameters, and return values matching the media queries above (ie: if it matches 600px it'll return `--mobile`, falling back to `--tablet` if not defined, and falling back to `--desktop` if thats not defined, etc) It'll make more sense when we define it
6262+6363+```css
6464+@function --media(--desktop, --tablet, --mobile) {
6565+ /* function body here */
6666+}
6767+```
6868+6969+This CSS defines a new function, which we could later use like `width: --media(1000px, 800px, 450px)`, however currently the arguments don't matter, and the result is the "guaranteed-invalid value" (same as if you use something like `var(--not-set)` without a fallback) and so won't work.
7070+7171+For this to be useful, we need a `result`. `result` is a property which can be used within `@function` to define the output of the function. A simple negation function could look something like this:
7272+7373+```css
7474+@function --negate(
7575+ --value type(<length> | <number> | <length-percentage>): initial
7676+) {
7777+ result: calc(-1 * var(--value));
7878+}
7979+```
8080+8181+But wait, I hear you ask, what does all that stuff after `--value` mean???
8282+CSS functions let you define a type and default value for a function. I've used the type definition of `type(<length> | <number> | <length-percentage>)` to tell the browser that if you pass in a value that isn't either a length, number, or length percentage, then it should treat the value as invalid. (note: browser dev tools for this aren't the _best_ right now, but this will come with time)
8383+The `: initial`
8484+8585+Going back to our `--negate` function, usage like `--negate(10px)` returns `-10px`, and `--negate(-5%)` returns `5%`, however something like `--negate(twenty)` wouldn't work, as `twenty` is not of type number, and even if we didn't have the type, `calc(-1 * twenty)` makes no sense, and would be invalid.
8686+8787+So we can see that `result` simply defines the output for the function. This is great for making complex, repeated calculations more legible, and for repeating reused syntax (`result: 10px 10px 5px var(--colour, black)` could be used for a `--shadow` function), but doesn't solve our function, which does media query things. Thankfully, `@function` supports `@media` inside it (and some other @rules)
8888+8989+```css
9090+@function --media(
9191+ --desktop,
9292+ --tablet: var(--desktop),
9393+ --mobile: var(--tablet)
9494+) {
9595+ result: var(--desktop);
9696+9797+ @media (max-width: 1200px) {
9898+ result: var(--tablet);
9999+ }
100100+ @media (max-width: 600px) {
101101+ result: var(--mobile);
102102+ }
103103+}
104104+```
105105+106106+This above code returns desktop, unless the screen is <1200px, in which case it returns tablet, and if the screen is <600px, it returns mobile. If mobile isn't defined, it's the same as tablet, and if tablet isn't defined, its the same as desktop.
107107+108108+We can now replace our original example with an updated `--media()` function!
109109+110110+```css
111111+h1 {
112112+ font-size: --media(3em, 2em, 1em);
113113+}
114114+115115+img {
116116+ width: --media(1000px, 800px, 450px);
117117+}
118118+119119+.screen-mode::after {
120120+ content: --media("desktop", "tablet", "mobile");
121121+}
122122+```
123123+124124+Another thing worth noting about `@function` is that it can access custom properties from the "calling scope":
125125+126126+```css
127127+@function --double--x() {
128128+ result: calc(2 * var(--x));
129129+}
130130+131131+h1 {
132132+ --x: 2em;
133133+ font-size: --double--x();
134134+}
135135+```
136136+137137+Keep in mind that arguments "shadow" the custom properties.
138138+139139+```css
140140+@function --double--x(--x) {
141141+ result: calc(2 * var(--x));
142142+}
143143+144144+h1 {
145145+ --x: 2em;
146146+ /* wont work because --x is not passed in */
147147+ font-size: --double--x();
148148+}
149149+```
150150+151151+If you want to be able to use custom properties when no argument is provided, you can set the default value to "inherit"
152152+153153+```css
154154+@function --double--x(--x: inherit) {
155155+ result: calc(2 * var(--x));
156156+}
157157+158158+h1 {
159159+ --x: 2em;
160160+ /* works because --x is inherited from context */
161161+ font-size: --double--x();
162162+}
163163+```
164164+165165+As you can see; there's quite a lot to functions. If you want more specific and detailed information, or anything not explained here (like how to pass in comma seperated lists or more specific type syntax), your best bet is to reference the spec (and eventually MDN) and just experiment!
166166+167167+## Mixin? Mixin what? Mixin. Now.
168168+169169+(I don't know what the title means and at this point I'm too afraid to ask [my brain])
170170+171171+As mixins are experimental and subject to change (and have no browser support yet) I'm not going to go into masses of detail like I did with `@function`, and instead just hit the highlights.
172172+Also these examples might be stupid I'm not justifying the feature its Fine
173173+174174+### Mixin Arguments
175175+176176+The most basic mixin definition is like this:
177177+178178+```css
179179+@mixin --copy-paste() {
180180+ /* properties go here */
181181+}
182182+```
183183+184184+`@mixin` is defined in basically the same way as `@function`:
185185+186186+```css
187187+@mixin --horrible(
188188+ --background <color>: red,
189189+ --accent <color>: blue,
190190+ --scale <number>: 1
191191+) {
192192+ /* properties go here */
193193+}
194194+```
195195+196196+Unlike `@function`, mixin properties are exposed under `env()`, not `var()`, so theres no risk of conflict and the properties arent injected outside the body. If you want values to be able to be accessed from outsize the mixin, you can still use custom properties.
197197+198198+Mixins can also accept blocks of properties as an argument:
199199+200200+```css
201201+@mixin --mobile(@contents) {
202202+ /* properties go here */
203203+}
204204+```
205205+206206+And can accept both at the same time:
207207+208208+```css
209209+@mixin --weird(--bg: <color>, @contents) {
210210+ /* properties go here */
211211+}
212212+```
213213+214214+### Mixin Application
215215+216216+When no args are passed/needed, you can do this to `@apply` a mixin:
217217+218218+```css
219219+body {
220220+ @apply --copy-paste;
221221+}
222222+```
223223+224224+Passing arguments is basically the same as custom functions:
225225+226226+```css
227227+body {
228228+ @apply --horrible(orange, purple, 2);
229229+}
230230+```
231231+232232+`@content` can be used like this:
233233+234234+```css
235235+body {
236236+ @apply --mobile {
237237+ background-color: red;
238238+ }
239239+}
240240+```
241241+242242+And you can use arguments and `@contents` together like this:
243243+244244+```css
245245+body {
246246+ @apply --weird(red) {
247247+ color: blue;
248248+ }
249249+}
250250+```
251251+252252+### Mixin Bodies
253253+254254+Mixin bodies are **_basically_** inserted wherever `@apply` is used. Just list the properties you want to include inside the mixin and they'll be inserted, including custom properties.
255255+256256+You can access the arguments to the function via a dashed indent in `env()`:
257257+258258+```css
259259+@mixin --horrible(
260260+ --background <color>: red,
261261+ --accent <color>: blue,
262262+ --scale <number>: 1
263263+) {
264264+ background-color: env(--background);
265265+ color: env(--accent);
266266+ border: calc(5px * env(--scale)) groove env(--accent);
267267+ border-radius: 50%;
268268+ float: right;
269269+ &:hover {
270270+ background-color: env(--accent);
271271+ color: env(--background);
272272+ border-color: env(--background);
273273+ }
274274+}
275275+```
276276+277277+You can insert whatever properties were passed into a mixin using the `@contents` at rule:
278278+279279+```css
280280+@mixin --mobile(@contents) {
281281+ @media (max-width: 600px) {
282282+ @contents;
283283+ }
284284+}
285285+```
286286+287287+You can also provide a default value for `@contents`:
288288+289289+```css
290290+@mixin --mobile(@contents) {
291291+ @media (max-width: 600px) {
292292+ @contents {
293293+ &::before,
294294+ &::after {
295295+ display: block;
296296+ background-color: red;
297297+ color: black;
298298+ contents: "Warning: Unused `@apply --mobile`. Include a declaration list";
299299+ }
300300+ }
301301+ }
302302+}
303303+```
304304+305305+If no declaration list is passed to the mixin, the fallback will be used instead.
306306+307307+In a mixin, if you use custom properties, they are inserted along with other properties. This is useful if you want values to be accessible to the user (ie: exposing a colour derived from input for use elsewhere), but also pollutes the namespace. As such, you should avoid using custom properties when you don't explicitly want the user to read/override things. As such, use `@env` to define environment variables scoped to the mixin body.
308308+309309+```css
310310+@mixin --mix-colour(--col1, --col2) {
311311+ @env --mix: color-mix(in oklab, env(--col1), env(--col2));
312312+ color: env(--mix);
313313+}
314314+```
315315+316316+In this example `env(--mix)` is only avaliable inside the `--mix-colour` body, and doesnt pollute the calling context.
317317+318318+Thats about the gist on mixins, its very new and experimental and has 0 implementation so go read the spec yourself lol
319319+320320+## Listen to me speak now please thank you
321321+322322+First note: I think these are really good so far, maybe some slight tweaks needed for mixins. I will say that with mixins I'm not the BIGGEST fan of using `env()`, but if I remember correctly it originally started with an `@result` at rule or something like that, so I'm pretty sure theres a reason we got an `@env` etc instead. That said I dont really mind `env()` to much so its Fine lmao.
323323+324324+I also think its worth considering named `@contents` blocks instead, maybe something like this:
325325+326326+```css
327327+@mixin --media(--desktop @contents, --mobile @contents) {
328328+ @media not (max-width: 600px) {
329329+ @contents --desktop {
330330+ desktop-fallback: here;
331331+ }
332332+ }
333333+ @media (max-width: 600px) {
334334+ @contents --mobile {
335335+ mobile-fallback: here;
336336+ }
337337+ }
338338+}
339339+340340+body {
341341+ @apply --media() {
342342+ desktop-properties: here;
343343+ } {
344344+ mobile-properties: here;
345345+ }
346346+ }
347347+}
348348+```
349349+350350+The current syntax of having a single trailing `@contents` and single trailing `{/*properties*/}` for cases where only one `@contents` is needed. This might need some tweaking for legibility etc, but some way to add multiple `@contents` blocks would be nice. I agree with the enforcing of them being at the end of the argument list though.
351351+352352+Another suggestion, which may be defered to level 2: expose functions to CSS houdini.
353353+354354+For example, something like this:
355355+356356+```js
357357+// index.js
358358+CSS.functions.addModule("css-arrays.js");
359359+```
360360+361361+```js
362362+// css-arrays.js
363363+registerFunction(
364364+ "--get",
365365+ class {
366366+ static get arguments() {
367367+ return [
368368+ {
369369+ type: "<string>#",
370370+ },
371371+ {
372372+ type: "<number>",
373373+ default: 0,
374374+ },
375375+ ];
376376+ }
377377+378378+ eval(array, index) {
379379+ return array[index];
380380+ }
381381+ },
382382+);
383383+```
384384+385385+`css-arrays.js` would run in a custom Worklet environment, most likely a plain ECMAScript environment with no APIs, which may be created and destroyed on each call. It wouldn't open the door to advanced functionality like `--anchor`, but could make "simpler" functions ponyfillable, like arrays/objects, or even things like `color-mix`.
386386+387387+This is NOT a formal proposal, just a suggestion. Might make an issue in the repo later if I can be bothered.
388388+389389+---
390390+391391+Anyway, to conclude: I'm extremely happy with the reccomended API right now, and I would be happy to see this or something very similar land in browsers. Also let us do functions with JS it would be v good for ponyfilling.