Browse Source

Links

master
David Larlet 1 month ago
parent
commit
b683483884
Signed by: David Larlet <david@larlet.fr> GPG Key ID: 3E2953A359E7E7BD

+ 716
- 0
cache/2024/0cc2e9c6b29f8326b2ff628f64e22888/index.html View File

@@ -0,0 +1,716 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="en">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>Proposal: CSS Variable Groups (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Is that even respected? Retrospectively? What a shAItshow…
https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
<meta name="robots" content="noai, noimageai">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://lea.verou.me/docs/var-groups/">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>Proposal: CSS Variable Groups</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://lea.verou.me/docs/var-groups/" title="Lien vers le contenu original">Source originale</a>
<br>
Mis en cache le 2024-02-27
</p>
</nav>
<hr>
<div class=nutshell>
<p>CSS Variable Groups is a way to define multiple properties under the same namespace and pass the entire group around,
addressing several pain points around design tokens, design systems, and integrating third-party components.</p>
</div>
<h2 id="pain-points" tabindex="-1"><a class="header-anchor" href="#pain-points">Pain points</a></h2>
<h3 id="background" tabindex="-1"><a class="header-anchor" href="#background">Background</a></h3>
<p>Design tokens and design systems are about a lot more than color, but I’ll focus on color here, as that is the worst of it and also easier to explain.</p>
<p>The color part of most design systems consists of the following:</p>
<ul>
<li>Core hues: red, yellow, green, blue, etc. These are hues specifically picked by designers, not to be confused with the corresponding named colors.</li>
<li>Neutrals / Grays (often more than one)</li>
</ul>
<p>Each of the above is typically defined as a main color plus tints/shades under a number, with larger numbers corresponding to darker colors.
There is no commonly agreed naming convention wrt the numbering scheme used.</p>
<p>Some popular examples of such design systems:</p>
<table>
<thead>
<tr>
<th>Design system</th>
<th>Hues</th>
<th>Neutrals</th>
<th>Levels</th>
<th>Range</th>
<th>Increment</th>
<th>Extras</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://yeun.github.io/open-color/">Open color</a></td>
<td>12</td>
<td>1</td>
<td>10</td>
<td>0 - 9</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://open-props.style/#colors">Open props</a></td>
<td>17</td>
<td>2</td>
<td>13</td>
<td>0 - 12</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://tailwindcss.com/docs/customizing-colors">Tailwind</a></td>
<td>17</td>
<td>5</td>
<td>11</td>
<td>50 - 950</td>
<td>100</td>
<td>50, 950</td>
</tr>
<tr>
<td><a href="https://m2.material.io/design/color/the-color-system.html#color-theme-creation">Material</a></td>
<td>17</td>
<td>2</td>
<td>10</td>
<td>50-900</td>
<td>100</td>
<td>50</td>
</tr>
<tr>
<td><a href="https://spectrum.adobe.com/page/color-palette/">Adobe Spectrum</a></td>
<td>13</td>
<td>1</td>
<td>13</td>
<td>100 - 1300</td>
<td>100</td>
<td>Gray 50, Gray 75</td>
</tr>
<tr>
<td><a href="https://primer.style/foundations/color/base-scales">GitHub Primer</a></td>
<td>8</td>
<td>1</td>
<td>10</td>
<td>0 - 9</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://ant.design/docs/spec/colors">Ant Design</a></td>
<td>12</td>
<td>1</td>
<td>10 (13 for grays)</td>
<td>1 - 10</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://www.ibm.com/design/language/color/">IBM Design Language</a></td>
<td>7</td>
<td>3</td>
<td>10</td>
<td>10 - 100</td>
<td>10</td>
<td></td>
</tr>
<tr>
<td><a href="https://www.radix-ui.com/themes/docs/theme/color">Radix UI</a></td>
<td>25</td>
<td>6</td>
<td>12</td>
<td>1 - 12</td>
<td>1</td>
<td></td>
</tr>
</tbody>
</table>
<p>In terms of CSS variables, this translates to variables like e.g. <code>--color-red-600</code>, <code>--color-gray-10</code> etc on <code>:root</code>. A LOT of them.</p>
<h3 id="summary-of-pain-points-%26-requirements" tabindex="-1"><a class="header-anchor" href="#summary-of-pain-points-%26-requirements">Summary of pain points &amp; requirements</a></h3>
<p>Distilling these pain points into their essence, it looks like the actual pain points are:</p>
<ol>
<li><strong>Aliasing</strong>: Aliasing a set of variables with a common prefix to a different prefix is very commonly needed, and requires <em>a lot</em> of CSS.
Even when using a build tool to automate this, the size of the resulting CSS is huge, and it is hard to debug as it clogs up the devtools.</li>
<li>Defining these tokens requires manually defining every single one, even when it could be computed via interpolation.</li>
<li>Getting arbitrary tokens (e.g. through a calculation) is impossible.</li>
</ol>
<p>Any solution would need to meet the following requirements:</p>
<ol>
<li>Subtree scoped: It needs to be possible to alias a set of variables to a different prefix on a subtree, so <code>@property</code> and any new tree-scoped @-rules are out.</li>
<li>Pave the cowpaths: Assuming a web app involves three classes of users (page authors, design system authors, web component authors)
the syntax should not require opt-in from all parties at once.
Individual classes of users should be able to derive value without all other parties having to change anything.</li>
<li>It should not require more than one declaration to style a single design aspect (e.g. the primary color) of a subtree or web component.</li>
</ol>
<h4 id="biggest-pain-point%3A-aliasing" tabindex="-1"><a class="header-anchor" href="#biggest-pain-point%3A-aliasing">Biggest pain point: Aliasing</a></h4>
<p>Then, to be used in the UI, the colors are also assigned <em>semantic</em> meaning: brand color, primary (or accent) color, secondary color, success, danger, etc. Pure hues can still be used directly for certain cases.</p>
<p>Assigning e.g.</p>
<pre><code class="language-css">--color-primary: var(--color-blue);
</code></pre>
<p>Does <em>not</em> also automatically give you <code>--color-primary-10</code>, <code>--color-primary-20</code> and so on. <strong>You have to painfully define them yourself</strong>:</p>
<pre><code class="language-css">--color-primary-10: var(--color-blue-10);
--color-primary-20: var(--color-blue-20);
/* ... */
--color-primary-100: var(--color-blue-100);
</code></pre>
<p>And this is for <em>all</em> your semantic colors. We’re talking about <em>a lot</em> of variable declarations.
And often specific tints are aliased further, e.g. text, border, background etc.
Often, all of this must be repeated for dark mode, high contrast mode, etc.</p>
<p>But let’s assume we dutifully do all this. Now let’s suppose we want to theme a certain part of the page with a different primary color. E.g. maybe we have callouts and we want to style notes (with shades of) green, tips yellow, warnings red, etc. We <em>could</em> have a <code>--color</code> or <code>--color-primary</code> property for the callout component, but that does not give us any of its variations, no, we need to define those manually <em>again</em>.</p>
<p>This also means that integrating third-party components is painful, because every single color variation they may need needs to be passed on individually. Most components <a href="https://shoelace.style/tokens/color#primitives">bundle an entire design system</a> and authors are expected to override every single property to integrate it with their own.</p>
<p>Note that this applies regardless of whether the tints and shades are precomputed (as is the case with most design systems today) or dynamically computed from color manipulation functions (our bright future?).</p>
<p>This is also a problem when adopting external libraries and design systems.
Currently, most libaries, design systems, icon libraries etc. have to use lengthy namespacing to avoid conflicts.
E.g. <a href="https://spectrum.adobe.com/page/color-palette/">Adobe Spectrum</a> prefixes each color with <code>--spectrum-global-color-</code> (e.g. <code>--spectrum-global-color-celery-100</code>).
They also often use color names that the author may want to remap to simpler names.
The effort needed for an author to remap all of these to more reasonable names is non-trivial (Radix UI even has <a href="https://www.radix-ui.com/themes/docs/theme/color#aliasing-colors">a section on this</a> — note that this is <em>just</em> for one color!).</p>
<h4 id="pain-point-2%3A-repetitiveness-and-verbosity" tabindex="-1"><a class="header-anchor" href="#pain-point-2%3A-repetitiveness-and-verbosity">Pain point 2: Repetitiveness and verbosity</a></h4>
<p>First, it is important to note that aesthetically pleasing color palettes are not completely perceptually uniform.
Chroma and hue often get skewed as you move towards the lightness edges, and they are skewed in different ways depending on the hue.
As the most obvious example, look at how yellows become orange as they darken in both of these but even more so in OC:</p>
<figure>
<img width="789" alt="image" src="images/yellow-tailwind.png">
<figcaption>Tailwind’s yellow palette</figcaption>
</figure>
<figure>
<img width="1527" alt="image" src="images/yellow-oc.png">
<figcaption>Open Color’s yellow palette</figcaption>
</figure>
<p><em>That said</em>, while we could not generate all tints through interpolation,
interpolation could approximate at least <em>some of them</em>.
But right now, the best we can do is something like this:</p>
<pre><code class="language-css">--color-green-200: color-mix(in oklch, var(--color-green-100) 20%, var(--color-green-500));
--color-green-300: color-mix(in oklch, var(--color-green-100) 40%, var(--color-green-500));
/* ... */
</code></pre>
<p>Now suppose we don’t like the interpolated <code>--color-green-300</code> and want to tweak it.
We’d <em>also</em> need to tweak <code>--color-green-200</code> if we want it to use that!</p>
<h4 id="pain-point-3%3A-cannot-reference-tokens-programmatically" tabindex="-1"><a class="header-anchor" href="#pain-point-3%3A-cannot-reference-tokens-programmatically">Pain point 3: Cannot reference tokens programmatically</a></h4>
<p>Since these are variables and variable names cannot be composed dynamically,
there is no way to transform a number (e.g. <code>200</code>) or a keyword (e.g. <code>red</code>) to a color token,
which could have allowed components to abstract away the specifics of the design system.</p>
<h2 id="proposal" tabindex="-1"><a class="header-anchor" href="#proposal">The Proposal: CSS Variable Groups</a></h2>
<p>The underlying pain point here is that authors need to be able to map <em>a set</em> of CSS variables to a different name, reactively.
What if we allowed them to do <em>just that</em>?</p>
<h3 id="defining-using" tabindex="-1"><a class="header-anchor" href="#defining-using">Defining and using a variable group</a></h3>
<p>This proposal allows authors to define groups of variables with the same prefix, by using braces
and then pass the whole group around to other variables:</p>
<pre><code class="language-css">--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
</code></pre>
<p>Then this is equivalent to creating <code>--color-green-100</code>, <code>--color-green-200</code>, etc. variables.
But with one difference. When doing:</p>
<pre><code class="language-css">--color-primary: var(--color-green);
</code></pre>
<p>This is passing a structured object behind the scenes so you <em>automatically</em> get <code>--color-primary-100</code>, <code>--color-primary-200</code> etc.</p>
<p>This allows patterns like:</p>
<pre><code class="language-css">/* Author CSS */
:root {
--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
}

some-component,
.callout-note {
--color-primary: var(--color-green);
}

.callout-note {
background: var(--color-primary-200);
}

/* some-component.css */
:host {
background: var(--color-primary-100);
border: var(--color-primary-400);
color: var(--color-primary-900);
}
</code></pre>
<div class="note">
<p>We may need to start with a hyphen (or two), because <a href="https://drafts.csswg.org/css-syntax/#ident-token-diagram">the <code>&lt;ident&gt;</code> production does not allow starting with a number</a>:</p>
<pre><code class="language-css">--color-green: {
-100: oklch(...);
-200: oklch(...);
/* ... */
}
</code></pre>
<p>But in the rest of this I’m gonna assume that Tab can come up with some ingenious solution to allow us to have the nicer syntax. 🙂
OTOH if these have a prefix, it means there are no naming conflicts with any predefined ones (<code>base</code>, <code>default</code>, etc.)</p>
</div>
<div class=note>
<p>Do we need a way to reference internal properties without having to use their full name (akin to JS <code>this</code>)?</p>
</div>
<p>The group inherits like a regular value, though if descendants define e.g. <code>--color-green-200</code>,
that would override the group value for that particular key.</p>
<h4 id="defining-or-overriding-tints-outside-the-group" tabindex="-1"><a class="header-anchor" href="#defining-or-overriding-tints-outside-the-group">Defining or overriding tints <em>outside</em> the group</a></h4>
<p>Note that once a variable is defined a group, ANY variable with that prefix on the same element becomes part of the group and is passed around or inherited down.
This means that this should work:</p>
<pre><code class="language-css">:root {
--color-green: {
100: oklch(95% 13% 135);
900: oklch(25% 20% 135);
}
}

html {
--color-green-200: oklch(95% 15% 135);
}

my-component {
--color-primary: var(--color-green);
background: var(--color-primary-200);
}
</code></pre>
<p>Or even this:</p>
<pre><code class="language-css">/* style.css */
:root {
--color-green: {};
}

my-component {
--color-primary: var(--color-green);
background: var(--color-primary-200);
}

/* design-system.css */
:root {
--color-green-100: oklch(95% 13% 135);
--color-green-200: oklch(95% 15% 135);
/* ... */
--color-green-900: oklch(25% 20% 135);
}
</code></pre>
<p>Which provides a way to “upgrade” existing design systems to using this new syntax without any changes to their code.</p>
<h3 id="using-groups-on-non-custom-properties%3A-the-base-property" tabindex="-1"><a class="header-anchor" href="#using-groups-on-non-custom-properties%3A-the-base-property">Using groups on non-custom properties: The <code>base</code> property</a></h3>
<p>When referencing a variable whose value is a group (e.g. <code>var(--color-green)</code> above) on a non-custom property,
by default it would either IACVT or resolve to <code>&lt;empty-token&gt;</code> (not sure which one is best here).</p>
<p>However, you can set a base value via the special <code>base</code> property (alternative names: <code>default</code>, <code>value</code>) which defines a plain value for when the property is used in a context that does not support groups, such as any of the existing non-custom properties:</p>
<pre><code class="language-css">--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};

