w3c / css-houdini-drafts

Mirror of https://hg.css-houdini.org/drafts
https://drafts.css-houdini.org/
Other
1.84k stars 141 forks source link

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

Open trusktr opened 4 years ago

trusktr commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

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 commented 4 years ago

Oh that thread looks beautiful! Thanks @bkardell

jimmyfrasche commented 3 years ago

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 commented 3 years ago

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 commented 1 year ago

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:

nuxodin commented 6 months ago

Noticed this overlaps with Issue https://github.com/w3c/csswg-drafts/issues/9350. Might be worth consolidating the discussion there.

oknoorap commented 6 months ago

@matthew-dean I agree use would be more DX friendly πŸ‘

mirisuzanne commented 6 months ago

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 commented 1 day ago

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

oknoorap commented 1 day ago

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 commented 22 hours ago

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