linaria
linaria copied to clipboard
Modifier classes
Do you want to request a feature or report a bug?
feature
What is the expected behavior?
To have the same modifiers syntax as in astroturf
const Button = styled('button')`
color: black;
border: 1px solid black;
background-color: white;
&.primary {
color: blue;
border: 1px solid blue;
}
&.color-green {
color: green;
}
`;
<Button primary color="green">
Compare to ${ } interpolation it keeps CSS syntax. CSS highlight and autocomplete works.
@thymikee @zamotany thoughts?
I think it introduces a level of indirection and complicates stuff. I'm not a fan.
@thymikee is it OK to simplify a lot of user’s code but increase complexity in a framework. Is how all features ware made?
Why you are not a fan of it?
I mean I'm not a fan of taking arbitrary props and converting them implicitly to be used by CSS. What about prop name clashes if we wrap custom components with styled()?
But I must say that not having interpolations seems nice, especially when converting regular CSS to JS.
What about prop name clashes if we wrap custom components with styled()?
Can you show a small example? I am thinking that I was worry about it too, but not sure that we are speaking about the same.
For example, say:
const Wrapper = styled(MyComponent)`
&.primary {
color: blue;
border: 1px solid blue;
}
`;
<Wrapper primary />
The primary prop is also going to be passed to MyComponent unless we filter it out. But we don't know supported props, so not possible filter it out.
Wondering something like this is a good idea:
<Wrapper {...mx({ primary: true, color: 'green' })} />
Or maybe the user can use this package: https://npmjs.com/package/classnames
I'm not opposed to the idea, but probably need to think a bit more to avoid the edge cases.
Was about to write the same, but Satya 🤖 was quicker!
@satya164 why we don’t know about supported props?
We have styled object from CSS Modules and can look into classes. astroturf does the same to detect what properties should be used as style modifiers. https://github.com/4Catalyzer/astroturf/blob/master/src/runtime/styled.js#L23-L25
@ai I was talking about the props that are accepted by MyComponent (in above case). We don't know if a prop is used only for styling or is also a regular prop. If the user uses a prop only for styling and later someone else adds that prop to the component without checking the callsites, it might change the behaviour.
It's also a problem with property interpolation though, so not specific to this approach. I'm thinking if there is a way to distinguish style only props. Maybe instead of primary, the user can write $primary, and we know for sure that it's a prop only used for styling, and we filter them out.
@satya164
Is styles the list of styles applied to the component and astroturf keeps them in the bundle?
Yeap, styles is that object from CSS Modules { list: 'component_list_hb4h5h45' }. We need it anyway when we use styled().
I was talking about the props that are accepted by MyComponent (in above case).
We can have the agreement that if prop is defined by class in styled(), it will not be passed down. It is pretty expected behaviour. If you declare prop as a class in styles, it definitely stops to be a just regular prop.
Also, what do you think about data- attributes? This is already supported:
const Box = styled.div`
/* some css */
&[data-primary] {
border: 2px solid blue;
}
&[data-color=green] {
color: green;
}
`;
<Box data-primary data-color="green" />
data-attributes will be DOM- It requires more symbols
- It is also a little hacky.
data-attributes were created for a different purpose. People will not love them, as result it will not be syntax sugar feature for linaria as it works forastroturf.
Makes sense. We'll think about it a bit more then.
Any reason you don't like following:
<Box $primary $color="green" />
One extra character, but I prefer to be explicit rather than assuming which properties shouldn't be passed.
$primary is OK if we will have a rational reason to not use styles object to filter props
@ai currently linaria doesn't use css-modules, just normal css-loader, so we don't have a styles object. it can be implemented though. though the reason I prefer $primary is because it's explicit that it's for styling, and not a regular prop/attribute. but maybe that's unnecessary. I just need to think a bit more :D
currently linaria doesn't use css-modules
How do you scope keyframes? Or what if I don’t want to scope class (:global() in CSS or injectGlobal in SC)?
Did I understand correctly that right now you collect all styles to the single file styles.css? How code splitting works?
How do you scope keyframes? Or what if I don’t want to scope class
Currently, we use stylis as the parser (though planning to switch to postcss for few reasons) which handles scoped keyframes as well as :global.
Did I understand correctly that right now you collect all styles to the single file styles.css? How code splitting works?
We create separate CSS files for each JS file and add a require statement, which gets picked up by css loader. Code splitting works similar to when you import vanilla css files with mini-css-extract-plugin.
Here's an idea if you want to keep nesting rather than composing individual classnames that would work with linaria's styled approach of using CSS variables for interpolations:
const Button = styled.button`
color: black;
border: 1px solid black;
background-color: white;
&[props|primary] {
color: blue;
border: 1px solid blue;
}
&[props|color=green] {
color: green;
}
`;
<Button primary color="green">
Linaria would then transform it to this before running stylis:
const Button = styled.button`
color: black;
border: 1px solid black;
background-color: white;
&[style*="--a"] {
color: blue;
border: 1px solid blue;
}
&[style*="--b"] {
color: green;
}
`;
<Button primary color="green">
Linaria would then accept the props for primary and color and if they match what was specified, it would apply the appropriate styles:
<button class="b13mnax5" style="--a:1;">Primary Button</button>
Here the value 1 is not important, we are matching for the word "--a". This feels a little hacky. These really could be converted to classnames instead, applied based on state, but I am leaving it as an option to consider.
What I like: Using css namespaces rather than JS interpolation. It is small annoyance to type out a function every time: ${props => props.primary || 'fallback'}
I've added a comment to my RFC that specifically addresses this issue. I've included a lot of detail, but the essence of it is this: Rather than using an arbitrary prefix for style only props, instead specify in the CSS rule if a prop should not be forwarded. Proposed syntax is like this:
&[props|primary--] {}
-- suffix, do not pass prop to children. Props are passed to children by default (unless it is a styled DOM node where valid props are known). Simply include a -- to opt out of this for a specific prop.
Not a fan of the syntax especially in terms of static typing.
Maybe something like this...
const Button = styled.button`
color: red;
`.variants({
primary: css`color: blue`,
secondary: css`color: white`,
disabled: css`cursor: default`
});
const OneVariant = <Button variant={"primary"}/>
const MultiVariant = <Button variant={["primary", "disabled"]}/>
and for static typing (typescript)
.variants<"primary" | "secondary" | "disabled>({....
// which types variants as Record<T, CSS>
Yes, I have revised by thoughts on the syntax. This is what I am using for my ~60 component UI library and it works out quite well. It feels a lot closer to CSS syntax IMO and works with static typing:
interface ButtonProps {
primary$?: boolean;
}
const Button = (p => styled.button<ButtonProps>`
background: black;
color: white;
&${[p.primary$]} {
background: blue;
color: black;
&:hover {
border-color: red;
}
}
`)({} as ButtonProps);
This works much better for static typing than my comment above.
The only issue I have run into with my syntax is I cannot target the primary modifier in another component. This isn't so bad, as styles are self contained, but I did find I wanted to do this once.
Your approach is interesting, but I am not a fan of having to call the css function everywhere. Also, accepting an array feels very much like reimplementing classnames and you lose out on accepting an arbitrary prop that can be used for other logic (you'd have to do let isPrimary = props.variant.includes('primary') in your render functions rather than destructuring a prop).
Another downside to your approach is you can't target multiple variants, i.e. applying some styles when both primary and disabled are enabled.
There's an example there with multiple modifiers
No there is not. You misunderstood what I intended to convey. Take this css:
.button {
background: pink;
}
.button__primary {
background: green;
}
.button__disabled {
cursor: default;
}
/* This */
.button__disabled.button__primary {
color: red;
}
Thought it about this for awhile the solution would be this:
styled.div generates an object (not a string like css), therefore just add modifiers as statics on that object.
I.E
const Button = styled.button`
color: red;
`.variants({
primary: `color: blue`,
secondary: `color: white`,
disabled: `cursor: default`
});
const TargetDisabledButton = css`
.${Button.disabled} {
color: coralpink;
}
`
- Supports static typing.
- Supports multiple modifiers.
- Supports targeting multiple modifiers
- Easy to understand syntax
- No longer need to
import { cx }so much because the array syntax ofvariantson the usage will implement cx under the hood I.E<Card variants={[disabled && "disabled"]}/> - No need to export modifiers separately to components which leads to bloat in my files.
- No need to create separation between components and modifiers (by separation i mean that the declaration for a component could be line 50 but the declaration for a modifier could be like 80 leading to cognitive overhead to reason about peoples styles).
- Easy implementation that wont add excess code to bundle keeping Linaria small.
Here's an example implementation to work around non-native syntax that works in the current version of linaria.
const Card = styled.div`
color: red;
` ;
Card.variants = (obj) => Object.keys(obj).forEach((key) => (Card[key] = obj[key]));
Card.variants({
get disabled() {
return css`
pointer-events: none;
opacity: 0.4;
user-select: none;
`;
},
});
// somewhere in a render and it works.
<Card className={Card.disabled}>HELLO</Card>
And the same code with hopefully a native implementation
const Card = styled.div`
color: red;
`.variants({
disabled: `
pointer-events: none;
opacity: 0.4;
user-select: none;
`
}) ;
<Card variants={[disabled && "disabled"]}/>
const OtherComponent = styled.div`
.${Card.disabled} {
color: blue;
}
`
@satya164 @brandonkal i hope this satisfies your concerns for a modifier implementation
That is a creative solution but I am still not a fan. To provide some reasons:
- It necessitates a lot of extra boilerplate and feels less like writing CSS. For instance your example would require always importing css and writing it for every variation:
import { css } from 'linaria'
const Button = styled.button`
color: red;
`.variants({
primary: css`
color: blue
`,
secondary: css`
color: white
`,
disabled: css`
cursor: default
`
});
Even for short strings, Prettier is typically going to break it into more lines of code.
- The variants array feels like a pointless re-implementation of cx. Writing code like this:
<Card variants={[disabled && "disabled"]}/>
// rather than
<Card disabled$ />
Doesn't seem like a win. You end up typing everything twice.
- Any logic cannot be contained in the styled component. An example of something that isn't possible with the last suggestion:
interface ButtonProps {
primary$?: boolean;
}
const Button = styled.button<ButtonProps>`
color: white;
&${[window.isUserLoggedIn]} {
background: blue;
}
`
That logic could of course be more complex than checking the truthiness of a window variable. The last suggestion would require repeating that logic everywhere Button is used.
I believe there should be a way to add static typing (i.e. accessing modifiers as if they were properties of a styled component (they are, but only after the linaria transform) to my suggested approach.
- let's change the implementation of variants to not need css keyword just a literal is fine (under the hood it will use css
- let's change the variants array to your syntax with just the modifier name on the component.
- that last example is possible you just wouldn't do it like that you'd call the modifier name "isLoggedIn" and then apply it in the component on the state window.isUserLoggedIn, this separates the concerns of styles and state.
- it actually reduces boilerplate because....
import { css } from 'linaria'
const Button = styled.button`
color: red;
`.variants({
primary: css`
color: blue
`,
secondary: css`
color: white
`,
disabled: css`
cursor: default
`
});
for me right now the equivalent is this..
import { css } from 'linaria'
const Button = styled.button`
color: red;
`;
export const ButtonPrimary = css`color: blue`;
export const ButtonSecondary = css`color: blue`;
export const ButtonDisabeld = css`cursor: default`;
or this which i try to do more lately..
import { css } from 'linaria'
const Button = styled.button`
color: red;
`;
const ButtonPrimary = css`color: blue`;
const ButtonSecondary = css`color: blue`;
const ButtonDisabeld = css`cursor: default`;
export const ButtonCss = {
primary: ButtonPrimary,
secondary: ButtonSecondary,
disabled: ButtonDisabled
}
the proposed way cleans up my code an insane amount.
The reason I say the object approach requires the css keyword is because no syntax highlighter or linter will accept a plain literal. That last example is just to illustrate that you can link state to style logic with the approach I suggested. The logic could be much more complex than just reading something off window in my simple example. The approach is inspired by LinkedIn's CSSBlocks if you wish to look into that.
Currently, we use stylis as the parser (though planning to switch to postcss for few reasons)
@satya164 What were those reasons?
I disabled stylis in my build, but haven't been able to get global styles working properly without it yet.
Linters and and code highlighters are just tooling; I'd rather rewrite the tooling based on ideal implementation than use a non-ideal implementation and avoid haivng to rewrite the tooling.
Syntax highlighting and linters will 100% accept a plain literal you just wont trigger it automatically on any literal you'll look to see if that literal looks like it belongs to styled component; Walk the AST.
EDIT: My point is not that my implementation is ideal; just that any implementation shouldn't be discarded just because tooling wont work anymore. Its unnecessary to have css tags there. I'm also more than happy to rewrite the tooling for the linaria community myself iv'e got strong experience with AST parsing
I have few experience with CSS and with Linaria, but this seems to work:
interface TestProps {
$color: string;
className?: string;
}
const Test = styled.a<TestProps>`
text-decoration: none;
&.foo {
color: ${({ $color }) => $color};
}
&.bar {
font-weight: 900;
}
`;
<Test href="#" $color="red" className="foo">Text</Test>
<Test href="#" $color="blue" className={cx("foo", "bar")}>Text</Test>
<Test href="#" $color="red" className={cx("foo", "bar", css`
background-color: blue;
`)}>Text</Test>
And this too:
const Test2 = styled(Test)`
&.bar {
font-size: 32px;
}
`;
<Test2 href="#" $color="red" className={cx("foo", "bar", css`
background-color: blue;
`)}>Text</Test2>
Yeah, I haven't tested it 100% and the DX with static typing is bad.