.note {
/* Same as border: 1px solid oklch(65% 50% 135)); */
border: 1px solid var(--color-green);
}
</code></pre>
<aside>
In the future we may want to introduce native properties that accept groups.
Another possibility is to allow groups to be used as a shorthand’s whole value.
</aside>
<h3 id="nested-variable-groups" tabindex="-1"><a class="header-anchor" href="#nested-variable-groups">Nested variable groups</a></h3>
<p>Variable groups can be infinitely nested, which allows a single variable to hold an entire color palette:</p>
<pre><code class="language-css">--color: {
green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
red: {
base: oklch(55% 55% 30);
100: oklch(95% 13% 30)
}
}
</code></pre>
<p>This color palette could be passed on to a component in one go:</p>
<pre><code class="language-css">--component-palette: var(--color);
</code></pre>
<p>The values are not limited to colors and the keys are not limited to numbers:</p>
<pre><code class="language-css">--font: {
serif: Vollkorn;
sans: Inter;
mono: Inconsolata;
};
</code></pre>
<p>In fact, together with nesting, one could imagine passing <em>entire</em> design systems around with a single variable reference!</p>
<pre><code class="language-css">/* primer.css */
--primer: {
color: {
/* ... */
},
font: {
/* ... */
},
/* ... */
};

/* author css */
--design: var(--primer);
</code></pre>
<p>With compatible naming schemes, authors could even compose their design system by mixing and matching from existing design systems.
E.g. using the Primer design system, with the Open Color color palette:</p>
<pre><code class="language-css">/* primer.css */
--primer: {
color: {/* ... */};
font: {/* ... */};
size: {/* ... */};
/* ... */
}

/* open-color.css */
--oc: {
red: { /* ... */ }
}

/* author css */
--design: var(--primer);
--design-color: var(--oc);
</code></pre>
<div class=note>
<p>Note that as currently defined, the pattern above would <em>override</em> Primer’s colors with Open Color’s,
so <code>--primer-color</code> would <em>also</em> resolve to <code>var(--oc)</code>.
To avoid this, authors could alias each top-level group separately:</p>
<pre><code class="language-css">--design: {
color: var(--oc);
font: var(--primer-font);
size: var(--primer-size);
/* ... */
}
</code></pre>
</div>
<h3 id="tweaking-the-default-value-without-destroying-the-group" tabindex="-1"><a class="header-anchor" href="#tweaking-the-default-value-without-destroying-the-group">Tweaking the default value without destroying the group</a></h3>
<p>To minimize surprise, things like this:</p>
<pre><code class="language-css">/* Make core green a little yellower */
--color-green: oklch(65% 50% 130);
</code></pre>
<p>Would need to override the whole variable value, meaning <code>--color-green-100</code> is now undefined (<code>initial</code>).
This is also consistent with how shorthands work.
Also, if we kept the subproperties intact when overriding the base color, they would get out of sync, which is worse.</p>
<p>But this begs the question: then how do we override <em>just</em> the default value?
For example, to tweak the base color and maintain dynamic tints generated from it.</p>
<p>One way would be to expose special properties like <code>base</code> like regular custom properties that can be overridden. Then you could just do that:</p>
<pre><code class="language-css">/* Make core green a little yellower */
--color-green-base: oklch(65% 50% 130);
</code></pre>
<p>And as a bonus, this facilitates debugging, and allows customizing more than just the default value.</p>
<h2 id="continuous" tabindex="-1"><a class="header-anchor" href="#continuous">Facilitating continuous variations</a></h2>
<p>So far, this proposal has been about facilitating the use of predefined static tokens.
But what if we could support <em>dynamic</em> variations, where only a few key values are defined, and the rest are interpolated within them?</p>
<div class=note>
This is the least fleshed out part of this proposal, but I think it could be a very powerful feature (possibly not MVP though).
</div>
<h3 id="the-default-property" tabindex="-1"><a class="header-anchor" href="#the-default-property">The <code>default</code> property</a></h3>
<p>I’m thinking of a <code>default</code> special property (other potential names: <code>any</code>, <code>other</code>, <code>else</code>, <code>*</code> (if possible)), that would be a catch-all for any undefined value.
The key would be passed to the expression as a predefined keyword (e.g. <code>arg</code>), but that can be customized.</p>
<pre><code class="language-css">--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
default: color-mix(in oklch, var(--color-green-100) calc((100 - arg / 10) * 1%), var(--color-green-900));
900: oklch(25% 20% 135);
};
</code></pre>
<p>It could even be a shorthand, with <code>default-value</code> and <code>default-type</code> to specify the return type.</p>
<p>Customizing the arg name (both for readabiity and to facilitate nested use cases):</p>
<pre><code class="language-css">--color-green: {
base: oklch(...);
100: oklch();
name: tint;
default: color-mix(in oklch, var(--color-green-100) calc((100 - tint / 10) * 1%), var(--color-green-900));
900: oklch();
};
</code></pre>
<h3 id="piecewise-interpolation" tabindex="-1"><a class="header-anchor" href="#piecewise-interpolation">Piecewise interpolation</a></h3>
<p>The previous example always calculates mid points from the ends of the spectrum. However, it would serve use cases far better to be able to set spot colors to course correct and have the intermediate tints compute from them, similar to how gradient color stops work.</p>
<p>Perhaps numerical keys could be auto-detected and the closest min and max keys and values could be made available to the expression as keywords.
Potential names: <code>min</code> and <code>max</code> for values, <code>min-key</code>, <code>max-key</code> for the keys.
Example (see <a href="https://drafts.csswg.org/css-values-5/#progress-func"><code>progress()</code></a>):</p>
<pre><code class="language-css">--color-green: {
base: oklch(...);
100: oklch();
default: color-mix(in oklch, min calc(progress(arg from min-key to max-key) * 100%), max);
900: oklch();
};
</code></pre>
<p>These would only be available when <code>arg</code> is numerical AND there are other numerical keys defined.
There could be a max without a min and vice versa if only larger or only smaller numerical keys are available.</p>
<p>It could even be specified with <em>just</em> <code>default</code>:</p>
<pre><code class="language-css">--gray: {
default: color-mix(in oklab, white calc(1% * arg), black);
}
</code></pre>
<h3 id="issues" tabindex="-1"><a class="header-anchor" href="#issues">Issues</a></h3>
<p>One issue is that while defining tokens via interpolation can be convenient, design system authors often do not want to expose the entire spectrum,
but only a few carefully chosen tokens.
So even if we allow the token values to be specified via a formula, we may need to introduce a way to optionally limit the keys that are exposed.
Potential solutions:</p>
<ul>
<li>A <code>default-keys</code> property (potentially a shorthand) that defines the min/max/step for the keys that are exposed.</li>
<li>A way to list specific keys, rather than a catch-all <code>default</code></li>
</ul>
<h2 id="functional-syntax" tabindex="-1"><a class="header-anchor" href="#functional-syntax">Getting group properties dynamically</a></h2>
<p>Currently, we can only access properties via static offsets, even when dynamic variations are allowed.
If we automatically exposed a functional syntax for every group, we could select the right token on the fly, possibly as a result of calculations.
Nested groups would simply involve more than one argument.</p>
<p>This would also allow mapping design tokens to a different naming scheme and reducing verbosity.
E.g. suppose we have <code>--spectrum-global-color-celery-100</code> to <code>--spectrum-global-color-celery-1300</code> and we want to map them to <code>--color-green-1</code> to <code>--color-green-13</code>,
i.e. not just a different prefix, but also a different scale:</p>
<pre><code class="language-css">/* Turn Spectrum colors into a group */
--spectrum-global-color: {};
--spectrum-global-color-celery: {};

