-
Notifications
You must be signed in to change notification settings - Fork 29
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Ability to group CSS Variables under multiple selectors #227
Comments
Admittedly this is quite difficult to do without a lot of knowledge about style-dictionary, and using its internal functions. Before I show how you might achieve this, I would challenge you a bit. In my opinion, combining all different theme options in a single stylesheet is not ideal from the perspective of performance. You'll have your end user load the entire stylesheet for all your themes even though they are only using/showing one theme, so that's a lot of unnecessary kb's over the wire. My recommendation is that you output a separate CSS file for each theme possibility, and dynamically load the correct stylesheet based on the end user's theme preference. This makes initial load much faster, with only a small delay upon a live theme switch by the user. Example of this approach, with multi-dimensional theming (so many options)) https://github.com/tokens-studio/lion-example -> live demo Furthermore, the way Tokens Studio approaches theming right now is that you do a completely separate run of style-dictionary for each theme, so then combining that with putting everything into a single file is a bit hard to accomplish, because you have to manually do those style-dictionary runs, only partially, and extract the tokens and format them into a single document yourself, this requires a fair bit of style-dictionary knowledge and javascript chops to accomplish, but here it goes (assuming one dimensional themes btw, but possible to extend to multi-dimensional theming as well).
const { registerTransforms } = require('@tokens-studio/sd-transforms');
const StyleDictionary = require('style-dictionary');
// private API, not recommended atm, probably will change in style-dictionary v4
const createDictionary = require('style-dictionary/lib/utils/createDictionary.js');
const { promises } = require('fs');
registerTransforms(StyleDictionary, {
/* options here if needed */
});
const { formatHelpers } = StyleDictionary;
const { fileHeader, formattedVariables } = formatHelpers;
async function run() {
const $themes = JSON.parse(await promises.readFile('$themes.json', 'utf-8'));
const configs = $themes.map(theme => [theme, ({
source: Object.entries(theme.selectedTokenSets)
.filter(([, val]) => val !== 'disabled')
.map(([tokenset]) => `${tokenset}.json`),
platforms: {
css: {
transformGroup: 'tokens-studio',
},
},
})]);
// Now we need to start gathering all the tokens, source tokens and theme specific tokens
// so that at the end, we can write everything to a single CSS file under seperate selectors.
let fileContent = fileHeader({});
const sourceTokens = [];
const themeSpecificTokens = {};
configs.forEach(([theme, cfg]) => {
themeSpecificTokens[theme.name] = [];
const sd = StyleDictionary.extend(cfg);
sd.cleanAllPlatforms(); // optionally, cleanup files first..
const exportedTokens = sd.exportPlatform('css');
const dictionary = createDictionary({ properties: exportedTokens });
dictionary.allTokens.forEach(token => {
const filePath = token.filePath.replace(/\.json$/g, '');
if (theme.selectedTokenSets[filePath] === 'source') {
// source tokens should go into :root selector
sourceTokens.push(token);
} else if (theme.selectedTokenSets[filePath] === 'enabled') {
// theme specific tokens should go into .theme-foo selector
themeSpecificTokens[theme.name].push(token);
}
});
});
// Create the CSS variables, reuse stuff from StyleDictionary, but this is a bit difficult right now due to
// `usesReference` and `getReferences` utilities being hard-coupled/bound to dictionary object rather
// than pure reusable functions. This should be made easier in style-dictionary, to reuse them.. maybe in v4!
// Should still work if you don't use outputReferences (so, set to `false`) though.
fileContent += `:root {\n`;
fileContent += formattedVariables({ format: 'css', dictionary: { allTokens: sourceTokens }, outputReferences: false });
fileContent += `\n}\n\n`;
Object.entries(themeSpecificTokens).forEach(([themeName, tokens]) => {
fileContent += `.theme-${themeName} {\n`;
fileContent += formattedVariables({ format: 'css', dictionary: { allTokens: tokens }, outputReferences: false });
fileContent += `\n}\n\n`;
});
await promises.writeFile('output.css' , fileContent, 'utf-8');
}
run(); Can be ran with the following tokenset, for me this gives the correct output.css file: Tested with:
|
Thank you @jorenbroekema for a detailed response.
I would much rather do this! I was doing the single CSS approach just because that's what the multi-dimensional theming example did in the README. Perhaps the lion example should also be mentioned in the README with the tradeoffs. Thanks you, again! |
@jorenbroekema, actually my comment above is confusing. I was under the impression that you were suggesting a separate CSS file per theme dimension, e.g. Now this is exactly what the example in the README does too. That code is actually a bit simpler. Is the lion example trying to do anything different that I am missing? |
So, the examples in the README indeed create a CSS file per theme combination. If you use single-dimensional theming and not multi, then it's a CSS file per theme (there is no concept of theme combinations in this case). It's in line with the Lion example, but the code in the lion example is a bit more complex because there I also make heavy use of splitting files further using Style-Dictionary filters, to ensure that my component tokens output end up in the component folders in my design system repository. So all the button related tokens go into |
Aha, that makes it crystal clear!
My use case matches the first bullet above, so I will follow the README example. As far as README goes, I think the following content should be added: Themes: complete example section:
Multi-dimensional Theming section:
Also add a general comment at the top that it is better to create standalone CSS files per theme combination instead of mixing up themes in a single file. In addition, add a README to the lion example:
|
No, Lion also uses multi-dimensional theming, it has two dimensions, brand and color.
Can you share what the issue is that you ran into and which version of NPM and NodeJS you're using? I agree with your suggestions to improve the README's. If you'd like to contribute it, would be highly appreciated, I'll tag this issue as documentation enhancement in the meantime. |
Hi @jorenbroekema, I will submit a PR to update the README here. As for the lion example, I am running node 18.16.0 & npm 9.5.1. Simply running
However deleting |
One more question for you @jorenbroekema: In the lion example, you extend all components using Do you have a good example of dynamically loading stylesheets for Tailwind? In this case I assume that the tailwind config remains unchanged but only the CSS Variables need to be switched at the // src/themes/utils.ts
import { themes } from './index';
...
export const applyTheme = (theme: string): void => {
const themeObject: IMappedTheme = mapTheme(themes[theme]);
if (!themeObject) return;
const root = document.documentElement;
Object.keys(themeObject).forEach((property) => {
if (property === 'name') {
return;
}
root.style.setProperty(property, themeObject[property]);
});
}; |
This is fixed now, I pushed a new commit. Rollup went to v4, and one of the plugins still had compatibility only for 1, 2 and 3, they updated this in their latest patch though so after bumping it, the error goes away.
I don't personally use tailwind so I don't really have an example or an answer to your question. As far as I know however, you provide a tailwind config as a JS file and you run the tool to create a CSS file which is then consumed by your app. You could probably make it create a tailwind CSS output file for each theme, and adopt the same strategy as I do in lion-example of dynamically switching the stylesheets when the theme changes. Note that you would need to check that you only switch the tailwind stylesheets that contain theme-specific stuff, so all the tailwind utility classes that don't change, try not to have those reload when you switch themes, that would be a lot of unnecessary kilobytes over the wire for the end user. Not sure if this is easy to do with tailwind, splitting theme specific stuff from tailwind core stuff. |
What feature would you like?
I would like to generate CSS Variables so that they are grouped under 3 selectors:
However a configuration like the one below, puts everything under
:root
:I know that
file
has a option calledselector
, but is it possible to apply it selectively to source files?Would you be available to contribute this feature?
The text was updated successfully, but these errors were encountered: