Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[css-custom-functions] user-defined custom CSS functions #1007

Open
trusktr opened this issue Aug 20, 2020 · 13 comments
Open

[css-custom-functions] user-defined custom CSS functions #1007

trusktr opened this issue Aug 20, 2020 · 13 comments

Comments

@trusktr
Copy link

trusktr commented Aug 20, 2020

I wish there was a way to make re-usable CSS "functions".

Currently, we can't make re-usable logic without unfortunately coupling to HTML markup. See:

As you can tell from those pages, in order to make re-usable expressions ("functions"), we must couple the re-usability to the HTML markup, which is not ideal.

It would be great to be able to create re-usable logic/functions in a way that does not require a CSS author to ever touch any HTML code.

How might an proposal syntax look like, to work around the current limitation?

Perhaps:

:root {
  --sum: function( calc( var(--a) + var(--b) ) )
}

/* Then anywhere in any CSS rules, without dependence on HTML markup: */
.foo.bar.whatever {
  --a: 4px; --b: 6px;
  font-size: call(--sum);
}

^ In that example, the user does not have to worry about "evaluating CSS variables at the level where they are needed" (which requires the CSS author to touch the HTML markup as shown by @Afif13's nicely-written answer in the second SO question).

There's probably a bunch of other ways we could do it.

Here's another idea for sake of spinning up some ideas:

:root {
  --sum: function(--a, --b, calc( arg(--a) + arg(--b) ) )
}

/* Then anywhere in any CSS rules, without dependence on HTML markup: */
.foo.bar.whatever {
  font-size: call(--sum, 4px, 6px);
}

Or, maybe we can create a new special type of block (it may be cleaner):

@function sum(a, b) {
  return: calc(arg(a) + arg(b))
}

/* Then anywhere in any CSS rules, without dependence on HTML markup: */
.foo.bar.whatever {
  font-size: sum(4px, 6px);
}

/* Maybe `call()` or similar is required to distinguish from builtins? */
.foo.bar.whatever {
  font-size: call(sum, 4px, 6px);
}

Maybe functions can also return a rule expression, which could be "mixed" onto a rule (not sure about syntax though):

@function foo(a, b, c) {
  return {
    background: a b c;
  }
}

.whatever {
  call(foo, lightblue, url("foo.gif"), no-repeat); /* How to handle commas? */
  color: red;
}

results in equivalent of

.whatever {
  background: lightblue url("foo.gif") no-repeat;
  color: red;
}

or something.

With @function, maybe the return just has to match the context where the function is being used. For example in the previous two examples we saw it used to return a property value, and to return a rule that is mixed in the rule where it is used.

Maybe we could also use it at the top level to create top-level rules (not sure about the particular syntax, probably someone with CSS implementation expertise can weigh in):

@function makeKeyframes(name, a, b, c) {
  @keyframes arg(name) { ... something here ... what are a, b and c? ... }
}
@SebastianZ
Copy link

As a programmer, I slightly prefer the at-rule syntax, which looks similar to different programming languages. It should also be kept in mind to be able to define a worklet for extended functionality.

I also like the idea of realizing mixins with that syntax, though I wonder whether functions and mixins should be mixed (pun intended 😄). I mean, whether it should be allowed to let a function return property values or several property declarations. I'd rather expect to have two different syntaxes for that, e.g. an @mixin rule.

Sebastian

@tomhodgins
Copy link

I think it would be great if there was a way for authors to wire up custom functions JavaScript knows about to CSS using a custom <dashed-ident> as the function name.

Suppose you have a function like this somewhere in JS:

function pickRandom(array = []) {
  return array[Math.floor(Math.random() * array.length]
}

You would need some way to register this with CSS to be used with a certain name:

CSS.registerCustomFunction({
  name: '--pick-random',
  syntax: '<any>#', // space separated tokens
  definition: pickRandom,
  events: ['load']
})

And then be able to use it anywhere in a CSS value as --pick-random():

:root {
  background-color: --pick-random(red green blue);
}

Being able to expose existing JavaScript functions to CSS (including all of the colour conversion and animation libs out there) would bring a lot of value because so many of the most useful functions might exist already and be immediately usable from new places. Another exciting possibility is allowing people to integrate some of the styling integrations currently only possible CSS-in-JS to be used from regular CSS stylesheets as well.

I think this would be true whether or not there was a way to declaratively define them from CSS syntax (which I also think is fun). As a side note, I am not proposing anything about defining them from CSS but an at-rule seems to make sense for something like that. Just for fun, here's a codepen I made recently exploring defining JS functions in CSS syntax to be used from CSS as custom functions: https://codepen.io/tomhodgins/pen/oNbBYZw

I'd love to see anything like this, even browsers parsing and hanging onto --custom() functions so we can try to support them client-side. I've noticed browsers today will hang onto --custom-functions() in two situations:

a {
  --works-inside-in-a-custom-property: --custom();
}

Inside the value of a custom property basically everything that's valid CSS parsed and kept, so any --custom() function usage is safe to use in-browsers here.

a {
  width: var(--works-in-any-property-value-that-includes-var-too) --custom();
}

It also seems like browsers parse and hang onto custom functions inside property values that include var() as well, so as long as var() is somewhere in the property value it seems like you can --something-like-this() in browsers today (it won't show up in the property value, but can be found in that CSS rule's cssText)

I'm all for anything like any of the above! 😎

@bkardell
Copy link

As I mentioned in the previous issue, #857 is related - unfortunately that linked to 857 in CSS instead of Houdini, sorry if it caused issues - this link should work though.

@tomhodgins
Copy link

Oh that thread looks beautiful! Thanks @bkardell

@jimmyfrasche
Copy link

Could regular custom property declaration/semantics be extended to take parameters? Something like

:root {
  --fn(--arg1, --arg2): calc(var(--arg1) + var(--arg2));
  --stroke(--c): 1px 1px var(--c), -1px 1px var(--c), 1px -1px var(--c), -1px -1px var(--c);
}
span.shadow {
  text-shadow: var(--stroke(black)); /*or call(--stroke, black) */
}

@oknoorap
Copy link

My proposal:

CSS.registerFunction({
  name: 'fnName',
  call: jsFunction,
  arguments: ["<color>", "<url>"]
})

function jsFunction(color, url) {
}
body {
   background: call("fnName", #000, url("https://imageurl"));
}

@matthew-dean
Copy link

matthew-dean commented May 7, 2023

I think for better performance, you'd need something more like the paint worklet, which can operate earlier in the render cycle. Also, passing to / from JavaScript requires you to define types in terms of the CSS OM. Otherwise, you can't do something like animate values, because the type returned from JavaScript doesn't conform to a CSS type. Which is why I'm proposing Value Transform worklets.

@oknoorap I think you got quite close, except:

  • Similar to the Paint Worklet, there's no need to put the function name in quotes. You can use plain identifiers as values in CSS.
  • You need to specify the return type as a CSS OM type; otherwise CSS doesn't really understand the nature of the value, and what can be done with it.
  • I considered call, but that's very imperative, and use felt to me like a more declarative form. Partly because the function may be "called" a number of times, not just at first-evaluation time. It's a computed value.

@nuxodin
Copy link

nuxodin commented Mar 14, 2024

Noticed this overlaps with Issue w3c/csswg-drafts#9350.
Might be worth consolidating the discussion there.

@oknoorap
Copy link

@matthew-dean I agree use would be more DX friendly 👍

@mirisuzanne
Copy link

Noticed this overlaps with Issue csswg-drafts#9350. Might be worth consolidating the discussion there.

I think it would be useful to approach this from both ends. Ideally CSS could provide a declarative syntax for the simpler use-cases, and we could also work on a Houdini syntax for more imperative extensions as needed.

@Secret-chest
Copy link

I think we can simply use --name() like with variables so we don't need call.

@oknoorap
Copy link

oknoorap commented Sep 19, 2024

alternatively, since the @property rule (https://developer.mozilla.org/en-US/docs/Web/CSS/@property) is available in modern browsers, how about using similar approaches?

@function fn-name {
  use: "awesomeFunction";
  /** alternative
  call: "awesomeFunction";
  invoke: "awesomeFunction";
  syntax: "awesomeFunction";
  source: "awesomeFunction"; */
  arguments: [<number> | <percentage>]?
}

body {
  width: fn-name(10);
}

@mirisuzanne
Copy link

Note that the @function rule is already being defined for declarative custom functions: https://drafts.csswg.org/css-mixins/#function-rule

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

10 participants