--color-green: {
default: --spectrum-global-color-celery(calc(arg / 100));
}
</code></pre>
<p>One downside to simply making these functions is that they could potentially clash with custom functions.
Roma Komarov <a href="https://github.com/w3c/csswg-drafts/issues/9992#issuecomment-1962321439">proposed</a> a separate <code>get()</code> function that would take the prefix as its first argument.
I quite like this, and it means it can ship separately, as it can be based on property naming, not groups.
This also means it does not require converting anything into groups.
So the example above would be way simpler:</p>
<pre><code class="language-css">--color-green: {
default: get(--spectrum-global-color-celery, calc(arg / 100));
}
</code></pre>
<h2 id="decomposed-alternative" tabindex="-1"><a class="header-anchor" href="#decomposed-alternative">Alternative decomposed design</a></h2>
<p>We could decouple this into three separate features.
This is likely easier to implement as a whole, but also these features can ship independently and add value on their own.
However, it also makes it less ambitious, as it becomes harder to add some of the more advanced features (e.g. continuous variations).</p>
<h3 id="a-function-to-map-css-variables-with-a-common-prefix-to-a-different-prefix" tabindex="-1"><a class="header-anchor" href="#a-function-to-map-css-variables-with-a-common-prefix-to-a-different-prefix">A function to map CSS variables with a common prefix to a different prefix</a></h3>
<p>A <code>var()</code>-like function (e.g. <code>vars()</code>, <code>group()</code>) for mapping many variables with a common prefix to a different prefix.</p>
<pre><code class="language-css">--color-primary: group(--color-green);
</code></pre>
<p>Maybe even <code>var()</code> itself, where we’d distinguish between the two because the var reference would include an asterisk:</p>
<pre><code class="language-css">--color-primary: var(--color-green-*);
</code></pre>
<p>The downside of this is that it’s unclear whether that also sets <code>--color-primary</code> to <code>var(--color-green)</code>.
Perhaps we should give up on base values and do:</p>
<pre><code class="language-css">--color-primary: var(--color-green);
--color-primary-*: var(--color-green-*);
</code></pre>
<p>That is certainly more explicit, at the cost of verbosity and potential for error.</p>
<h3 id="a-nesting-syntax-for-setting-multiple-variables-with-the-same-prefix-at-once" tabindex="-1"><a class="header-anchor" href="#a-nesting-syntax-for-setting-multiple-variables-with-the-same-prefix-at-once">A nesting syntax for setting multiple variables with the same prefix at once</a></h3>
<p>This would look just like the one above, but instead of specifying a group, it is just syntactic sugar for setting many variables at once.</p>
<h3 id="continuous-variations%3F" tabindex="-1"><a class="header-anchor" href="#continuous-variations%3F">Continuous variations?</a></h3>
<p>If nesting is merely syntactic sugar, that definitely makes it harder to add continuous variations as a feature.
It would need to be a separate feature that does not depend on nesting.</p>
<p>Perhaps something like this could work:</p>
<pre><code class="language-css">--color-green-*: color-mix(in oklch, var(--color-green-100) calc((100 - arg / 10) * 1%), var(--color-green-900));
</code></pre>
<p>or:</p>
<pre><code class="language-css">--color-green-[tint]: color-mix(in oklch, var(--color-green-100) calc((100 - tint / 10) * 1%), var(--color-green-900));
</code></pre>
<p>It is unclear whether these are possible syntax-wise, since we had to introduce a bunch of restrictions to future syntax to make <code>&amp;</code>-less nesting work.</p>
<h2 id="other-ideas" tabindex="-1"><a class="header-anchor" href="#other-ideas">Other ideas explored</a></h2>
<p>Some of the following may be useful in their own right, but I don’t think solve the pain points equally well.</p>
<h3 id="custom-functions" tabindex="-1"><a class="header-anchor" href="#custom-functions">Custom Functions</a></h3>
<p>At first glance it appears that <a href="https://github.com/w3c/csswg-drafts/issues/9350">custom functions</a> can solve all of these issues.
Instead of defining tokens like <code>--color-red-200</code> authors would instead be defining <code>--color-red(200)</code>.</p>
<p>There are several issues with this approach.</p>
<ol>
<li>Aliasing becomes extremely heavyweight as it requires a whole new function:</li>
</ol>
<pre><code class="language-css">@function --color-red(--tint: 40) {
/* ... */
}

