-
Notifications
You must be signed in to change notification settings - Fork 253
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
[Feature] Dynamic variants #721
Conversation
This pull request is automatically built and testable in CodeSandbox. To see build info of the built libraries, click here or the icon next to each commit SHA. Latest deployment of this branch, based on commit 7e656fc:
|
I cant wait to dig into this. We'll do so after v1. 🔥 |
…stitches into feature/dynamic-functions
It looks like there are still some conflicts to work out. Starting with a clean copy of |
Thanks for the help @jonathantneal, really helped with the typings! Okay so we finally got the typings to work properly, now I just have to ensure the tests run perfectly. Well, I have a discussion with myself about the arguments of a function, but I realize this also includes utility functions. variants: {
size: (value: Stitches.ScaleValue<'space'>) => ({
width: value,
height: value
})
} This should result in a component with the prop variants: {
translate: (x: number, y: number) => ({
translate: `${x}px, ${y}px`
})
} Currently, the typings only support the first argument as prop value (this also applies to utils), but should we look into allowing for multiple arguments through It will not be a part of this pull request, but a general question about the future of utility functions and dynamic variants. |
Just started getting my feet wet with Stitches, coming from a rather large emotion + styled-system codebase. One of the first things I was also looking for was a way to map variants to a whole scale of theme tokens or valid set of css properties without having to manually map every individual value so that we could easily port over our primitives. As far as I can tell there is currently no way to achieve this, which would make this a rather cumbersome migration (and possibly also maintenance) effort. edit: I guess that mapping the theme tokens could be accomplished with some utils that generate variants out of a theme definition, like: type SpaceScale = 's00' | 's01' | 's02' | 's03' | 's04';
type SpaceTheme = Record<SpaceScale, number | string>;
const space: SpaceTheme = {
s00: 0,
s01: '0.25rem',
s02: '0.5rem',
s03: '0.75rem',
s04: '1rem'
};
type SpaceVariant = Record<SpaceScale, CSSProperties>;
const createGapVariant = (theme: SpaceTheme): SpaceVariant =>
Object.keys(theme).reduce(
(acc, key) => ({ ...acc, [key]: { gap: `$${key}` } }),
{} as SpaceVariant
);
const Grid = styled('div', {
display: 'grid',
variants: {
gap: createGapVariant(space)
}
}); Which should have this as output: const Grid = styled('div', {
display: 'grid',
variants: {
gap: {
s00: { gap: '$s00' },
s01: { gap: '$s01' },
s02: { gap: '$s02' },
s03: { gap: '$s03' },
s04: { gap: '$s04' }
}
}
}); Just not sure how well that'd play with TypeScript, suppose it should be fine. Could be turned into a generic function for some reusability as well. Very roughly something like: const { styled, css, config, theme } = createStitches({ ... });
type CSS = Stitches.CSS<typeof config>;
type Theme = typeof theme;
type TokenByScaleName<ScaleName extends keyof Theme> = Prefixed<'$', keyof Theme[ScaleName]>;
type ScaleVariant<ScaleName extends keyof Theme> = Record<keyof Theme[ScaleName], CSS>;
type GetCss<ScaleName extends keyof Theme> = (token: TokenByScaleName<ScaleName>) => CSS;
function createScaleVariant<ScaleName extends keyof Theme>(
scaleName: ScaleName,
getCss: GetCss<ScaleName>
): ScaleVariant<ScaleName> {
return Object.keys(theme[scaleName]).reduce(
(acc, key) => ({ ...acc, [key]: getCss(`$${key}` as TokenByScaleName<ScaleName>) }),
{} as any
);
} Which can then be used like this: export const Box = styled('div', {
boxSizing: 'border-box',
variants: {
mb: createScaleVariant('space', token => ({ marginBottom: token })),
ml: createScaleVariant('space', token => ({ marginLeft: token })),
mr: createScaleVariant('space', token => ({ marginRight: token })),
mt: createScaleVariant('space', token => ({ marginTop: token })),
mx: createScaleVariant('space', token => ({ marginLeft: token, marginRight: token })),
my: createScaleVariant('space', token => ({ marginBottom: token, marginTop: token })),
pb: createScaleVariant('space', token => ({ paddingBottom: token })),
pl: createScaleVariant('space', token => ({ paddingLeft: token })),
pr: createScaleVariant('space', token => ({ paddingRight: token })),
pt: createScaleVariant('space', token => ({ paddingTop: token })),
px: createScaleVariant('space', token => ({ paddingLeft: token, paddingRight: token })),
py: createScaleVariant('space', token => ({ paddingBottom: token, paddingTop: token }))
}
});
<Box mx="s04" py="s02">...</Box> Or am I missing something here and is this really not the way Stitches is intended to be used? |
@pleunv replied here #816 (comment) |
Any update here? Is this being considered? Thanks! |
Yeah, it is being considered but unfortunately we're currently working on other issues related to injection order, and they're higher in our priority list. I'll keep this updated as I have more updates |
Very, very keen to have this baked in. I understand the issue with performance, but it is probably solvable. I am also using a variant of |
@ch99q, the package contributions have stopped, any updates on when this feature will be released? |
Waiting to see what the stitiches team say, but they seem to look after alternatives |
@darklight9811 we're currently working on a relatively big feature related to injection order. Nothing will happen until that's done. Sorry, but we need to prioritise what's more important. Dynamic variants was not in the Stitches spec on purpose. This is not to say we're not opened to it. It will be considered with an open mind. |
@peduarte. I'm not complaining about prioritizing what is important. I'm just concerned that the main branch has not received any updates yet and there is no clear roadmap of features to be implemented or bugs to be fixed, I'm afraid of this package being deprecated. But about the dynamic variants, I'm really interested in them because I need them in my project, using static variants would increase file sizes considerably (about 50-60%), so I really wanted them (besides I would like to use runtime color transformations so I wouldn't have to serialize every value beforehand). |
I think the maintainers have been pretty clear regarding this particular request and also regarding the status of the project and the process for requesting and discussing new functionality.
No need to jump to conclusions and spread FUD just because people haven't been pushing code for a few weeks. Stitches just had its first major release and there's plenty of activity on issues and twitter. Give them a break. |
Actually, I think this is not very import. Using css and utils partially solves the problem.
If there is a function, it would be simple.. |
@Tatamethues thats similar to what I did, but that is not really readable and can end up creating or really noisy code or unscalable code. And it becomes hardcoded in the bundle file, that can really end up increasing it. |
It's a little hacky, but if it's just a type NumberRange<T extends number> = number extends T ? number : InnerNumberRange<T, []>;
type InnerNumberRange<T extends number, R extends unknown[]> = R['length'] extends T
? R['length']
: InnerNumberRange<T, [T, ...R]> | R['length'];
type RangeVariant<Length extends number> = Record<NumberRange<Length>, CSS>;
export function createRangeVariant<Length extends number>(
length: Length,
get: (index: number) => CSS
): RangeVariant<Length> {
return new Array(length).fill(0).reduce((acc, _, index) => {
acc[index] = get(index);
return acc;
}, {} as any);
} Based on this SO answer. Can probably be done better though. The difficulty here is creating a dynamic number range, which TypeScript can't really do well. export const Box = styled('div', {
boxSizing: 'border-box',
variants: {
col: createRangeVariant(12, index => ({ flex: '0 0 auto', width: (100 / 12) * index }))
}
}); and |
Here's how I solved a similar problem for creating a component that can act as having a dynamic variant. You can do more powerful stuff that isn't just mapping 1:1 to a theme scale. tl;dr import "./styles.css";
import { styled, theme } from "./theme";
export const CoreVStack = styled("div", {
"> * + *": {
marginTop: "$$vStackSpace"
}
});
export const VStack = ({
space,
...props
}: {
space: keyof typeof theme["space"];
children: React.ReactNode;
}) => {
return <CoreVStack css={{ $$vStackSpace: theme.space[space] }} {...props} />;
};
export default function App() {
return (
<VStack space="5">
{Array.from({ length: 10 }, (_, i) => i + 1).map((index) => (
<div key={index}>Item {index}</div>
))}
</VStack>
);
} Codehttps://codesandbox.io/s/stitches-dyanamic-variant-6hirg?file=/src/App.tsx:23-63 |
@emadabdulrahim I'm not pro to typescript. That's why I used the silliest way. Thanks for your solution |
@emadabdulrahim This is the same pattern I came to before realizing there was a request for dynamic variants. I feel like having this in the docs as a recipe would be great. While shorthand could be nice, I don't personally feel like I'm missing anything from stitches using static objects as long as I can use my token definitions as types. |
So I was struggling with this kind of usage a bit at first; but since I'm creating a whole design system, with extensive theme tokens (including over 30 size/spacing tokens), I was ultimately able to use variants to cover most cases, without requiring this kind of runtime-centric solution. For instance, here is a bit of code I'm producing, via Stitches-powered primitives (NOTE: my variants are based on <Row alignItems="center">
<Box h="7" w="6" bg="primary9" radiusLeft="4" mr="px" />
<Box h="7" w="6" bg="secondary9" radiusRight="4" mr="3" />
<Heading bold flat h="7" mb="1" color="secondary9">
{title}
</Heading>
</Row> I've created some helper functions to help me generate sequences of variants, without writing them all out, but having them fully typed — e.g.: // Produces a sequence of 12 numbered fontSize variants, starting at 1
fontSize: generateVariantSequence<Size>(12, (n: number) => ({
fontSize: `$${n}`,
})), You can even extend it in some ways that really simplify some complex cases. For instance, I created a <Grid columnFit="11" p="5" radius="2">
{keys.map((key: number) => (
<GridItem
key={key}
bg={`primary${key as ColorNumberKey}`}
color={`textPrimary${key as ColorNumberKey}`}
p="4"
>
{inner}
</GridItem>
))}
{keys.map((key: number) => (
<GridItem
key={key}
bg={`secondary${key as ColorNumberKey}`}
color={`textSecondary${key as ColorNumberKey}`}
p="4"
>
{inner}
</GridItem>
))}
</Grid> The heavy lifting columnFill: generateVariantSequence<FillVariant>(
15,
(n: number) => ({ gridTemplateColumns: `repeat(auto-fill, minmax($sizes$${n}, 1fr))` }),
4
), If you like the DX of something like Chakra, but want the performance and customizability of Stitches, I think this is a great solution. Between something like what I'm doing, and the creation of your own primitives — which can map additional dynamic props to One might argue and say, "well, you don't need to use it, even if they put it in", which is true, but...
Anyhow, that's my 2 cents. |
@LucasUnplugged really nice examples, would you mind sharing a codesandbox with the implementation of |
Yeah, I'd be happy to! I'll post here once it's ready. |
As promised, here's a sandbox showcasing some of what I mentioned above; it's not a super complex example, and the typing isn't perfect (I spent quite a while on this in my design system), but it should do the trick: |
I followed @emadabdulrahim 's idea and created this demo, though I'm not sure it's a good idea as it will generate a huge style for each component, as I have to use |
const { config } = createStitches({
theme: {
colors: {
primary: 'red',
},
space: {
1: '',
2: '',
3: '',
},
},
});
type CSS = Stitches.CSS<typeof config>;
export const getVariant = <
P extends keyof typeof config.theme,
T extends keyof typeof config.theme[P],
R extends Record<T, CSS>,
>(
prop: P,
map: (tokenValue: `$${T}`) => CSS,
): R => {
const values = Object.keys(config.theme[prop]) as T[];
return values.reduce<R>(
(acc, tokenValue) => ({ ...acc, [tokenValue]: map(`$${tokenValue}`) }),
{} as R,
);
};
const Flex = styled('div', {
boxSizing: 'border-box',
display: 'flex',
variants: {
color: getVariant('colors', (tokenValue) => ({
color: tokenValue,
})),
gap: getVariant('space', (tokenValue) => ({
gap: tokenValue,
})),
},
});
// <Flex color="primary" gap={2} /> |
@elsangedy seems like TypeScript has some issues with your example: https://codesandbox.io/s/priceless-shape-6i7ku?file=/src/App.tsx ❤️ the solution by the way |
This was mt approach import "./styles.css";
import type * as Stitches from "@stitches/react";
import { styled, theme, CSS, css } from "./stitches.config";
const { colors } = theme;
type TColors = {
[K in keyof typeof colors]: { $$color: string };
};
const ColorsVariants = css({
variants: {
color: {
inherit: {
$$color: "currentColor"
},
...Object.keys(colors).reduce(
(prev, curr) => ({ ...prev, [curr]: { $$color: "$colors$" + curr } }),
{} as TColors
)
}
}
});
const SvgIcon = styled(
"svg",
{
$$size: "1em",
$$color: "$colors$black",
lineHeight: "1em",
verticalAlign: "middle",
width: "$$size",
height: "$$size",
// here we actually select the path data with css
"& path": {
stroke: "$$color"
},
variants: {
size: {
xs: {
$$size: "10px"
},
xl: {
$$size: "$sizes$xl"
}
}
}
},
ColorsVariants
);
type TSvgIcon = {
css?: CSS;
};
const MyCoolIcon = (
props: TSvgIcon & Stitches.VariantProps<typeof SvgIcon>
) => (
<SvgIcon
viewBox="0 0 10 10"
fill="none"
xmlns="http://www.w3.org/2000/svg"
{...props}
>
<path
d="M8.74997 1.25L1.25 8.74997M1.25 8.74997L8.75 8.75M1.25 8.74997L1.25003 1.25003"
stroke="currentColor"
strokeLinecap="round"
strokeLinejoin="round"
/>
</SvgIcon>
);
export default function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
<MyCoolIcon size="xs" color={{ "@xs": "inherit", "@sm": "red" }} />
</div>
);
} https://codesandbox.io/s/thirsty-satoshi-bbggf?file=/src/App.tsx:0-1652 I will try yours because seems more clever in matter to type the key of the locale token |
@ch99q Thanks for all of your hard work here. I will be closing this for now but if anyone else have some more examples on dynamic values/ variants, post them in the issue that I linked as I will be creating a recipes section to document such usages |
in case anyone is interested in this, I did a little experiment to showcase how to handle highly dynamic variants using css custom properties and inline styles. you can check it out here |
This introduces a new set of features in stitches called dynamic variants.
The purpose of this is to allow dynamic props to apply CSS values at static, server, and client-side.
Example of dynamic spacer component.