@function --color-primary(--tint: 40) {
result: --color-red(var(--tint));
}
</code></pre>
<ol start="2">
<li>Which part is variable is part of the syntax, so e.g. in the example above, there is no clear path to defining a <code>--color()</code> function from that.</li>
<li>There is no way to pass a few key colors to a component or subtree and have the rest be computed from them.
In fact, we cannot pass functions around at all, only the result of their invocation.</li>
<li>Functions are global, whereas things like “primary color” often need to be scoped to a subtree.</li>
<li>The fallback story is unclear (see <a href="https://github.com/w3c/csswg-drafts/issues/9990">#9990</a>)</li>
<li>This approach works far better for tints that are generated as samples on a continuous axis.
It is unclear how a set of predefined tints would look like as something like that.</li>
<li>The migration path from existing design systems is rocky, whereas nested groups paves the cowpaths by allowing the same syntax to continue to be used and even provides a way to convert <em>existing</em> tokens to a group (and potentially <a href="#functional-syntax">allowing a functional syntax <em>as well</em></a>).</li>
<li>This only allows a single level, so entire palettes or design systems cannot be passed around unless the entire design system is encapsulated in a single function.</li>
</ol>
<h3 id="tint-shade" tabindex="-1"><a class="header-anchor" href="#tint-shade">Handle tints and shades in CSS …automagically?</a></h3>
<p>This idea involves trying to eliminate the need for precomputed variations by simply doing it in CSS. E.g. <code>color-tint(var(--color-yellow) 30%)</code>.
While these functions would be useful in their own right, it is incredibly difficult (and likely impossible) to design something that would completely remove the need for custom designer intervention due to the <a href="">lack of uniformity in the current manual palettes</a>.</p>
<h3 id="design-systems-syntax" tabindex="-1"><a class="header-anchor" href="#design-systems-syntax">Make design systems a first-class citizen</a></h3>
<p>This would involve standardizing a dedicated syntax and naming scheme (for the low level common denominator things — tints, hues, fonts, etc.) for design tokens,
and providing authors with a whole different syntax for passing design tokens around.
In some ways a bit like <code>accent-color</code> on steroids.</p>
<p>There is certainly some value in such an endeavor:</p>
<ul>
<li>Something like this would work <em>wonders</em> for making it easier to integrate web components into a page without having to tweak a ton of knobs (since even with variable groups, the component needs to be aware of the naming scheme used for the variations)</li>
<li>Similarly, authors could experiment with different themes without having to tweak anything in their own CSS or page.</li>
<li>They would be visible everywhere, even in non tree-abiding pseudo-elements and <code>@-rules</code> (but we could solve that in a much simpler way, e.g. via a <code>@document</code> or <code>::document</code> rule).</li>
</ul>
<p>However, this would be a far bigger undertaking and the Impact / Effort does not seem favorable.
It is unclear if there is any advantage other than standardizing names (which could simply be “standardized” by convention).
Variables get you a lot out of the box, that with this would need to redefine.
E.g. it would be very important to pass design tokens to SVGs, but <a href="https://tabatkins.github.io/specs/svg-params/">SVG params</a> are designed around variables.</p>
<p>It is also unclear if baking a naming scheme into CSS, even just for the lowest common denominator things, is feasible, given the amount of variation out there.</p>
<hr>
<p>I ran this by a couple design systems folks I know, and the response so far has been overwhelmingly “I NEED THIS YESTERDAY”.
While I’m pretty sure the design can use a lot of refinement (especially around continuous values) and I have not yet checked with implementors about feasibility, I’m really hoping we can prioritize solving this problem.</p>
<p>Note that beyond design systems, this would also address many (most?) of the use cases around maps that keep coming up (don’t have time to track them down right now, but maybe someone else can).</p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 549
- 0
cache/2024/0cc2e9c6b29f8326b2ff628f64e22888/index.md View File

@@ -0,0 +1,549 @@
title: Proposal: CSS Variable Groups
url: https://lea.verou.me/docs/var-groups/
hash_url: 0cc2e9c6b29f8326b2ff628f64e22888
archive_date: 2024-02-27
og_image: https://lea.verou.me/mark.svg
description: CSS Variable Groups is a way to define multiple properties under the same namespace
favicon: https://lea.verou.me/mark.svg
language: en_US

<div class=nutshell>
<p>CSS Variable Groups is a way to define multiple properties under the same namespace and pass the entire group around,
addressing several pain points around design tokens, design systems, and integrating third-party components.</p>
</div>
<h2 id="pain-points" tabindex="-1"><a class="header-anchor" href="#pain-points">Pain points</a></h2>
<h3 id="background" tabindex="-1"><a class="header-anchor" href="#background">Background</a></h3>
<p>Design tokens and design systems are about a lot more than color, but I’ll focus on color here, as that is the worst of it and also easier to explain.</p>
<p>The color part of most design systems consists of the following:</p>
<ul>
<li>Core hues: red, yellow, green, blue, etc. These are hues specifically picked by designers, not to be confused with the corresponding named colors.</li>
<li>Neutrals / Grays (often more than one)</li>
</ul>
<p>Each of the above is typically defined as a main color plus tints/shades under a number, with larger numbers corresponding to darker colors.
There is no commonly agreed naming convention wrt the numbering scheme used.</p>
<p>Some popular examples of such design systems:</p>
<table>
<thead>
<tr>
<th>Design system</th>
<th>Hues</th>
<th>Neutrals</th>
<th>Levels</th>
<th>Range</th>
<th>Increment</th>
<th>Extras</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="https://yeun.github.io/open-color/">Open color</a></td>
<td>12</td>
<td>1</td>
<td>10</td>
<td>0 - 9</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://open-props.style/#colors">Open props</a></td>
<td>17</td>
<td>2</td>
<td>13</td>
<td>0 - 12</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://tailwindcss.com/docs/customizing-colors">Tailwind</a></td>
<td>17</td>
<td>5</td>
<td>11</td>
<td>50 - 950</td>
<td>100</td>
<td>50, 950</td>
</tr>
<tr>
<td><a href="https://m2.material.io/design/color/the-color-system.html#color-theme-creation">Material</a></td>
<td>17</td>
<td>2</td>
<td>10</td>
<td>50-900</td>
<td>100</td>
<td>50</td>
</tr>
<tr>
<td><a href="https://spectrum.adobe.com/page/color-palette/">Adobe Spectrum</a></td>
<td>13</td>
<td>1</td>
<td>13</td>
<td>100 - 1300</td>
<td>100</td>
<td>Gray 50, Gray 75</td>
</tr>
<tr>
<td><a href="https://primer.style/foundations/color/base-scales">GitHub Primer</a></td>
<td>8</td>
<td>1</td>
<td>10</td>
<td>0 - 9</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://ant.design/docs/spec/colors">Ant Design</a></td>
<td>12</td>
<td>1</td>
<td>10 (13 for grays)</td>
<td>1 - 10</td>
<td>1</td>
<td></td>
</tr>
<tr>
<td><a href="https://www.ibm.com/design/language/color/">IBM Design Language</a></td>
<td>7</td>
<td>3</td>
<td>10</td>
<td>10 - 100</td>
<td>10</td>
<td></td>
</tr>
<tr>
<td><a href="https://www.radix-ui.com/themes/docs/theme/color">Radix UI</a></td>
<td>25</td>
<td>6</td>
<td>12</td>
<td>1 - 12</td>
<td>1</td>
<td></td>
</tr>
</tbody>
</table>
<p>In terms of CSS variables, this translates to variables like e.g. <code>--color-red-600</code>, <code>--color-gray-10</code> etc on <code>:root</code>. A LOT of them.</p>
<h3 id="summary-of-pain-points-%26-requirements" tabindex="-1"><a class="header-anchor" href="#summary-of-pain-points-%26-requirements">Summary of pain points &amp; requirements</a></h3>
<p>Distilling these pain points into their essence, it looks like the actual pain points are:</p>
<ol>
<li><strong>Aliasing</strong>: Aliasing a set of variables with a common prefix to a different prefix is very commonly needed, and requires <em>a lot</em> of CSS.
Even when using a build tool to automate this, the size of the resulting CSS is huge, and it is hard to debug as it clogs up the devtools.</li>
<li>Defining these tokens requires manually defining every single one, even when it could be computed via interpolation.</li>
<li>Getting arbitrary tokens (e.g. through a calculation) is impossible.</li>
</ol>
<p>Any solution would need to meet the following requirements:</p>
<ol>
<li>Subtree scoped: It needs to be possible to alias a set of variables to a different prefix on a subtree, so <code>@property</code> and any new tree-scoped @-rules are out.</li>
<li>Pave the cowpaths: Assuming a web app involves three classes of users (page authors, design system authors, web component authors)
the syntax should not require opt-in from all parties at once.
Individual classes of users should be able to derive value without all other parties having to change anything.</li>
<li>It should not require more than one declaration to style a single design aspect (e.g. the primary color) of a subtree or web component.</li>
</ol>
<h4 id="biggest-pain-point%3A-aliasing" tabindex="-1"><a class="header-anchor" href="#biggest-pain-point%3A-aliasing">Biggest pain point: Aliasing</a></h4>
<p>Then, to be used in the UI, the colors are also assigned <em>semantic</em> meaning: brand color, primary (or accent) color, secondary color, success, danger, etc. Pure hues can still be used directly for certain cases.</p>
<p>Assigning e.g.</p>
<pre><code class="language-css">--color-primary: var(--color-blue);
</code></pre>
<p>Does <em>not</em> also automatically give you <code>--color-primary-10</code>, <code>--color-primary-20</code> and so on. <strong>You have to painfully define them yourself</strong>:</p>
<pre><code class="language-css">--color-primary-10: var(--color-blue-10);
--color-primary-20: var(--color-blue-20);
/* ... */
--color-primary-100: var(--color-blue-100);
</code></pre>
<p>And this is for <em>all</em> your semantic colors. We’re talking about <em>a lot</em> of variable declarations.
And often specific tints are aliased further, e.g. text, border, background etc.
Often, all of this must be repeated for dark mode, high contrast mode, etc.</p>
<p>But let’s assume we dutifully do all this. Now let’s suppose we want to theme a certain part of the page with a different primary color. E.g. maybe we have callouts and we want to style notes (with shades of) green, tips yellow, warnings red, etc. We <em>could</em> have a <code>--color</code> or <code>--color-primary</code> property for the callout component, but that does not give us any of its variations, no, we need to define those manually <em>again</em>.</p>
<p>This also means that integrating third-party components is painful, because every single color variation they may need needs to be passed on individually. Most components <a href="https://shoelace.style/tokens/color#primitives">bundle an entire design system</a> and authors are expected to override every single property to integrate it with their own.</p>
<p>Note that this applies regardless of whether the tints and shades are precomputed (as is the case with most design systems today) or dynamically computed from color manipulation functions (our bright future?).</p>
<p>This is also a problem when adopting external libraries and design systems.
Currently, most libaries, design systems, icon libraries etc. have to use lengthy namespacing to avoid conflicts.
E.g. <a href="https://spectrum.adobe.com/page/color-palette/">Adobe Spectrum</a> prefixes each color with <code>--spectrum-global-color-</code> (e.g. <code>--spectrum-global-color-celery-100</code>).
They also often use color names that the author may want to remap to simpler names.
The effort needed for an author to remap all of these to more reasonable names is non-trivial (Radix UI even has <a href="https://www.radix-ui.com/themes/docs/theme/color#aliasing-colors">a section on this</a> — note that this is <em>just</em> for one color!).</p>
<h4 id="pain-point-2%3A-repetitiveness-and-verbosity" tabindex="-1"><a class="header-anchor" href="#pain-point-2%3A-repetitiveness-and-verbosity">Pain point 2: Repetitiveness and verbosity</a></h4>
<p>First, it is important to note that aesthetically pleasing color palettes are not completely perceptually uniform.
Chroma and hue often get skewed as you move towards the lightness edges, and they are skewed in different ways depending on the hue.
As the most obvious example, look at how yellows become orange as they darken in both of these but even more so in OC:</p>
<figure>
<img width="789" alt="image" src="images/yellow-tailwind.png">
<figcaption>Tailwind’s yellow palette</figcaption>
</figure>
<figure>
<img width="1527" alt="image" src="images/yellow-oc.png">
<figcaption>Open Color’s yellow palette</figcaption>
</figure>
<p><em>That said</em>, while we could not generate all tints through interpolation,
interpolation could approximate at least <em>some of them</em>.
But right now, the best we can do is something like this:</p>
<pre><code class="language-css">--color-green-200: color-mix(in oklch, var(--color-green-100) 20%, var(--color-green-500));
--color-green-300: color-mix(in oklch, var(--color-green-100) 40%, var(--color-green-500));
/* ... */
</code></pre>
<p>Now suppose we don’t like the interpolated <code>--color-green-300</code> and want to tweak it.
We’d <em>also</em> need to tweak <code>--color-green-200</code> if we want it to use that!</p>
<h4 id="pain-point-3%3A-cannot-reference-tokens-programmatically" tabindex="-1"><a class="header-anchor" href="#pain-point-3%3A-cannot-reference-tokens-programmatically">Pain point 3: Cannot reference tokens programmatically</a></h4>
<p>Since these are variables and variable names cannot be composed dynamically,
there is no way to transform a number (e.g. <code>200</code>) or a keyword (e.g. <code>red</code>) to a color token,
which could have allowed components to abstract away the specifics of the design system.</p>
<h2 id="proposal" tabindex="-1"><a class="header-anchor" href="#proposal">The Proposal: CSS Variable Groups</a></h2>
<p>The underlying pain point here is that authors need to be able to map <em>a set</em> of CSS variables to a different name, reactively.
What if we allowed them to do <em>just that</em>?</p>
<h3 id="defining-using" tabindex="-1"><a class="header-anchor" href="#defining-using">Defining and using a variable group</a></h3>
<p>This proposal allows authors to define groups of variables with the same prefix, by using braces
and then pass the whole group around to other variables:</p>
<pre><code class="language-css">--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
</code></pre>
<p>Then this is equivalent to creating <code>--color-green-100</code>, <code>--color-green-200</code>, etc. variables.
But with one difference. When doing:</p>
<pre><code class="language-css">--color-primary: var(--color-green);
</code></pre>
<p>This is passing a structured object behind the scenes so you <em>automatically</em> get <code>--color-primary-100</code>, <code>--color-primary-200</code> etc.</p>
<p>This allows patterns like:</p>
<pre><code class="language-css">/* Author CSS */
:root {
--color-green: {
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
}

some-component,
.callout-note {
--color-primary: var(--color-green);
}

.callout-note {
background: var(--color-primary-200);
}

/* some-component.css */
:host {
background: var(--color-primary-100);
border: var(--color-primary-400);
color: var(--color-primary-900);
}
</code></pre>
<div class="note">
<p>We may need to start with a hyphen (or two), because <a href="https://drafts.csswg.org/css-syntax/#ident-token-diagram">the <code>&lt;ident&gt;</code> production does not allow starting with a number</a>:</p>
<pre><code class="language-css">--color-green: {
-100: oklch(...);
-200: oklch(...);
/* ... */
}
</code></pre>
<p>But in the rest of this I’m gonna assume that Tab can come up with some ingenious solution to allow us to have the nicer syntax. 🙂
OTOH if these have a prefix, it means there are no naming conflicts with any predefined ones (<code>base</code>, <code>default</code>, etc.)</p>
</div>
<div class=note>
<p>Do we need a way to reference internal properties without having to use their full name (akin to JS <code>this</code>)?</p>
</div>
<p>The group inherits like a regular value, though if descendants define e.g. <code>--color-green-200</code>,
that would override the group value for that particular key.</p>
<h4 id="defining-or-overriding-tints-outside-the-group" tabindex="-1"><a class="header-anchor" href="#defining-or-overriding-tints-outside-the-group">Defining or overriding tints <em>outside</em> the group</a></h4>
<p>Note that once a variable is defined a group, ANY variable with that prefix on the same element becomes part of the group and is passed around or inherited down.
This means that this should work:</p>
<pre><code class="language-css">:root {
--color-green: {
100: oklch(95% 13% 135);
900: oklch(25% 20% 135);
}
}

html {
--color-green-200: oklch(95% 15% 135);
}

my-component {
--color-primary: var(--color-green);
background: var(--color-primary-200);
}
</code></pre>
<p>Or even this:</p>
<pre><code class="language-css">/* style.css */
:root {
--color-green: {};
}

my-component {
--color-primary: var(--color-green);
background: var(--color-primary-200);
}

/* design-system.css */
:root {
--color-green-100: oklch(95% 13% 135);
--color-green-200: oklch(95% 15% 135);
/* ... */
--color-green-900: oklch(25% 20% 135);
}
</code></pre>
<p>Which provides a way to “upgrade” existing design systems to using this new syntax without any changes to their code.</p>
<h3 id="using-groups-on-non-custom-properties%3A-the-base-property" tabindex="-1"><a class="header-anchor" href="#using-groups-on-non-custom-properties%3A-the-base-property">Using groups on non-custom properties: The <code>base</code> property</a></h3>
<p>When referencing a variable whose value is a group (e.g. <code>var(--color-green)</code> above) on a non-custom property,
by default it would either IACVT or resolve to <code>&lt;empty-token&gt;</code> (not sure which one is best here).</p>
<p>However, you can set a base value via the special <code>base</code> property (alternative names: <code>default</code>, <code>value</code>) which defines a plain value for when the property is used in a context that does not support groups, such as any of the existing non-custom properties:</p>
<pre><code class="language-css">--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};

.note {
/* Same as border: 1px solid oklch(65% 50% 135)); */
border: 1px solid var(--color-green);
}
</code></pre>
<aside>
In the future we may want to introduce native properties that accept groups.
Another possibility is to allow groups to be used as a shorthand’s whole value.
</aside>
<h3 id="nested-variable-groups" tabindex="-1"><a class="header-anchor" href="#nested-variable-groups">Nested variable groups</a></h3>
<p>Variable groups can be infinitely nested, which allows a single variable to hold an entire color palette:</p>
<pre><code class="language-css">--color: {
green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
200: oklch(95% 15% 135);
/* ... */
900: oklch(25% 20% 135);
};
red: {
base: oklch(55% 55% 30);
100: oklch(95% 13% 30)
}
}
</code></pre>
<p>This color palette could be passed on to a component in one go:</p>
<pre><code class="language-css">--component-palette: var(--color);
</code></pre>
<p>The values are not limited to colors and the keys are not limited to numbers:</p>
<pre><code class="language-css">--font: {
serif: Vollkorn;
sans: Inter;
mono: Inconsolata;
};
</code></pre>
<p>In fact, together with nesting, one could imagine passing <em>entire</em> design systems around with a single variable reference!</p>
<pre><code class="language-css">/* primer.css */
--primer: {
color: {
/* ... */
},
font: {
/* ... */
},
/* ... */
};

/* author css */
--design: var(--primer);
</code></pre>
<p>With compatible naming schemes, authors could even compose their design system by mixing and matching from existing design systems.
E.g. using the Primer design system, with the Open Color color palette:</p>
<pre><code class="language-css">/* primer.css */
--primer: {
color: {/* ... */};
font: {/* ... */};
size: {/* ... */};
/* ... */
}

/* open-color.css */
--oc: {
red: { /* ... */ }
}

/* author css */
--design: var(--primer);
--design-color: var(--oc);
</code></pre>
<div class=note>
<p>Note that as currently defined, the pattern above would <em>override</em> Primer’s colors with Open Color’s,
so <code>--primer-color</code> would <em>also</em> resolve to <code>var(--oc)</code>.
To avoid this, authors could alias each top-level group separately:</p>
<pre><code class="language-css">--design: {
color: var(--oc);
font: var(--primer-font);
size: var(--primer-size);
/* ... */
}
</code></pre>
</div>
<h3 id="tweaking-the-default-value-without-destroying-the-group" tabindex="-1"><a class="header-anchor" href="#tweaking-the-default-value-without-destroying-the-group">Tweaking the default value without destroying the group</a></h3>
<p>To minimize surprise, things like this:</p>
<pre><code class="language-css">/* Make core green a little yellower */
--color-green: oklch(65% 50% 130);
</code></pre>
<p>Would need to override the whole variable value, meaning <code>--color-green-100</code> is now undefined (<code>initial</code>).
This is also consistent with how shorthands work.
Also, if we kept the subproperties intact when overriding the base color, they would get out of sync, which is worse.</p>
<p>But this begs the question: then how do we override <em>just</em> the default value?
For example, to tweak the base color and maintain dynamic tints generated from it.</p>
<p>One way would be to expose special properties like <code>base</code> like regular custom properties that can be overridden. Then you could just do that:</p>
<pre><code class="language-css">/* Make core green a little yellower */
--color-green-base: oklch(65% 50% 130);
</code></pre>
<p>And as a bonus, this facilitates debugging, and allows customizing more than just the default value.</p>
<h2 id="continuous" tabindex="-1"><a class="header-anchor" href="#continuous">Facilitating continuous variations</a></h2>
<p>So far, this proposal has been about facilitating the use of predefined static tokens.
But what if we could support <em>dynamic</em> variations, where only a few key values are defined, and the rest are interpolated within them?</p>
<div class=note>
This is the least fleshed out part of this proposal, but I think it could be a very powerful feature (possibly not MVP though).
</div>
<h3 id="the-default-property" tabindex="-1"><a class="header-anchor" href="#the-default-property">The <code>default</code> property</a></h3>
<p>I’m thinking of a <code>default</code> special property (other potential names: <code>any</code>, <code>other</code>, <code>else</code>, <code>*</code> (if possible)), that would be a catch-all for any undefined value.
The key would be passed to the expression as a predefined keyword (e.g. <code>arg</code>), but that can be customized.</p>
<pre><code class="language-css">--color-green: {
base: oklch(65% 50% 135);
100: oklch(95% 13% 135);
default: color-mix(in oklch, var(--color-green-100) calc((100 - arg / 10) * 1%), var(--color-green-900));
900: oklch(25% 20% 135);
};
</code></pre>
<p>It could even be a shorthand, with <code>default-value</code> and <code>default-type</code> to specify the return type.</p>
<p>Customizing the arg name (both for readabiity and to facilitate nested use cases):</p>
<pre><code class="language-css">--color-green: {
base: oklch(...);
100: oklch();
name: tint;
default: color-mix(in oklch, var(--color-green-100) calc((100 - tint / 10) * 1%), var(--color-green-900));
900: oklch();
};
</code></pre>
<h3 id="piecewise-interpolation" tabindex="-1"><a class="header-anchor" href="#piecewise-interpolation">Piecewise interpolation</a></h3>
<p>The previous example always calculates mid points from the ends of the spectrum. However, it would serve use cases far better to be able to set spot colors to course correct and have the intermediate tints compute from them, similar to how gradient color stops work.</p>
<p>Perhaps numerical keys could be auto-detected and the closest min and max keys and values could be made available to the expression as keywords.
Potential names: <code>min</code> and <code>max</code> for values, <code>min-key</code>, <code>max-key</code> for the keys.
Example (see <a href="https://drafts.csswg.org/css-values-5/#progress-func"><code>progress()</code></a>):</p>
<pre><code class="language-css">--color-green: {
base: oklch(...);
100: oklch();
default: color-mix(in oklch, min calc(progress(arg from min-key to max-key) * 100%), max);
900: oklch();
};
</code></pre>
<p>These would only be available when <code>arg</code> is numerical AND there are other numerical keys defined.
There could be a max without a min and vice versa if only larger or only smaller numerical keys are available.</p>
<p>It could even be specified with <em>just</em> <code>default</code>:</p>
<pre><code class="language-css">--gray: {
default: color-mix(in oklab, white calc(1% * arg), black);
}
</code></pre>
<h3 id="issues" tabindex="-1"><a class="header-anchor" href="#issues">Issues</a></h3>
<p>One issue is that while defining tokens via interpolation can be convenient, design system authors often do not want to expose the entire spectrum,
but only a few carefully chosen tokens.
So even if we allow the token values to be specified via a formula, we may need to introduce a way to optionally limit the keys that are exposed.
Potential solutions:</p>
<ul>
<li>A <code>default-keys</code> property (potentially a shorthand) that defines the min/max/step for the keys that are exposed.</li>
<li>A way to list specific keys, rather than a catch-all <code>default</code></li>
</ul>
<h2 id="functional-syntax" tabindex="-1"><a class="header-anchor" href="#functional-syntax">Getting group properties dynamically</a></h2>
<p>Currently, we can only access properties via static offsets, even when dynamic variations are allowed.
If we automatically exposed a functional syntax for every group, we could select the right token on the fly, possibly as a result of calculations.
Nested groups would simply involve more than one argument.</p>
<p>This would also allow mapping design tokens to a different naming scheme and reducing verbosity.
E.g. suppose we have <code>--spectrum-global-color-celery-100</code> to <code>--spectrum-global-color-celery-1300</code> and we want to map them to <code>--color-green-1</code> to <code>--color-green-13</code>,
i.e. not just a different prefix, but also a different scale:</p>
<pre><code class="language-css">/* Turn Spectrum colors into a group */
--spectrum-global-color: {};
--spectrum-global-color-celery: {};

--color-green: {
default: --spectrum-global-color-celery(calc(arg / 100));
}
</code></pre>
<p>One downside to simply making these functions is that they could potentially clash with custom functions.
Roma Komarov <a href="https://github.com/w3c/csswg-drafts/issues/9992#issuecomment-1962321439">proposed</a> a separate <code>get()</code> function that would take the prefix as its first argument.
I quite like this, and it means it can ship separately, as it can be based on property naming, not groups.
This also means it does not require converting anything into groups.
So the example above would be way simpler:</p>
<pre><code class="language-css">--color-green: {
default: get(--spectrum-global-color-celery, calc(arg / 100));
}
</code></pre>
<h2 id="decomposed-alternative" tabindex="-1"><a class="header-anchor" href="#decomposed-alternative">Alternative decomposed design</a></h2>
<p>We could decouple this into three separate features.
This is likely easier to implement as a whole, but also these features can ship independently and add value on their own.
However, it also makes it less ambitious, as it becomes harder to add some of the more advanced features (e.g. continuous variations).</p>
<h3 id="a-function-to-map-css-variables-with-a-common-prefix-to-a-different-prefix" tabindex="-1"><a class="header-anchor" href="#a-function-to-map-css-variables-with-a-common-prefix-to-a-different-prefix">A function to map CSS variables with a common prefix to a different prefix</a></h3>
<p>A <code>var()</code>-like function (e.g. <code>vars()</code>, <code>group()</code>) for mapping many variables with a common prefix to a different prefix.</p>
<pre><code class="language-css">--color-primary: group(--color-green);
</code></pre>
<p>Maybe even <code>var()</code> itself, where we’d distinguish between the two because the var reference would include an asterisk:</p>
<pre><code class="language-css">--color-primary: var(--color-green-*);
</code></pre>
<p>The downside of this is that it’s unclear whether that also sets <code>--color-primary</code> to <code>var(--color-green)</code>.
Perhaps we should give up on base values and do:</p>
<pre><code class="language-css">--color-primary: var(--color-green);
--color-primary-*: var(--color-green-*);
</code></pre>
<p>That is certainly more explicit, at the cost of verbosity and potential for error.</p>
<h3 id="a-nesting-syntax-for-setting-multiple-variables-with-the-same-prefix-at-once" tabindex="-1"><a class="header-anchor" href="#a-nesting-syntax-for-setting-multiple-variables-with-the-same-prefix-at-once">A nesting syntax for setting multiple variables with the same prefix at once</a></h3>
<p>This would look just like the one above, but instead of specifying a group, it is just syntactic sugar for setting many variables at once.</p>
<h3 id="continuous-variations%3F" tabindex="-1"><a class="header-anchor" href="#continuous-variations%3F">Continuous variations?</a></h3>
<p>If nesting is merely syntactic sugar, that definitely makes it harder to add continuous variations as a feature.
It would need to be a separate feature that does not depend on nesting.</p>
<p>Perhaps something like this could work:</p>
<pre><code class="language-css">--color-green-*: color-mix(in oklch, var(--color-green-100) calc((100 - arg / 10) * 1%), var(--color-green-900));
</code></pre>
<p>or:</p>
<pre><code class="language-css">--color-green-[tint]: color-mix(in oklch, var(--color-green-100) calc((100 - tint / 10) * 1%), var(--color-green-900));
</code></pre>
<p>It is unclear whether these are possible syntax-wise, since we had to introduce a bunch of restrictions to future syntax to make <code>&amp;</code>-less nesting work.</p>
<h2 id="other-ideas" tabindex="-1"><a class="header-anchor" href="#other-ideas">Other ideas explored</a></h2>
<p>Some of the following may be useful in their own right, but I don’t think solve the pain points equally well.</p>
<h3 id="custom-functions" tabindex="-1"><a class="header-anchor" href="#custom-functions">Custom Functions</a></h3>
<p>At first glance it appears that <a href="https://github.com/w3c/csswg-drafts/issues/9350">custom functions</a> can solve all of these issues.
Instead of defining tokens like <code>--color-red-200</code> authors would instead be defining <code>--color-red(200)</code>.</p>
<p>There are several issues with this approach.</p>
<ol>
<li>Aliasing becomes extremely heavyweight as it requires a whole new function:</li>
</ol>
<pre><code class="language-css">@function --color-red(--tint: 40) {
/* ... */
}

@function --color-primary(--tint: 40) {
result: --color-red(var(--tint));
}
</code></pre>
<ol start="2">
<li>Which part is variable is part of the syntax, so e.g. in the example above, there is no clear path to defining a <code>--color()</code> function from that.</li>
<li>There is no way to pass a few key colors to a component or subtree and have the rest be computed from them.
In fact, we cannot pass functions around at all, only the result of their invocation.</li>
<li>Functions are global, whereas things like “primary color” often need to be scoped to a subtree.</li>
<li>The fallback story is unclear (see <a href="https://github.com/w3c/csswg-drafts/issues/9990">#9990</a>)</li>
<li>This approach works far better for tints that are generated as samples on a continuous axis.
It is unclear how a set of predefined tints would look like as something like that.</li>
<li>The migration path from existing design systems is rocky, whereas nested groups paves the cowpaths by allowing the same syntax to continue to be used and even provides a way to convert <em>existing</em> tokens to a group (and potentially <a href="#functional-syntax">allowing a functional syntax <em>as well</em></a>).</li>
<li>This only allows a single level, so entire palettes or design systems cannot be passed around unless the entire design system is encapsulated in a single function.</li>
</ol>
<h3 id="tint-shade" tabindex="-1"><a class="header-anchor" href="#tint-shade">Handle tints and shades in CSS …automagically?</a></h3>
<p>This idea involves trying to eliminate the need for precomputed variations by simply doing it in CSS. E.g. <code>color-tint(var(--color-yellow) 30%)</code>.
While these functions would be useful in their own right, it is incredibly difficult (and likely impossible) to design something that would completely remove the need for custom designer intervention due to the <a href="">lack of uniformity in the current manual palettes</a>.</p>
<h3 id="design-systems-syntax" tabindex="-1"><a class="header-anchor" href="#design-systems-syntax">Make design systems a first-class citizen</a></h3>
<p>This would involve standardizing a dedicated syntax and naming scheme (for the low level common denominator things — tints, hues, fonts, etc.) for design tokens,
and providing authors with a whole different syntax for passing design tokens around.
In some ways a bit like <code>accent-color</code> on steroids.</p>
<p>There is certainly some value in such an endeavor:</p>
<ul>
<li>Something like this would work <em>wonders</em> for making it easier to integrate web components into a page without having to tweak a ton of knobs (since even with variable groups, the component needs to be aware of the naming scheme used for the variations)</li>
<li>Similarly, authors could experiment with different themes without having to tweak anything in their own CSS or page.</li>
<li>They would be visible everywhere, even in non tree-abiding pseudo-elements and <code>@-rules</code> (but we could solve that in a much simpler way, e.g. via a <code>@document</code> or <code>::document</code> rule).</li>
</ul>
<p>However, this would be a far bigger undertaking and the Impact / Effort does not seem favorable.
It is unclear if there is any advantage other than standardizing names (which could simply be “standardized” by convention).
Variables get you a lot out of the box, that with this would need to redefine.
E.g. it would be very important to pass design tokens to SVGs, but <a href="https://tabatkins.github.io/specs/svg-params/">SVG params</a> are designed around variables.</p>
<p>It is also unclear if baking a naming scheme into CSS, even just for the lowest common denominator things, is feasible, given the amount of variation out there.</p>
<hr>
<p>I ran this by a couple design systems folks I know, and the response so far has been overwhelmingly “I NEED THIS YESTERDAY”.
While I’m pretty sure the design can use a lot of refinement (especially around continuous values) and I have not yet checked with implementors about feasibility, I’m really hoping we can prioritize solving this problem.</p>
<p>Note that beyond design systems, this would also address many (most?) of the use cases around maps that keep coming up (don’t have time to track them down right now, but maybe someone else can).</p>

+ 356
- 0
cache/2024/d9c30865dde8c88394ba054836a18ae3/index.html View File

@@ -0,0 +1,356 @@
<!doctype html><!-- This is a valid HTML5 document. -->
<!-- Screen readers, SEO, extensions and so on. -->
<html lang="en">
<!-- Has to be within the first 1024 bytes, hence before the `title` element
See: https://www.w3.org/TR/2012/CR-html5-20121217/document-metadata.html#charset -->
<meta charset="utf-8">
<!-- Why no `X-UA-Compatible` meta: https://stackoverflow.com/a/6771584 -->
<!-- The viewport meta is quite crowded and we are responsible for that.
See: https://codepen.io/tigt/post/meta-viewport-for-2015 -->
<meta name="viewport" content="width=device-width,initial-scale=1">
<!-- Required to make a valid HTML5 document. -->
<title>Designing a JavaScript Plugin System (archive) — David Larlet</title>
<meta name="description" content="Publication mise en cache pour en conserver une trace.">
<!-- That good ol' feed, subscribe :). -->
<link rel="alternate" type="application/atom+xml" title="Feed" href="/david/log/">
<!-- Generated from https://realfavicongenerator.net/ such a mess. -->
<link rel="apple-touch-icon" sizes="180x180" href="/static/david/icons2/apple-touch-icon.png">
<link rel="icon" type="image/png" sizes="32x32" href="/static/david/icons2/favicon-32x32.png">
<link rel="icon" type="image/png" sizes="16x16" href="/static/david/icons2/favicon-16x16.png">
<link rel="manifest" href="/static/david/icons2/site.webmanifest">
<link rel="mask-icon" href="/static/david/icons2/safari-pinned-tab.svg" color="#07486c">
<link rel="shortcut icon" href="/static/david/icons2/favicon.ico">
<meta name="msapplication-TileColor" content="#f7f7f7">
<meta name="msapplication-config" content="/static/david/icons2/browserconfig.xml">
<meta name="theme-color" content="#f7f7f7" media="(prefers-color-scheme: light)">
<meta name="theme-color" content="#272727" media="(prefers-color-scheme: dark)">
<!-- Is that even respected? Retrospectively? What a shAItshow…
https://neil-clarke.com/block-the-bots-that-feed-ai-models-by-scraping-your-website/ -->
<meta name="robots" content="noai, noimageai">
<!-- Documented, feel free to shoot an email. -->
<link rel="stylesheet" href="/static/david/css/style_2021-01-20.css">
<!-- See https://www.zachleat.com/web/comprehensive-webfonts/ for the trade-off. -->
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t4_poly_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: light), (prefers-color-scheme: no-preference)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_regular.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_bold.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<link rel="preload" href="/static/david/css/fonts/triplicate_t3_italic.woff2" as="font" type="font/woff2" media="(prefers-color-scheme: dark)" crossorigin>
<script>
function toggleTheme(themeName) {
document.documentElement.classList.toggle(
'forced-dark',
themeName === 'dark'
)
document.documentElement.classList.toggle(
'forced-light',
themeName === 'light'
)
}
const selectedTheme = localStorage.getItem('theme')
if (selectedTheme !== 'undefined') {
toggleTheme(selectedTheme)
}
</script>

<meta name="robots" content="noindex, nofollow">
<meta content="origin-when-cross-origin" name="referrer">
<!-- Canonical URL for SEO purposes -->
<link rel="canonical" href="https://css-tricks.com/designing-a-javascript-plugin-system/">

<body class="remarkdown h1-underline h2-underline h3-underline em-underscore hr-center ul-star pre-tick" data-instant-intensity="viewport-all">


<article>
<header>
<h1>Designing a JavaScript Plugin System</h1>
</header>
<nav>
<p class="center">
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="https://css-tricks.com/designing-a-javascript-plugin-system/" title="Lien vers le contenu original">Source originale</a>
<br>
Mis en cache le 2024-02-27
</p>
</nav>
<hr>
<p>WordPress has <a href="https://wordpress.org/plugins/" rel="noopener">plugins</a>. jQuery has <a href="https://plugins.jquery.com" rel="noopener">plugins</a>. <a href="https://www.gatsbyjs.org/docs/plugins/" rel="noopener">Gatsby</a>, <a href="https://www.11ty.dev/docs/plugins/" rel="noopener">Eleventy</a>, and <a href="https://css-tricks.com/getting-started-with-vue-plugins/">Vue </a>do, too.</p>

<p>Plugins are a common feature of libraries and frameworks, and for a good reason: they allow developers to add functionality, in a safe, scalable way. This makes the core project more valuable, and it builds a community — all without creating an additional maintenance burden. What a great deal!</p>

<p>So how do you go about building a plugin system? Let’s answer that question by building one of our own, in JavaScript.</p>

<p><span id="more-318994"></span></p>
<p class="explanation">I’m using the word “plugin” but these things are sometimes called other names, like “extensions,” “add-ons,” or “modules.” Whatever you call them, the concept (and benefit) is the same.</p>

<h3 class="wp-block-heading" id="lets-build-a-plugin-system"><a href="#aa-lets-build-a-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-lets-build-a-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Let’s build a plugin system</h3>

<p>Let’s start with an example project called BetaCalc. The goal for BetaCalc is to be a minimalist JavaScript calculator that other developers can add “buttons” to. Here’s some basic code to get us started:</p>

<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};
// Using the calculator
betaCalc.setValue(3); // =&gt; 3
betaCalc.plus(3);     // =&gt; 6
betaCalc.minus(2);    // =&gt; 4</code></pre>

<p>We’re defining our calculator as an object-literal to keep things simple. The calculator works by printing its result via <code>console.log</code>.</p>

<p>Functionality is really limited right now. We have a <code>setValue</code> method, which takes a number and displays it on the “screen.” We also have <code>plus</code> and <code>minus</code> methods, which will perform an operation on the currently displayed value.</p>

<p>It’s time to add more functionality. Let’s start by creating a plugin system.</p>

<h3 class="wp-block-heading" id="the-worlds-smallest-plugin-system"><a href="#aa-the-worlds-smallest-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-the-worlds-smallest-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>The world’s smallest plugin system</h3>

<p>We’ll start by creating a <code>register</code> method that other developers can use to register a plugin with BetaCalc. The job of this method is simple: take the external plugin, grab its <code>exec</code> function, and attach it to our calculator as a new method:</p>

<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  // ...other calculator code up here
  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};</code></pre>

<p>And here’s an example plugin, which gives our calculator a “squared” button:</p>

<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Define the plugin
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};
// Register the plugin
betaCalc.register(squaredPlugin);</code></pre>

<p>In many plugin systems, it’s common for plugins to have two parts:</p>

<ol><li>Code to be executed</li><li>Metadata (like a name, description, version number, dependencies, etc.)</li></ol>

<p>In our plugin, the <code>exec</code> function contains our code, and the <code>name</code> is our metadata. When the plugin is registered, the exec function is attached directly to our <code>betaCalc</code> object as a method, giving it access to BetaCalc’s <code>this</code>.</p>

<p>So now, BetaCalc has a new “squared” button, which can be called directly:</p>

<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">betaCalc.setValue(3); // =&gt; 3
betaCalc.plus(2);     // =&gt; 5
betaCalc.squared();   // =&gt; 25
betaCalc.squared();   // =&gt; 625</code></pre>

<p>There’s a lot to like about this system. The plugin is a simple object-literal that can be passed into our function. This means that plugins can be downloaded via npm and imported as ES6 modules. Easy distribution is super important!</p>

<p>But our system has a few flaws.</p>

<p>By giving plugins access to BetaCalc’s <code>this</code>, they get read/write access to all of BetaCalc’s code. While this is useful for getting and setting the <code>currentValue</code>, it’s also dangerous. If a plugin was to redefine an internal function (like <code>setValue</code>), it could produce unexpected results for BetaCalc and other plugins. This violates <a href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle" rel="noopener">the open-closed principle</a>, which states that a software entity should be open for extension but closed for modification.</p>

<p>Also, the “squared” function works by producing <a href="https://en.wikipedia.org/wiki/Side_effect_(computer_science)" rel="nofollow noopener">side effects</a>. That’s not uncommon in JavaScript, but it doesn’t feel great — especially when other plugins could be in there messing with the same internal state. A more <a href="https://en.wikipedia.org/wiki/Functional_programming" rel="nofollow noopener">functional</a> approach would go a long way toward making our system safer and more predictable.</p>

<h3 class="wp-block-heading" id="a-better-plugin-architecture"><a href="#aa-a-better-plugin-architecture" aria-hidden="true" class="aal_anchor" id="aa-a-better-plugin-architecture"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>A better plugin architecture</h3>

<p>Let’s take another pass at a better plugin architecture. This next example changes both the calculator and its plugin API:</p>

<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) =&gt; currentVal + addend,
    'minus': (currentVal, subtrahend) =&gt; currentVal - subtrahend
  },
  plugins: {},    
  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },
  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// Our Plugin
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};
betaCalc.register(squaredPlugin);
// Using the calculator
betaCalc.setValue(3);      // =&gt; 3
betaCalc.press('plus', 2); // =&gt; 5
betaCalc.press('squared'); // =&gt; 25
betaCalc.press('squared'); // =&gt; 625</code></pre>

<p>We’ve got a few notable changes here.</p>

<p>First, we’ve separated the plugins from “core” calculator methods (like <code>plus</code> and <code>minus</code>), by putting them in their own plugins object. Storing our plugins in a <code>plugin</code> object makes our system safer. Now plugins accessing this can’t see the BetaCalc properties — they can only see properties of <code>betaCalc.plugins</code>.</p>

<p>Second, we’ve implemented a <code>press</code> method, which looks up the button’s function by name and then calls it. Now when we call a plugin’s <code>exec</code> function, we pass it the current calculator value (<code>currentValue</code>), and we expect it to return the new calculator value.</p>

<p>Essentially, this new <code>press</code> method converts all of our calculator buttons into <a href="https://en.wikipedia.org/wiki/Pure_function" rel="nofollow noopener">pure functions</a>. They take a value, perform an operation, and return the result. This has a lot of benefits:</p>

<ul><li>It simplifies the API.</li><li>It makes testing easier (for both BetaCalc and the plugins themselves).</li><li>It reduces the dependencies of our system, making it more <a href="https://en.wikipedia.org/wiki/Loose_coupling" rel="noopener">loosely coupled</a>.</li></ul>

<p>This new architecture is more limited than the first example, but in a good way. We’ve essentially put up guardrails for plugin authors, restricting them to <a href="https://www.bryanbraun.com/2015/02/16/on-designing-great-systems/" rel="noopener">only the kind of changes that we want them to make</a>.</p>

<p>In fact, it might be too restrictive! Now our calculator plugins can only do operations on the <code>currentValue</code>. If a plugin author wanted to add advanced functionality like a “memory” button or a way to track history, they wouldn’t be able to.</p>

<p>Maybe that’s ok. The amount of power you give plugin authors is a delicate balance. Giving them too much power could impact the stability of your project. But giving them too little power makes it hard for them to solve their problems — in that case you might as well not have plugins.</p>

<h3 class="wp-block-heading" id="what-more-could-we-do"><a href="#aa-what-more-could-we-do" aria-hidden="true" class="aal_anchor" id="aa-what-more-could-we-do"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>What more could we do?</h3>

<p>There’s a lot more we could do to improve our system.</p>

<p>We could add error handling to notify plugin authors if they forget to define a name or return a value. It’s good to think like a QA dev and imagine how our system could break so we can proactively handle those cases.</p>

<p>We could expand the scope of what a plugin can do. Currently, a BetaCalc plugin can add a button. But what if it could also register callbacks for certain lifecycle events — like when the calculator is about to display a value? Or what if there was a dedicated place for it to store a piece of state across multiple interactions? Would that open up some new use cases?</p>

<p>We could also expand plugin registration. What if a plugin could be registered with some initial settings? Could that make the plugins more flexible? What if a plugin author wanted to register a whole suite of buttons instead of a single one — like a “BetaCalc Statistics Pack”? What changes would be needed to support that?</p>

<h3 class="wp-block-heading" id="your-plugin-system"><a href="#aa-your-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-your-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Your plugin system</h3>

<p>Both BetaCalc and its plugin system are deliberately simple. If your project is larger, then you’ll want to explore some other plugin architectures.</p>

<p>One good place to start is to look at existing projects for examples of successful plugin systems. For JavaScript, that could mean <a href="https://learn.jquery.com/plugins/basic-plugin-creation/" rel="nofollow noopener">jQuery</a>, <a href="https://www.gatsbyjs.org/docs/creating-plugins/" rel="nofollow noopener">Gatsby</a>, <a href="https://github.com/d3/d3/wiki/Plugins" rel="nofollow noopener">D3</a>, <a href="https://ckeditor.com/docs/ckeditor4/latest/guide/plugin_sdk_intro.html" rel="nofollow noopener">CKEditor</a>, or others.</p>

<p>You may also want to be familiar with various <a href="https://seesparkbox.com/foundry/javascript_design_patterns" rel="noopener">JavaScript design patterns</a>. (Addy Osmani <a href="https://addyosmani.com/resources/essentialjsdesignpatterns/book/" rel="noopener">has a book</a> on the subject.)  Each pattern provides a different interface and degree of coupling, which gives you a lot of good plugin architecture options to choose from. Being aware of these options helps you better balance the needs of everyone who uses your project.</p>

<p>Besides the patterns themselves, there’s a lot of good software development principles you can draw on to make these kinds of decisions. I’ve mentioned a few along the way (like the open-closed principle and loose coupling), but some other relevant ones include the <a href="http://wiki.c2.com/?LawOfDemeter" rel="nofollow noopener">Law of Demeter</a> and <a href="https://www.martinfowler.com/articles/injection.html" rel="noopener">dependency injection</a>.</p>

<p>I know it sounds like a lot, but you’ve gotta do your research. Nothing is more painful than making everyone rewrite their plugins because you needed to change the plugin architecture. It’s a quick way to lose trust and discourage people from contributing in the future.</p>

<h3 class="wp-block-heading" id="conclusion"><a href="#aa-conclusion" aria-hidden="true" class="aal_anchor" id="aa-conclusion"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Conclusion</h3>

<p>Writing a good plugin architecture from scratch is difficult! You have to balance a lot of considerations to build a system that meets everyone’s needs. Is it simple enough? Powerful enough? Will it work long term?</p>

<p>It’s worth the effort though. Having a good plugin system helps everyone. Developers get the freedom to solve their problems. End users get a large number of opt-in features to choose from. And you get to grow an ecosystem and community around your project. It’s a win-win-win situation.</p>
</article>


<hr>

<footer>
<p>
<a href="/david/" title="Aller à l’accueil"><svg class="icon icon-home">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-home"></use>
</svg> Accueil</a> •
<a href="/david/log/" title="Accès au flux RSS"><svg class="icon icon-rss2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-rss2"></use>
</svg> Suivre</a> •
<a href="http://larlet.com" title="Go to my English profile" data-instant><svg class="icon icon-user-tie">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-user-tie"></use>
</svg> Pro</a> •
<a href="mailto:david%40larlet.fr" title="Envoyer un courriel"><svg class="icon icon-mail">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-mail"></use>
</svg> Email</a> •
<abbr class="nowrap" title="Hébergeur : Alwaysdata, 62 rue Tiquetonne 75002 Paris, +33184162340"><svg class="icon icon-hammer2">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-hammer2"></use>
</svg> Légal</abbr>
</p>
<template id="theme-selector">
<form>
<fieldset>
<legend><svg class="icon icon-brightness-contrast">
<use xlink:href="/static/david/icons2/symbol-defs-2021-12.svg#icon-brightness-contrast"></use>
</svg> Thème</legend>
<label>
<input type="radio" value="auto" name="chosen-color-scheme" checked> Auto
</label>
<label>
<input type="radio" value="dark" name="chosen-color-scheme"> Foncé
</label>
<label>
<input type="radio" value="light" name="chosen-color-scheme"> Clair
</label>
</fieldset>
</form>
</template>
</footer>
<script src="/static/david/js/instantpage-5.1.0.min.js" type="module"></script>
<script>
function loadThemeForm(templateName) {
const themeSelectorTemplate = document.querySelector(templateName)
const form = themeSelectorTemplate.content.firstElementChild
themeSelectorTemplate.replaceWith(form)

form.addEventListener('change', (e) => {
const chosenColorScheme = e.target.value
localStorage.setItem('theme', chosenColorScheme)
toggleTheme(chosenColorScheme)
})

const selectedTheme = localStorage.getItem('theme')
if (selectedTheme && selectedTheme !== 'undefined') {
form.querySelector(`[value="${selectedTheme}"]`).checked = true
}
}

const prefersColorSchemeDark = '(prefers-color-scheme: dark)'
window.addEventListener('load', () => {
let hasDarkRules = false
for (const styleSheet of Array.from(document.styleSheets)) {
let mediaRules = []
for (const cssRule of styleSheet.cssRules) {
if (cssRule.type !== CSSRule.MEDIA_RULE) {
continue
}
// WARNING: Safari does not have/supports `conditionText`.
if (cssRule.conditionText) {
if (cssRule.conditionText !== prefersColorSchemeDark) {
continue
}
} else {
if (cssRule.cssText.startsWith(prefersColorSchemeDark)) {
continue
}
}
mediaRules = mediaRules.concat(Array.from(cssRule.cssRules))
}

// WARNING: do not try to insert a Rule to a styleSheet you are
// currently iterating on, otherwise the browser will be stuck
// in a infinite loop…
for (const mediaRule of mediaRules) {
styleSheet.insertRule(mediaRule.cssText)
hasDarkRules = true
}
}
if (hasDarkRules) {
loadThemeForm('#theme-selector')
}
})
</script>
</body>
</html>

+ 276
- 0
cache/2024/d9c30865dde8c88394ba054836a18ae3/index.md View File

@@ -0,0 +1,276 @@
title: Designing a JavaScript Plugin System
url: https://css-tricks.com/designing-a-javascript-plugin-system/
hash_url: d9c30865dde8c88394ba054836a18ae3
archive_date: 2024-02-27
og_image: https://css-tricks.com/wp-json/social-image-generator/v1/image/318994
description: WordPress has plugins. jQuery has plugins. Gatsby, Eleventy, and Vue do, too.
favicon: https://css-tricks.com/favicon.svg
language: en_US

<p>WordPress has <a href="https://wordpress.org/plugins/" rel="noopener">plugins</a>. jQuery has <a href="https://plugins.jquery.com" rel="noopener">plugins</a>. <a href="https://www.gatsbyjs.org/docs/plugins/" rel="noopener">Gatsby</a>, <a href="https://www.11ty.dev/docs/plugins/" rel="noopener">Eleventy</a>, and <a href="https://css-tricks.com/getting-started-with-vue-plugins/">Vue </a>do, too.</p>



<p>Plugins are a common feature of libraries and frameworks, and for a good reason: they allow developers to add functionality, in a safe, scalable way. This makes the core project more valuable, and it builds a community — all without creating an additional maintenance burden. What a great deal!</p>



<p>So how do you go about building a plugin system? Let’s answer that question by building one of our own, in JavaScript.</p>



<span id="more-318994"></span>



<p class="explanation">I’m using the word “plugin” but these things are sometimes called other names, like “extensions,” “add-ons,” or “modules.” Whatever you call them, the concept (and benefit) is the same.</p>


<h3 class="wp-block-heading" id="lets-build-a-plugin-system"><a href="#aa-lets-build-a-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-lets-build-a-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Let’s build a plugin system</h3>


<p>Let’s start with an example project called BetaCalc. The goal for BetaCalc is to be a minimalist JavaScript calculator that other developers can add “buttons” to. Here’s some basic code to get us started:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(newValue) {
    this.currentValue = newValue;
    console.log(this.currentValue);
  },
  
  plus(addend) {
    this.setValue(this.currentValue + addend);
  },
  
  minus(subtrahend) {
    this.setValue(this.currentValue - subtrahend);
  }
};
// Using the calculator
betaCalc.setValue(3); // =&gt; 3
betaCalc.plus(3);     // =&gt; 6
betaCalc.minus(2);    // =&gt; 4</code></pre>



<p>We’re defining our calculator as an object-literal to keep things simple. The calculator works by printing its result via <code>console.log</code>.</p>



<p>Functionality is really limited right now. We have a <code>setValue</code> method, which takes a number and displays it on the “screen.” We also have <code>plus</code> and <code>minus</code> methods, which will perform an operation on the currently displayed value.</p>



<p>It’s time to add more functionality. Let’s start by creating a plugin system.</p>


<h3 class="wp-block-heading" id="the-worlds-smallest-plugin-system"><a href="#aa-the-worlds-smallest-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-the-worlds-smallest-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>The world’s smallest plugin system</h3>


<p>We’ll start by creating a <code>register</code> method that other developers can use to register a plugin with BetaCalc. The job of this method is simple: take the external plugin, grab its <code>exec</code> function, and attach it to our calculator as a new method:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  // ...other calculator code up here
  register(plugin) {
    const { name, exec } = plugin;
    this[name] = exec;
  }
};</code></pre>



<p>And here’s an example plugin, which gives our calculator a “squared” button:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// Define the plugin
const squaredPlugin = {
  name: 'squared',
  exec: function() {
    this.setValue(this.currentValue * this.currentValue)
  }
};
// Register the plugin
betaCalc.register(squaredPlugin);</code></pre>



<p>In many plugin systems, it’s common for plugins to have two parts:</p>



<ol><li>Code to be executed</li><li>Metadata (like a name, description, version number, dependencies, etc.)</li></ol>



<p>In our plugin, the <code>exec</code> function contains our code, and the <code>name</code> is our metadata. When the plugin is registered, the exec function is attached directly to our <code>betaCalc</code> object as a method, giving it access to BetaCalc’s <code>this</code>.</p>



<p>So now, BetaCalc has a new “squared” button, which can be called directly:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">betaCalc.setValue(3); // =&gt; 3
betaCalc.plus(2);     // =&gt; 5
betaCalc.squared();   // =&gt; 25
betaCalc.squared();   // =&gt; 625</code></pre>



<p>There’s a lot to like about this system. The plugin is a simple object-literal that can be passed into our function. This means that plugins can be downloaded via npm and imported as ES6 modules. Easy distribution is super important!</p>



<p>But our system has a few flaws.</p>



<p>By giving plugins access to BetaCalc’s <code>this</code>, they get read/write access to all of BetaCalc’s code. While this is useful for getting and setting the <code>currentValue</code>, it’s also dangerous. If a plugin was to redefine an internal function (like <code>setValue</code>), it could produce unexpected results for BetaCalc and other plugins. This violates <a href="https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle" rel="noopener">the open-closed principle</a>, which states that a software entity should be open for extension but closed for modification.</p>



<p>Also, the “squared” function works by producing <a href="https://en.wikipedia.org/wiki/Side_effect_(computer_science)" rel="nofollow noopener">side effects</a>. That’s not uncommon in JavaScript, but it doesn’t feel great — especially when other plugins could be in there messing with the same internal state. A more <a href="https://en.wikipedia.org/wiki/Functional_programming" rel="nofollow noopener">functional</a> approach would go a long way toward making our system safer and more predictable.</p>


<h3 class="wp-block-heading" id="a-better-plugin-architecture"><a href="#aa-a-better-plugin-architecture" aria-hidden="true" class="aal_anchor" id="aa-a-better-plugin-architecture"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>A better plugin architecture</h3>


<p>Let’s take another pass at a better plugin architecture. This next example changes both the calculator and its plugin API:</p>



<pre rel="JavaScript" class="wp-block-csstricks-code-block language-javascript" data-line=""><code markup="tt">// The Calculator
const betaCalc = {
  currentValue: 0,
  
  setValue(value) {
    this.currentValue = value;
    console.log(this.currentValue);
  },
 
  core: {
    'plus': (currentVal, addend) =&gt; currentVal + addend,
    'minus': (currentVal, subtrahend) =&gt; currentVal - subtrahend
  },
  plugins: {},    
  press(buttonName, newVal) {
    const func = this.core[buttonName] || this.plugins[buttonName];
    this.setValue(func(this.currentValue, newVal));
  },
  register(plugin) {
    const { name, exec } = plugin;
    this.plugins[name] = exec;
  }
};
  
// Our Plugin
const squaredPlugin = { 
  name: 'squared',
  exec: function(currentValue) {
    return currentValue * currentValue;
  }
};
betaCalc.register(squaredPlugin);
// Using the calculator
betaCalc.setValue(3);      // =&gt; 3
betaCalc.press('plus', 2); // =&gt; 5
betaCalc.press('squared'); // =&gt; 25
betaCalc.press('squared'); // =&gt; 625</code></pre>



<p>We’ve got a few notable changes here.</p>



<p>First, we’ve separated the plugins from “core” calculator methods (like <code>plus</code> and <code>minus</code>), by putting them in their own plugins object. Storing our plugins in a <code>plugin</code> object makes our system safer. Now plugins accessing this can’t see the BetaCalc properties — they can only see properties of <code>betaCalc.plugins</code>.</p>



<p>Second, we’ve implemented a <code>press</code> method, which looks up the button’s function by name and then calls it. Now when we call a plugin’s <code>exec</code> function, we pass it the current calculator value (<code>currentValue</code>), and we expect it to return the new calculator value.</p>



<p>Essentially, this new <code>press</code> method converts all of our calculator buttons into <a href="https://en.wikipedia.org/wiki/Pure_function" rel="nofollow noopener">pure functions</a>. They take a value, perform an operation, and return the result. This has a lot of benefits:</p>



<ul><li>It simplifies the API.</li><li>It makes testing easier (for both BetaCalc and the plugins themselves).</li><li>It reduces the dependencies of our system, making it more <a href="https://en.wikipedia.org/wiki/Loose_coupling" rel="noopener">loosely coupled</a>.</li></ul>



<p>This new architecture is more limited than the first example, but in a good way. We’ve essentially put up guardrails for plugin authors, restricting them to <a href="https://www.bryanbraun.com/2015/02/16/on-designing-great-systems/" rel="noopener">only the kind of changes that we want them to make</a>.</p>



<p>In fact, it might be too restrictive! Now our calculator plugins can only do operations on the <code>currentValue</code>. If a plugin author wanted to add advanced functionality like a “memory” button or a way to track history, they wouldn’t be able to.</p>



<p>Maybe that’s ok. The amount of power you give plugin authors is a delicate balance. Giving them too much power could impact the stability of your project. But giving them too little power makes it hard for them to solve their problems — in that case you might as well not have plugins.</p>


<h3 class="wp-block-heading" id="what-more-could-we-do"><a href="#aa-what-more-could-we-do" aria-hidden="true" class="aal_anchor" id="aa-what-more-could-we-do"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>What more could we do?</h3>


<p>There’s a lot more we could do to improve our system.</p>



<p>We could add error handling to notify plugin authors if they forget to define a name or return a value. It’s good to think like a QA dev and imagine how our system could break so we can proactively handle those cases.</p>



<p>We could expand the scope of what a plugin can do. Currently, a BetaCalc plugin can add a button. But what if it could also register callbacks for certain lifecycle events — like when the calculator is about to display a value? Or what if there was a dedicated place for it to store a piece of state across multiple interactions? Would that open up some new use cases?</p>



<p>We could also expand plugin registration. What if a plugin could be registered with some initial settings? Could that make the plugins more flexible? What if a plugin author wanted to register a whole suite of buttons instead of a single one — like a “BetaCalc Statistics Pack”? What changes would be needed to support that?</p>


<h3 class="wp-block-heading" id="your-plugin-system"><a href="#aa-your-plugin-system" aria-hidden="true" class="aal_anchor" id="aa-your-plugin-system"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Your plugin system</h3>


<p>Both BetaCalc and its plugin system are deliberately simple. If your project is larger, then you’ll want to explore some other plugin architectures.</p>



<p>One good place to start is to look at existing projects for examples of successful plugin systems. For JavaScript, that could mean <a href="https://learn.jquery.com/plugins/basic-plugin-creation/" rel="nofollow noopener">jQuery</a>, <a href="https://www.gatsbyjs.org/docs/creating-plugins/" rel="nofollow noopener">Gatsby</a>, <a href="https://github.com/d3/d3/wiki/Plugins" rel="nofollow noopener">D3</a>, <a href="https://ckeditor.com/docs/ckeditor4/latest/guide/plugin_sdk_intro.html" rel="nofollow noopener">CKEditor</a>, or others.</p>



<p>You may also want to be familiar with various <a href="https://seesparkbox.com/foundry/javascript_design_patterns" rel="noopener">JavaScript design patterns</a>. (Addy Osmani <a href="https://addyosmani.com/resources/essentialjsdesignpatterns/book/" rel="noopener">has a book</a> on the subject.)  Each pattern provides a different interface and degree of coupling, which gives you a lot of good plugin architecture options to choose from. Being aware of these options helps you better balance the needs of everyone who uses your project.</p>



<p>Besides the patterns themselves, there’s a lot of good software development principles you can draw on to make these kinds of decisions. I’ve mentioned a few along the way (like the open-closed principle and loose coupling), but some other relevant ones include the <a href="http://wiki.c2.com/?LawOfDemeter" rel="nofollow noopener">Law of Demeter</a> and <a href="https://www.martinfowler.com/articles/injection.html" rel="noopener">dependency injection</a>.</p>



<p>I know it sounds like a lot, but you’ve gotta do your research. Nothing is more painful than making everyone rewrite their plugins because you needed to change the plugin architecture. It’s a quick way to lose trust and discourage people from contributing in the future.</p>


<h3 class="wp-block-heading" id="conclusion"><a href="#aa-conclusion" aria-hidden="true" class="aal_anchor" id="aa-conclusion"><svg aria-hidden="true" class="aal_svg" version="1.1" viewbox="0 0 16 16"><path fill-rule="evenodd" d="M4 9h1v1H4c-1.5 0-3-1.69-3-3.5S2.55 3 4 3h4c1.45 0 3 1.69 3 3.5 0 1.41-.91 2.72-2 3.25V8.59c.58-.45 1-1.27 1-2.09C10 5.22 8.98 4 8 4H4c-.98 0-2 1.22-2 2.5S3 9 4 9zm9-3h-1v1h1c1 0 2 1.22 2 2.5S13.98 12 13 12H9c-.98 0-2-1.22-2-2.5 0-.83.42-1.64 1-2.09V6.25c-1.09.53-2 1.84-2 3.25C6 11.31 7.55 13 9 13h4c1.45 0 3-1.69 3-3.5S14.5 6 13 6z"></path></svg></a>Conclusion</h3>


<p>Writing a good plugin architecture from scratch is difficult! You have to balance a lot of considerations to build a system that meets everyone’s needs. Is it simple enough? Powerful enough? Will it work long term?</p>



<p>It’s worth the effort though. Having a good plugin system helps everyone. Developers get the freedom to solve their problems. End users get a large number of opt-in features to choose from. And you get to grow an ecosystem and community around your project. It’s a win-win-win situation.</p>

+ 4
- 0
cache/2024/index.html View File

@@ -108,6 +108,8 @@
<li><a href="/david/cache/2024/ba977526c7a8cab6935708b2cdba5c0c/" title="Accès à l’article dans le cache local : Aging programmer">Aging programmer</a> (<a href="https://world.hey.com/jorge/aging-programmer-d448bdec" title="Accès à l’article original distant : Aging programmer">original</a>)</li>
<li><a href="/david/cache/2024/0cc2e9c6b29f8326b2ff628f64e22888/" title="Accès à l’article dans le cache local : Proposal: CSS Variable Groups">Proposal: CSS Variable Groups</a> (<a href="https://lea.verou.me/docs/var-groups/" title="Accès à l’article original distant : Proposal: CSS Variable Groups">original</a>)</li>
<li><a href="/david/cache/2024/ce5fdc61fd66cdb9ce548fb543eba986/" title="Accès à l’article dans le cache local : Unsigned Commits">Unsigned Commits</a> (<a href="https://blog.glyph.im/2024/01/unsigned-commits.html" title="Accès à l’article original distant : Unsigned Commits">original</a>)</li>
<li><a href="/david/cache/2024/5030196507bcf3e06162e9eaed40abbe/" title="Accès à l’article dans le cache local : Blogging and Composting">Blogging and Composting</a> (<a href="https://blog.jim-nielsen.com/2023/blogging-and-compositing/" title="Accès à l’article original distant : Blogging and Composting">original</a>)</li>
@@ -224,6 +226,8 @@
<li><a href="/david/cache/2024/55477786fc56b6fc37bb97231b634d90/" title="Accès à l’article dans le cache local : Fabrique : concept">Fabrique : concept</a> (<a href="https://www.quaternum.net/2023/06/02/fabrique-concept/" title="Accès à l’article original distant : Fabrique : concept">original</a>)</li>
<li><a href="/david/cache/2024/d9c30865dde8c88394ba054836a18ae3/" title="Accès à l’article dans le cache local : Designing a JavaScript Plugin System">Designing a JavaScript Plugin System</a> (<a href="https://css-tricks.com/designing-a-javascript-plugin-system/" title="Accès à l’article original distant : Designing a JavaScript Plugin System">original</a>)</li>
<li><a href="/david/cache/2024/6b26bff7f4772cf8fb78878ff4f9594f/" title="Accès à l’article dans le cache local : command center: Simplicity">command center: Simplicity</a> (<a href="https://commandcenter.blogspot.com/2023/12/simplicity.html" title="Accès à l’article original distant : command center: Simplicity">original</a>)</li>
<li><a href="/david/cache/2024/2ad967b8fc35e160fa8e6c1d2a3b4734/" title="Accès à l’article dans le cache local : If Architects had to work like Programmers">If Architects had to work like Programmers</a> (<a href="http://www.gksoft.com/a/fun/architects.html" title="Accès à l’article original distant : If Architects had to work like Programmers">original</a>)</li>

Loading…
Cancel
Save