This is a proposal for an improvement, based on a question I've asked on gitter.
I think that the styled component creation process should be tag agnostic: you shouldn't be forced to specify an element tag prematurely, as you may need to use a different tag semantically, but keep the same presentation aspect, like a button button vs an anchor button.
@xcoderzach suggested the following solution (using recompose):
const Button = styled(componentFromProp('c'))`
...myStyles...
`
<Button c="button" foo="bar" /> // renders <button foo="bar" />
<Button c="a" foo="bar" /> // renders <a foo="bar" />
<Button c={Link} foo="bar" /> // renders react router <Link foo="bar" />
And I like it (as I like recompose too), but I still think it should be a direct styled-components feature.
I suggest to default as div (the most generic one, or span?), and leave as developer choice the possibility to specify a different default tag if needed.
Then leave like a tag prop to permit the following form: <Button tag="a" /> to override the default tag on create element time.
You can do this with:
const e = React.createElement
const Button = styled(({ tag, children, ...props }) => e(tag, props, children))`
// styles
`
which is quite small. I don't think we should add your proposal to styled-components because it's nothing you need so often and there is a simple workaround.
Maybe @k15a is right (compliment for the poro's avatar!) so, what about a docs section about that solution?
Yeah it would probably be good to have something like this and https://github.com/styled-components/styled-components/issues/393#issuecomment-274106357 in the docs.
I'm on the fence about this one. With both my and @k15a's solutions, you lose the attribute whitelist. You'll end up with a bunch of warnings if you pass in any custom props. A real workaround is going to be more verbose (blacklisting or whitelisting props 😢).
Buttons that can be an <a />, <input type=submit />, or <button /> are pretty common. But, that's the only example I can think of, so an affordance just for this case may be overkill.
Simple improvement with attribute whitelist:
const e = React.createElement
const Button = styled(({tag = 'div', children, ...props}) => e(tag, props, children))`
// styles
`
You can do this with defaultProps as well:
const e = React.createElement
const Button = styled(({tag, children, ...props}) => e(tag, props, children))`
// styles
`
Button.defaultProps = {
tag: 'div'
}
Which is in my eyes the more React way of doing this 😉
When using alongside props for determining styles, I'm getting 'unknown prop' warnings from react. My example:
const Card = styled(({ tag, children, ...props }) => React.createElement(tag, props, children))`
width: 256px;
height: 358px;
border-radius: 10px;
box-shadow: 0 1rem 1rem rgba(0,0,0,.1);
background-color: ${props => props.bgColor};
`
Card.defaultProps = {
bgColor: 'white',
tag: 'div'
}
// invoked in render via
<Card bgColor={color}>
</Card>
Creates the error
Warning: Unknown prop `bgColor` on <div> tag. Remove this prop from the element. For details, see https://fb.me/react-unknown-prop
in div (created by Unknown)
in Unknown (created by Styled(undefined))
in Styled(undefined) (created by TeamCardComponent)
Which, to me, reads as tag='div'is not getting passed…
my solution works (i've tested it)
@cloud-walker your solution does work, but i'm still getting that warning.
@ericdfields That's a warning React is showing if you pass props to HTML tags which aren't valid HTMl attributes. The problem with my solution is that the whitelist from styled-components isn't working because styled-components doesn't know that this is basically a HTML element.
This workaround isn't that great as it breaks code highlighting at least for https://github.com/styled-components/vscode-styled-components.
I think this should be built in into api as the use case is pretty obvious and common.
I've also wrote a SO Question: http://stackoverflow.com/questions/42204232/how-to-achieve-tag-agnostic-styled-components/43423505
And this is my solution used here: https://codesandbox.io/s/6881pjMLQ
Can this be discussed more? This also seems like very simple functionality that opens styled-components up to much more flexible use cases.
<button>, an <a href="">, a <input type="submit">, <input type="clear">, etc.<div> or <span>. (Can be useful for UI Kits.)BorderedSection and it defaults to being a <div> because that's sane, but someone could easily want to use a <section> instead, or even a <fieldset> for semantics.Basically, this seems like a nice, simple addition, (that is very lean in terms of new API scope,) that opens up styled-components to be element-independent, which allows authors to have full control over the HTML semantics of what their building, detached from styles.
I think the api should use the term tagName, but it matches the existing DOM API the same way that className was chosen.
@k15a what do you think? Can you consider it?
And on top of that, as far as I understand, this is extremely easy to implement in core? But it's a more complex to implement in userland, especially in ways that aren't re-creating new styled components on every render, which will be an easy mistake for people to make.
In v2 this is actually already supported, though slightly differently:
const X = styled.div` ... `
const Y = X.extendWith('span')` ... `
const Z = X.extendWith('button').attrs({
type: 'submit',
})` ... `
Sorry this isn't properly documented yet, but if you install styled-components@next you can give it a try.
Again, this is not the same thing, this way I'm forced to re-create new components (LinkButton, ButtonButton for example) just to achieve a different HTML semantic behaviour, when, in my opinion, semantics are not in the scope of styled-components, which focus is presentation.
I'm right?
+1 to what @cloud-walker mentions.
The key with the tagName= property is that it allows styles and semantics to be completely separated, and changeable based on where the specific component is used in the tree—which makes the styles reusable/composable in the same ways that classes are in the typical HTML/CSS/JS trio.
I'm with @cloud-walker and @ianstormtaylor on this. This is a huge blocker for me. I've been hoping to build Modulz UI on top of styled-components and I just can't continue with this blocker.
As people have pointed out, this issue is not infrequent, for me, it causes problems with almost every component. One of the most problematic components is text. Here are all the html tags I need to be able to render text with:
I could go on but I think that should illustrate my point. The same goes for most components.
It's not feasible to define several new components for the sole purpose of using a different html tag. If semantics and accessibility are important to you (and they should be), you will end up with tons of redundant components just to get around this issue.
For me, the bottom line here is that style should be completely and utterly decoupled from content. Styled-components forces you to tightly couple styles to tags and to me that is an oversight.
Hmm, sorry I'm not convinced. Yes you _can_ use all of those elements for rendering text but _they're not interchangeable_ semantically. I'd argue they are, in fact, different components, whether or not they share some/all styles.
If you _really_ want to expose a tagName attribute, you can always do something like this:
const RedDiv = styled.div` color: red; `
const RedSpan = RedDiv.extendWith('span')``
const RedText = ({ tagName, ...props}) => tagName === 'span' ? <RedSpan {...props}/> : <RedDiv {...props}/>
Styled Components are best considered low-level building blocks from which you can build up complex UIs. For any unit of logic more complex than a single block, you already have React's component API.
I prefer @cloud-walker's solution and I've used it with great success in my own library Boundless
(example). It's very easy to support and adds a massive amount of utility for almost no expense.
You're correct that some of those text html tags are not interchangeable semantically. Using a <blockquote> is dramatically different from using <h3>. I'm not arguing that point though.
The issue here is that it is _extremely_ common to need to apply the exact same styles to several html tags. This is not just the case for text but for almost all components.
Let's look at as another example. Every one of the below input types should look identical:
You just swap the type attribute to define semantics and also to utilise different native functionality. But I need _all_ types and they _all_ need to look identical and they're certainly all the same input component.
Yeah, I simply disagree that this is "extremely common". Moreover, Styled Components is founded on making some things easy at great cost to complexity (interpolations, nested selectors etc) and deliberately _not_ supporting everything (like postcss plugin chains, a non-componentised-api using classNames={css` ... `}, etc), in order to guide people towards decomposed, maintainable component hierarchies. I think it's important to recognise visual information and semantic information are linked more often than not, therefore having free choice of tag for a styled component seems strange.
There _are_ definitely cases where two components might be identical except for tagName, which is why extendWith has been added. But it's a clear, deliberate and bounded act to create multiple components that share styles. It's self-documenting, rather than unconstrained. I think it's sufficient to capture the relatively few cases where this is necessary.
Your example with inputs proves my point, somewhat. All of those are <input> tags. They might render quite differently in a browser but they're all the same tag.
I think the most interesting case is where two tags (say a <blockquote> and <h3>) have _almost_ the exact same styles. The reason SC has had so much thought into interpolations is to make that as easy as possible:
const sharedTextStyles = css`
color: red;
${ props => props.inheritFont ? '' : 'font-family: Avenir;' }
`
const BlockQuote = styled.blockquote`
${ sharedTextStyles }
border: 2px solid black;
`
const H3 = styled.h3`
${ sharedTextStyles }
margin-bottom: 1rem;
`
This is the preferred approach, rather than defining and then extending the component multiple times. The design idea is this: make the component the building block but make it easy for building blocks to share traits.
I think it's important to recognise visual information and semantic information are linked more often than not.
I think it would help to develop this point some more because this statement is simply false. In the majority of cases, the opposite is true.
<a>.<h6>. A tiny heading can be a <h1>.<section>, <header>, <footer> or <main>.<span>, <time>, <strong> or <p>.<article>, <div> or <li>.Having free choice of tag for a styled component seems strange.
Sure, that would be misleading (although still better than the current situation). A button component would never be an <article>, for example. The ideal setup would be to be able to define tag options and also define a default tag.
The problem right now is, as I've mentioned, the tag options are frequently so numerous that the current solution ends up being so unwieldy.
There are definitely cases where two components might be identical except for tagName, which is why extendWith has been added. But it's a clear, deliberate and bounded act to create multiple components that share styles.
The use case I have been referring to is where you're using 4+ tags frequently which share _exactly_ the same styles.
There is a deeper issue here though. The simple fact that SC works by defining a html tag, then rendering styles on it, reflects a fundamental misunderstanding of UI design. It's been widely accepted for years now that styles should _never_ be tightly coupled to content.
As I mentioned, this is a serious blocker for me, I unfortunately have to drop SC without it. So if there's a small chance you might reconsider, I'm willing to invest a few days composing an argument?
Failing that, I'd suggest reading through this article on semantics from Nicolas Gallagher.
I... really can't help but think you're making the opposite point to what you're trying to. Do you really think a <section>, <header>, <footer> or <main> would be the _same_ component that only differed on the tag? There would be no other differences? Surely, wouldn't it be better to extract their _similarities_ into variables or fragments and create a SC for each element that interpolates all the information each instance requires? Wouldn't that better reflect the roles those elements have in the design of the site you're building?
I think, perhaps, you're taking a too narrow view of what a styled component is. It's not necessarily something for export, it can be entirely local to a component. The point is it's supposed to encapsulate something with a name that _has meaning in the context in which it's defined_. If you have complex behaviour that you'd like to expose to other files, define a React component.
Yes, a Button component can render an <a> tag if it really is a href. Likewise, something that looks like a Link might actually be a button underneath, because it has no logical href attribute. But the key point is that these have _vastly_ different styling constraints, since the number of user-agent styles present are extremely different. So, I'd probably do:
/* Button.js */
const shared = css` ... `
const Button = styled.button` ... `
const A = styled.a` ... `
export default ({ type, href, ...props }) => {
if (type && href) throw new Error("A button shouldn't have a href if it has a type!")
return type ? <Button type={type} {...props}/> : <A href={href} {...props}/>
}
I've still exposed a Button component to the rest of my app, but now I make the correct tag choice based on the type of button I'm working with. To me, this is a far better result than simply letting a Button expose its tag variably.
If you've got a h6 in one place and a h1 in another they sound like different components, each local to their own environment:
/* FooComponent.js */
import { H1 } from '../components/font-sizes'
// Has to be a h6 due to reasons
const FooHeader = H1.extendWith('h6')
export default ({ headerText }) => (
<div>
<FooHeader>{headerText}</FooHeader>
...
</div>
)
I guess what I'm trying to convey is this: for me, there's literally no use case you've raised that would make me want to make the tagName a prop. I totally get that traditionally styling and tag choice are supposed to be decoupled, but I see that as a function of the constraints of a global CSS environment. Personally, that thinking leads to CSS frameworks like Tachyons (which I fkn LOVE) but SC is proposing a different unit of encapsulation. In the same way that React promoted a component-centric view of behaviour, SC is promoting tag+styles+props as the abstraction of UI. I think it models the intrinsic coupling between visual and semantic information that real-world designs have, but does so in a way that allows for sharing of styling to happen at a _deeper_ level than the component boundary.
I respect the fact this might be a dealbreaker for you, but I do appreciate this discussion. It's given me a bit of a chance to explain some of the reasoning behind some design decisions, and I've been trying to give more-than-minimal responses to each of your arguments to help you, or anyone else reading this in the future, understand that there's a reason behind my obstinance.
In that sense, I'd love it if you'd take the time to put forward your argument. There's absolutely a chance I'd reconsider my position, but I think it's more valuable to everyone else who might read this to hear the full version of your argument. My only request is that you show _code_ that might be cleaned up by this change. I've done my best to show you how I'd solve these problems within the current design, but you've claimed that "the tag options are frequently so numerous that the current solution ends up being so unwieldy" or "you're using 4+ tags frequently which share exactly the same styles." SC exists to solve real-world problems, not abstract ones. So if you (or anyone else in this thread) can show realistic examples where the current API breaks down, I'll absolutely entertain the idea of changing it.
Thanks for the detailed response, I really appreciate it. If nothing else, it inspires confidence to know that the core team are listening.
Do you really think a
<section>,<header>,<footer>or<main>would be the same component that only differed on the tag?
Yes, I absolutely think this. Let me share a simplified code example of my Section component from Modulz, the UI toolkit I've been building on SC.
const Section = styled.section`
display: flex;
background-color: ${palette.WHITE};
padding: ${palette.SPACING_600} 0;
${props => props.small && css`
padding: ${palette.SPACING_300} 0;
`}
${props => props.large && css`
padding: ${palette.SPACING_900} 0;
`}
${props => props.snow && css` background-color: ${palette.GRAY_200}; `}
${props => props.charcoal && css` background-color: ${palette.GRAY_900}; `}
${props => props.blue && css` background-color: ${palette.BLUE_500}; `}
${props => props.amber && css` background-color: ${palette.AMBER_500}; `}
`;
That will render this white <section> which contains other components. It will also render this dark section. It will render any section on a marketing website - it is designed to handle the padding and background colour, style properties which are shared between _all_ sections.
Now, here is my footer. It shares the exact same styles, nothing more and nothing less. It is the exact same component, the only difference being that in this particular case, it represents the enclosing section of my website.
The same goes for my header section. There is absolutely nothing different about this, except that it represent the head of my website. Everythsing else about this component is identical to the section and the footer.
This component could also be a <main> tag for a Section housing the primary content on the screen. Or it could be an <article> tag maybe housing some blog posts links.
In this case, it doesn't make sense to create a new Footer.js component or a new Header.js component because there is absolutely nothing different about those components other than the html tag. I would just end up with 3 or 4 components all doing exactly the same thing.
You could argue that I could wrap the Section component in a <header> or <article> tag. But that would be unnecessarily painting people into a corner, limiting their options for no good reason. The fact of the matter is, the style of the component has very little to with the html tag it is rendered in. Sure, it wouldn't make sense to render this particular component in most html tags available, but it certainly doesn't make any sense at all to couple the styles with a single tag.
Yes, a Button component can render an
<a>tag if it really is ahref.
The code example you posted here looks good and would actually almost suffice for the button component, except for the outstanding issue that the button component must be able to render a <button>, <a> _and_ <input type="submit">. I'm not being finicky here, not at all. Now, I'm not experienced with React or even JS, so perhaps React somehow removes the need for buttons using <input type="submit"> and <a> but unless I'm overlooking something...almost 100% of web apps need at least these three tag options for buttons. Foundation supports three options. Bootstrap supports _five_ options.
Again, unless I'm unaware of some JS magic that single-page apps provide which negates this need, it should be obvious that this is a standard and essential use case.
If you've got a h6 in one place and a h1 in another they sound like different components.
I don't understand your logic here, what makes it seem like they should be different components?
In any well designed website or web app, the same heading styles will require many different <h> tags.
const Heading = styled.h1`
font-size: ${palette.FONTSIZE_L};
color: ${palette.WHITE};
letter-spacing: -.08em;
margin-left: -.5rem;
${props => props.light && css` color: ${palette.GRAY_500}; `}
${props => props.thin && css` color: font-weight: 300; `}
`;
In the larger context of a design system that serves a large product with a marketing site, web app, mobile website plus in-house apps, this heading component will be used in dozens of different environments, performing a number of different semantic functions. It is entirely possible, in fact it is likely that it will need at least <h2>, <h3>, <h4> and <h5> tags.
Bootstrap and almost all other frameworks have supported double-stranded headings for years now. Harry Roberts wrote about the benefits of double-stranded headings.
This is not about encapsulation, not at all. This is just simply because it is so unbelievably common to need to use the _exact_ same heading style with several different <h> tags.
I think our tag+styles abstraction models the intrinsic coupling between visual and semantic information that real-world designs have.
I'm really not sure where you're getting this idea. Real-world designs literally don't have any intrinsic coupling of visual and semantic information. In a well-designed, comprehensive design system, components can be reused in any environment, regardless of context. That is the essence of good design and I think you might be misunderstanding this point.
The whole point of building a component is to enable reuse. That component should be able to appear in any context without modification and it should be able to support any content without colouring the semantics of that content.
When I build an input component, I want to be able to render that input component to any context. It will be rendered in marketing site forms. It will be rendered in header navs. It will be rendered inside dropdowns. Inside modals. In sidebars. Anywhere. Every single instance is identical. Every instance is unmodified, aside from the components own props. Certain contexts will require different semantics but that has absolutely _nothing_ to do with the component. The component is just style and behaviour. Content, context and semantics are entirely separate. I chatted with Max about this on Twitter where we both ended up agreeing.
Extending components contradicts the very purpose of components. If you have lots of components which are almost like other components, but slightly different, this is what leads to an inconsistent design. Every time you extend or modify an existing component, you dilute the consistency of your design system.
Furthermore, extending components and chaining them together creates a bond between multiple separate components. This type of unnecessary coupling is toxic in a large codebase. When you change one component, the change bubbles down to other components. We've all seen this nightmare play out with the CSS cascade and subsequently with Sass extend function. I thought we learned from these mistakes. Carrying this chained architecture over to JS is just _extending_ (see what I did there) the nightmare.
Sorry for rambling on with such an unrefined rant, I wish I had more time to express myself better. I think the bottom line is that style just has nothing to do with semantics or content. SC does an amazing job of making it easy to style components. Though where we place those components, how we use them and what content we render into them is a completely separate matter entirely.
I agree a bit with both of you, and I've not used SC in a real application yet, so I guess I cant argument more.
I simply think that is hard to understand which is the best approach to tackle the complexity we are talking about.
My personal logical argument was that React led devs from a Structure - Presentation - Behaviour world to a component-centric world. Now, with SC we are isolating the presentation components away from the behaviour components. What I miss here is the old Structure separation of concern part, why should we care about it on SC?
Ah, and to reply to the input tag arguments: why we treat html tags standards like dogmas? What if w3c decided to ship an input tag for every type? Like
@colmtuite I think you're trying to squeeze some semantics into styling that aren't necessary:
<a>.A button is a button. A link that looks like a button is still a link. Accessibility-wise this still holds up, and is not a 1-1 translation;
On the other hand, if you just want a link to look like a button, then you can use extendWith.
<h6>. A tiny heading can be a <h1>.Headings might share a base style, that you can abstract using a css mixin, or a "BaseHeading", that you can then extend for each tag with extendWith.
Not all headings are the same, neither in styling, nor in semantics in the document.
<section>, <header>, <footer> or <main>.Realistically they'll all be different, but if they share some styles, which won't be a lot, then use css. On the other hand, since you only gain some semantics for parsers here, you could also use divs inside these tags, without any problems.
<span>, <time>, <strong> or <p>.Oftentimes, you'll want time, span and strong to be encapsulated in a p or something else, but they won't share styling. Use CSS' inheritance for these sub-tags to share some styles, like the font-size.
<article>, <div> or <li>.<article> is for semantics. I'm not sure what you mean by a link here. These are all different tags, in different contexts.
This might seem very restrictive, but as you can see, these problems just go away, since they're either not 100% accurate, or not as restrictive and necessary as it sounds.
SC is a departure from frameworks like Bootstrap — which doesn't mean that you can't build one on top of SC. We're not looking into adding bits and pieces of styling onto some elements, so that they look like you want to in any case. Oftentimes it doesn't even lead to semantic documents.
We're looking into styling components in their respective contexts. And reusability and structure just don't work like they would with classnames and old-school css frameworks.
Hey, I'd like to jump back in here because I might be able to explain some of what @colmtuite is trying to get at, because we're both trying to achieve the same thing. (Him in a re-usable UI Kit way, and me inside my own app now that I've heard his ideas and realized they are really good.)
Before I start, I just want to say that I think the styled-components API is amazing. It's extremely simple to define components, in a way that feels almost like the "functional component" equivalent for the styling side of things. So the reason I'm arguing here is because I really want to make it the perfect fit for everything I'm doing, and I think this issue does that.
First off, what we're advocating for is not to allow, "free choice of tag for a styled component", as if there is no default to begin with. The API we'd like to see is actually extremely similar to the current styled-components API, but with the ability for the user to override the default tagName when creating the component element.
Such that rendering a Text component will default to using span, since that's the least semantics-laden, and most useful for random UI contexts:
<Text small gray>Some random piece of UI text.</Text>
But there are cases where you'll want to use that text in a place where the semantics are actually important, for example when using it for form-field labels...
<Text small uppercase gray tagName="label">Your Name</Text>
So, the proposal isn't to allow creation of components that are _required_ to define their tag name at creation time, but instead to allow users to override the default tag name on creation, when semantics require it.
The second piece required for this argument is talking about _why_ we want to do this. And this is slightly harder to explain, because it's a different mentality for thinking about components than the usual styled-components codebase uses. It feels much more like the Bootstrap/Foundation-like, because it's a mentality geared towards creating extremely reusable styles, like those required for UI Kits that are used across huge (and/or multiple) code bases.
Note: I realize that up to now in this argument that styled-components has been framed as taking some sort of different "ideological" stance on what a component is, and that's why it doesn't allow what we're asking for. I'd like to ask that you put that out of your mind while reading this section, because I think having the "ideological" frame of mind prevents you from seeing other ways of doing things that are very valid, and potentially even better for certain circumstances.
Right now many of the styled-components examples preach making tiny modules, where the styled and tag are tightly coupled. Usually you'll do this even in the same file, right above your "non-tiny" component that uses them.
This patterns works great—for certain use cases. It works amazingly for simple landing pages, for one-off product pages, for small open-source demos, etc.
However, it does not scale well when you're trying to create UI Kit–type frameworks, because in those cases you actually don't really want people defining one-off components all over the place, tailored to their exact needs. In those cases, what you want is to be able to define a series of tiny style abstractions, package them up in a reusable library, and then for the "users" to just compose those abstractions to create more complex apps.
This is a different way of thinking. I'm sure of it, because I'm currently transitioning an app from the old way to this new way.
The "composable" way of thinking of styling components is actually much more like the "HOC mentality" that React preaches. Whereas the "one-off/extend" way of thinking is like traditional OO inheritance. The composable approach results in flatter inheritance structures and much much fewer hidden dependency trees. Which leads to more manageable codebases.
This "composable" way is actually the "atoms" from Atomic Design. And here's a really good article that @colmtuite has written that explains the mentality.
In the "one-off" approach, you might do...
const Label = styled.label`
font-size: 12px;
color: ${UI.gray};
text-transform: uppercase;
margin-bottom: 8px;
`
// And then later, when you need to tweak it...
const MyCustomLabel = styled(Label)`
color: ${UI.navy};
`
// And elsewhere...
const StyledLabel = styled(Label)`
margin-bottom: 0;
margin-top: 16px;
`
// And elsewhere...
const InlineLabel = styled(Label)`
display: inline-block;
margin-bottom: 0;
margin-right: 40px;
`
This is classic OO-style inheritance. And don't get me wrong, this works absolutely fine for small use cases. If I was building a single landing page for one of my open-source projects I would absolutely use this approach, it's simple, everything stays in one file, and it doesn't require massive forethought.
However, when you're getting into trying to build a UI Kit that can be re-used across your entire product—and often even across repositories—this doesn't scale. You end up with crazy styles added all over the places and the brand starts to fall apart because you're not consistent.
In those cases, what you want to be doing instead is using the "composable" approach. Which might instead look something like this:
const Text = `
${p => p.large && css`font-size: 24px`}
${p => p.small && css`font-size: 16px`}
...
${p => p.gray && css`color: ${UI.gray}`}
${p => p.black && css`color: ${UI.black}`}
${p => p.red && css`color: ${UI.red}`}
...
${p => p.uppercase && css`text-transform: uppercase;`}
`
const Spacing = `
${p => p.margin && css`
${p => p.left && css`margin-left: ${SIZES.small}`}
...
`}
...
`
Such that when you use your "atoms", later it looks like:
<Spacing small margin bottom>
<Text small gray uppercase>A Label</Text>
<Input type="text" />
</Spacing>
// Or elsewhere...
<Spacing large margin bottom>
<Text xlarge black>A Huge Heading</Text>
</Spacing>
// Or even...
<Spacing large margin top>
<Plane gray high>
<Spacing small padding>
<Text large black>A Modal Heading</Text>
<Text medium gray>A paragraph about whether you want to...</Text>
<Button solid blue>Accept</Button>
<Button hollow red>Cancel</Button>
</Spacing>
</Plane>
</Spacing>
It's a different way of doing things. You're no longer extending components from each other and creating inheritance trees. Instead you're using them more like HOC components (except inline, so not exactly) to apply their very specific use case in each location.
Except the issue is that in this way of thinking, your styles are just simple "atoms" and shouldn't really care about what tag they are attached to. For example, you're just using a simple <Text> component for a label, but right now there's not easy way to be like, "oh this invocation of <Text> should actually use a <label> tag, because that's what I'm using it for". If you were building it with the proper semantics, those examples might have looked like...
<Spacing small margin bottom tag="fieldset">
<Text small gray uppercase tag="label" for="first-name">A Label</Text>
<Input name="first-name" type="text" />
</Spacing>
// Or elsewhere...
<Spacing large margin bottom tag="section">
<Text xlarge black tag="h1">A Huge Heading</Text>
</Spacing>
Which is why we'd love to see something like tagName adopted. Because, in my opinion it's very very lightweight in terms of API footprint, but it opens up styled-components to entirely new use cases that aren't easily possible right now.
Finally, of course, there are ways to get this functionality, as have been mentioned up-thread, but none that work without losing the whitelist, which results in a ton of React warnings...
But the other reason I'd like to see this in core, is because I think it is a core use case (in that many, many users will run into wanting to do this if they are building truly reusable components), and the I'd like to see it standardized at the library level, so that we don't end up with a ton of different ways to do it with different naming systems and approaches—since it's such a simple piece of logic to begin with.
And that said, given that React thinks className vs. class was a mistake, we should probably use tag instead of tagName to avoid making the same mistake just because they did it.
Thanks for reading. Happy to discuss further.
Yep, I agree with pretty much everything @ianstormtaylor said. Thanks for clearing up some points I was trying to get at.
I'm sleep deprived right now so I'll try to avoid rambling on much further. In case it helps though, I'll try to explain in layman's terms, the main differences between the two approaches as I see it.
Please correct me if I'm wrong, but the current SC approach seems to be something like:
"Oh you need to build a small piece of UI? No problem, define a new styled-component, choose a tag relevant to the context of whatever problem you're building for now, then whack some styles onto it. Selector inheritance? That's fine. Nesting? That's cool too."
The approach Ian and I are advocating is more like this:
"Oh you need to build a small piece of UI? Here are your 22 components and their props. Don't add to them, don't modify them, don't build anything that is not exclusively built from those 22 components. If you absolutely must build a new component, make sure it is reusable in any context imaginable and make sure any custom values are defined using global variables."
I probably take a much more rigid stance on this than Ian does, but the first approach is just plain bad practice and should never be used. It will inevitably lead to an inconsistent, unscalable mess riddled with inheritance trees.
I do want to draw attention to one point though. The first approach is the approach currently adopted by the vast majority of people. This is mainly due to a lack of understanding of design systems but also due to most people not having to construct large, scalable design systems to an extremely high standard. Most of the content in SC readme and docs is geared toward this approach. As soon as you attempt to scale that out to a large, super-consistent, high quality design system...it just falls apart.
If anyone is interested, I'd love if you could take a look over the small bit of work I've done with SC on Modulz. I've also put together a rough demo. Hopefully this will help to illustrate what I'm talking about. No style should be unique. No component should be modified. Everything should be infinitely reusable. The only way to achieve this is to decouple style from markup.
@irfanhudda @colmtuite Don't get me wrong, I do see y'alls points, but there's a couple of misunderstandings here that I want to resolve, since we're throwing building complex sites & building UI kits into a big bucket. Building UI kits is totally a valid use-case, but it's not going to end up looking like the Text component above all the time, since there's more options as to how to do this.
Given that a team of designers build consistent pages, that are component-driven, you're going to end up with lots of your designs inside the semantics of components this way. Basically what the designer is saying could be:
If I have a block of text in an article, it's always going to look like this.
Or also:
The menu will use the base font size, consists of links, is inside this box...
It's always about context. And there's different ways of establishing it. For me, and for most, building a Text, and a Heading, and an Spacing component is too abstracted, since it pushes the basic styling choices that were made into your document, not your styling code.
To talk about what Ian has said here:
In those cases, what you want to be doing instead is using the "composable" approach. Which might instead look something like this:
It's not this black and white. This is not composed / the compositional way at all actually. This is just plain classNames like .uppercase brought into a Styled Components environment.
Oftentimes you could just say: Let's make mixins for all these shared chunks of styles (interpolations / css helpers). Together with these you could indeed have more specific base components to your brand, that are more context aware, and are specific to what you're doing.
Take the SC Docs for example — admittedly not the cleanest / best example right now:
https://github.com/styled-components/styled-components-website/blob/master/components/Anchor.js
This Anchor is reusing our links and can either be a "Header" or a "SubHeader". It's specifically geared towards in which contexts it can appear.
So when you're building a UI Kit, you're going to keep running into these issues, because you're trying to make sth a reusable as possible. And when we look at it, it's not necessarily geared towards a certain brand, since these kits are just extremely low level building blocks.
Nothing against kits like these at all: http://rizzo.lonelyplanet.com/styleguide/ui-components/cards
But when you go even lower level and create components like Text, you're making something that is not targeted towards your design anymore, but targeted towards 99% of what you'll ever do, abstracted behind props. So you're basically abstracting actual styling into your markup.
I think the HOC solution to determine a tag later on is a nice escape hatch for when you absolutely have to do it, but it's not what Styled Components is targeted at unfortunately.
On the other hand I do think that what you're describing is totally possible with our:
Thanks for all the replies, I really appreciate the effort and know how tiring it can be! I've just about lost all hope that we will see eye to eye but I'll give it one last-ditch effort.
If I have a block of text in an article, it's always going to look like this.
No consistent, component-driven product was ever designed with this logic. It's obvious to me that you're falling into the same trap most people fall into. You're misunderstanding the essence of design systems. You're thinking in modules, I'm thinking in components.
I'm never concerned about text appearing inside an article. This is how I think about text in a design system:
"These are my 6 text styles. Every single instance of text across my _entire_ product will look _exactly_ like one of these 6 text styles, regardless of where it appears or what its function is."
If you're not thinking like that, you're missing the point.
The menu will use the base font size, consists of links, is inside this box.
Again, this is the same misunderstanding. You should never decide upfront what font size the menu will use or restrict which types of content it contains or restrict which components it can be rendered into. You should just say, "The menu will have a white background, a distant box shadow and will contain content." That's it.
Let me use Bootstrap's dropdown component as an example here because bootstrap has been inaccurately compared to to component-driven approach I'm advocating. Here, Bootstrap have made the fatal and extremely common mistake of prescribing which content goes into a dropdown. You can add a dropdown-item, a dropdown-header, and a dropdown-divider.
Now, this poses two major problems.
Check out profile, notifications and settings dropdowns on products like Medium, Facebook, Quora, Youtube etc. I should be able to build every single one of these dropdowns easily with the same set of components. How? Simple, completely decouple components from content and context. Just let components be components. Mix and match them to construct modules, applying semantics after the fact.
It's always about context.
So, to continue on from my last point: no, it's not. It's absolutely _never_ about context. It's only ever about components. Isolated, reusable, consistent components.
@colmtuite @philpl hey guys, good conviction! :smile: I think we should put away the discussion about how to build proper UI kits, since it's not necessary for the solution here.
As far as this feature request, I think it's enough that wanting to set the tag name at creation is a valid approach, not the only approach.
@geelen @k15a can you check out my reply a few comments ago and give it another consideration? Thank you.
Thanks for the big detailed responses, I think I get a bit more where you're coming from, so let me see if I can sum up my position shortly.
I totally agree with your view on good component design for consistent UI, I just don't think SC's API needs to change to make that possible.
Adding a tagName as a core SC prop would add the ability to change tag name to every SC for every user, in every use case. To me that's a very hard sell. There would have to be some reason overridable tag names make sense as the _default_, which I still think they don't.
I really don't get why the current situation is that bad. You can do everything you want, we're not locking you in at all. For example:
const agnostic = defaultTag => ({ tagName = defaultTag, ...props }) => <tagName {...props}/>
export const Section = styled(agnostic('section'))`
...
`
alternatively
const makeAgnostic = Base => {
const instances = {[Base.target]: Base}
return ({ tagName = Base.target, ...props }) => {
const SC = instances[tagName] || (instances[tagName] = Base.extendWith(tagName)``)
return <SC {...props}/>
}
}
const Section = makeAgnostic(styled.section`
...
`)
Personally, I'd prefer doing things explicitly like I showed earlier with the button/a based on props (I don't believe there is any need for input[type='submit'] in modern browsers btw), since it gives you an opportunity to give a contextual error message if someone passes an incorrect tag. But either way is already possible, it's just opt-in rather than opt-out.
The reason I was banging on about extendWith btw is because I code like this:
import { Section } from '../components/structural'
import { Text } from '../components/typography'
const Header = Section.extendWith('header')``
const Content = Section
const Footer = Section.extendWith('footer')`
padding-bottom: 3rem;
`
const P = Text
const Label = Text.extendWith('label').attrs({
'small gray uppercase': true,
})``
export default () => (
<Header>
<P>Some text here</P>
</Header>
<Content>
<Label htmlFor="foo">A label here:</Label>
<input type="text" id="foo"/>
</Content>
<Footer>
<P>...</P>
</Footer>
)
(I don't normally have to use extendWith btw, this is just for an example of my structure)
I think content & structure belong together (JSX), so I try to hoist styling up outside the render method & give it a name. The styling lives in this flat layer of SCs or Stateless Functional Components, which maps the contextual names to the reusable styling components (or defines them in-place). This kind of content/styling separation is what lead me down the SC path in the first place.
@geelen thanks for the reply.
The issue with your first example is that (as far as I can tell from trying that already) it will result in lots of React warnings in the console because styled-components will pass through even non-React properties and then React will complain when they are passed to the native elements. The only solution I can see to this issue is to require anyone who wants to do this to maintain yet another whitelist, or somehow ensure they depend on the exact same version styled-components uses.
The issue with the second example is partly that it's (obviously? please tell me you see this) extremely hacky to be generating new components at runtime whenever a previously unknown tag shows up. This will lead to brand new class names being generated each time, which isn't the worst but will be harder to debug. Generally I feel like it's not ridiculous to assume that it feels hacky and that has the potential for weird edge cases. If that's the absolute only way to do it, I guess I either have to go with it or move off styled-components instead.
The thing is, I understand how you write components. I also write many components that way. I'm not asking you to change how you write them at all. I'm trying to draw attention to what I see as a very obvious hole in the escape hatches that the styled-component's API provides...
You can change both the styles and the tag of a component at define time. You can override them both again at extend time. But at runtime you can only override the styles via style= or className=, but you cannot override the tag anymore. Why? I don't believe there is a good reason. This feels arbitrary. In my opinion a purely ideological argument without a clear benefit to the end user shouldn't be enough to block the addition of an escape hatch that solves known pain points...
All HTML headings,
<h1>through<h6>, are available..h1through.h6classes are also available, for when you want to match the font styling of a heading but still want your text to be displayed inline.
— Bootstrap
Because Bulma solely comprises CSS classes, the HTML code you write has no impact on the styling of your page.
— Bulma
Add the
.uk-h1,.uk-h2,.uk-h3,.uk-h4,.uk-h5or.uk-h6class to alter the size of your headings, for example have a h1 look like a h3. For additional styling options, take a look at the Heading component.
— UIkit
It's not an arbitrary edge case. This is how many CSS frameworks and styling models have been built up over the course of CSS, it's how frameworks are able to component-ize styling logic while allowing users to choose their own tag semantics, and is used by Boostrap, Foundation, Tachyons, BassCSS, Pure, Bulma, etc. They use it for buttons, for headers, for layouts, for helpers, for many many things.
In my opinion you're viewing styled-components too much as a library that's primary value add is an ideology, when it is much more powerful to view it as the most convenient styling DSL for React components. (Just like JSX is the easiest DSL for HTML in Javascript.) And a good DSL's job is to make the 90% simple and the 10% possible.
I'm working on problem 1 separately. If that wasn't an issue, would it be sufficient for your use case?
@geelen it would be sufficient for cases where the functionality is covered up inside an individual app. But it would make sharing libraries and tools that want to rely on that ability much harder, because either every userland person would have to re-implement the shim, or they'd need to import it in separately from the library. Ideally, in terms of the flexibility of styled-components as a DSL, it would be in core, so that it could be used for first-class use cases by library authors.
To try to give a more concrete example...
Right now in the non-React, non-CSS-in-JS world, working with styled for modular components looks something like:
.grid { ... }
.col-6 { ... }
.button { ... }
.button--primary { ... }
.button--secondary { ... }
.button-group { ... }
.h1 { ... }
.h2 { ... }
.h3 { ... }
.h4 { ... }
.bg--gray { ... }
.bg--white { ... }
.bg--green { ... }
Which on some landing page might be used as...
<section class="grid bg--gray">
<div class="col-6">
<h4>Some Feature</h4>
<p>A description of the feature.</p>
<a href="..." class="button button--primary">Sign Up</a>
<a href="..." class="button button--secondary">Learn More</a>
</div>
<div class="col-6">
<h4>Some Feature</h4>
<p>A description of the feature.</p>
<a href="..." class="button button--primary">Sign Up</a>
<a href="..." class="button button--secondary">Learn More</a>
</div>
</section>
But elsewhere, in the app itself is used as...
<main class="flex flex--column">
<nav class="flex--1 nav-bar">
<div class="button-group">
<button class="button button--primary">Save</button>
<button class="button button-secondary">Logout</button>
</div>
</nav>
<div>
<h2 class="h4">User Settings</h2>
<p>Some short things about settings.</p>
<form>
<fieldset class="bg--gray">
...
</fieldset>
<form>
</div>
</main>
But we all know that keeping those stylesheets in separate files, using a separate language is nowhere near as good as using CSS-in-JS. It gets tedious, it's hard to maintain, etc.
Just by switching to something like Glamor, you get advantages of keeping it all in JavaScript. And in terms of the flexibility of using classes and tags, there are really no downsides. Since Glamor, Aphrodite, etc. all treat bundles of styles similarly to class names, you're always free to choose your tag still at runtime.
But we also know (as styled-components proponents) that even with Glamor, Aphrodite, etc. there are still pains, partially because you're no longer writing CSS you're writing JS objects, which are awkward. And partially because it's so much nicer to be able to pass simple boolean props and simple string props in React than it is to worry about concat-ing class names. And also partially because for the most part, the tags that components map to is fairly standard. In other words, it's very easy to choose a high-hit-rate default tag.
So all in all, the benefits of styled-components are amazing.
But in the process of creating the styled-components DSL, with super-easy default tags, we've lost one of the properties of the original class-name-based syntax that was actually important for certain use cases: the ability to choose tags at runtime, or at "use point".
And really, we haven't lost that ability for any real reason... it's just because right now the "sane default" tag isn't overridable when it needs to be. But why limit ourselves to that? Why only allow a subset of the functionality that the class-name-based approach offers, when it's so easy to match it completely? Turning styled-components into a purely better experience, with no added constraints on what can be done.
Right now if you try to convert Bootstrap, Pure, Tachyons, etc. to styled-components you run into this issue where you can't quite match the flexibility of those frameworks, despite all of the other improvements in terms of DX that you do gain. And what I'm trying to argue is that I don't think there's a reason for us to make that sacrifice.
If you take a look at "rebass" which is a library that's trying to port some of the Tachyons-like concepts to React, it ends up creating it's own Base component that implements a tagName prop. Because it's necessary to achieve the flexibility. (They also have something called baseRef which is the same as styled-components innerRef, and they too pass through className and style.)
What I'm super interested in is the ability to implement Rebass-like libraries and tools, but with styled-components, instead of rolling an entirely new styling system.
And even more interested in is taking it one step further... (and why it's important in my opinion to have something like tagName= or component= handled in core) ...and instead of packaging these components up as these standalone libraries full of opinions and decisions like Rebase... building simple helper utilities that let users mix-in and compose their own components with the scale-based thinking that Rebass/Tachyons/Basscss/etc. have been championing.
Imagine being able to do something like...
const sizes = [
'8px',
'16px',
'24px',
'32px',
'40px',
]
const colors = [
red: '#f00',
black: '#000',
white: '#fff',
]
const Box = styled.div`
${mapScaleToTopLevel(colors, 'background-color'}
${mapScaleToProp(sizes, 'marginLeft', 'margin-left'}
`
Letting users do things like:
<Box red marginLeft={3}>
...
</Box>
But by giving them the utils to build the components themselves, and have complete control, instead of building these monolithic, opinionated, soon-to-be-dead, open-source UI libraries.
But without the concept of being able to override tag names in core, tools like this become fragmented because they have to explain to users why styled-components doesn't support it, and how they should either import some random utility component from the library itself always whenever they're creating their UI kit components, or how to re-implement that utility component themselves. It becomes a blocker, that creates a barrier to the kind of "innovation" (as much as I hate that word) that _could_ be possible.
So I really think it's something that should be in core. Because it makes styled-components a "complete" solution, and makes it truly a no-downside alternative way to write CSS and to build styled components.
But it would make sharing libraries and tools that want to rely on that ability much harder, because either every userland person would have to re-implement the shim, or they'd need to import it in separately from the library.
No... it wouldn't. It's internal logic to your styled component. You're still just exporting a component.
This is why this feature request has been so baffling to me. I've tried to explain why adding tagName to every SC goes against the design principles of Styled Components, while at every opportunity trying to show _how you can still accomplish your design without needing API changes_. Because you're always just working with a component API, so anything that's possible with components is still possible.
I appreciate you're going to a lot of effort here to make your case but I'm afraid it's just confusing me as to what exactly the issue is. There's no actual missing functionality here, it's just you'd prefer to be able to write:
<Box tagName='section' red marginLeft={3}>
instead of:
const Section = Box.extendWith('section')
<Section red marginLeft={3}>
without having to also do:
const Box = styled(({ tagName ='div', ...props }) => <div {...props}/>)`
...
`
If you think the point of a DSL is "to make the 90% simple and the 10% possible", I'm afraid we'll never come to an agreement. The point of a DSL is to expose a logically consistent subset of the available behaviour, in order to guide people towards a preferred design. A good DSL gets out the way when some functionality goes against that design, rather than extending itself to try to cover all bases, losing the original design goals in the process.
I agree this functionality is important for ui-kits and I've had to implement the suggested solution in a couple of company specific ui-kits (e.g. <Heading size={4} component="h1"/>, <Button href="/profile" component="a"/> and <Button to="/profile" component={Link}/>).
However, I think this functionality should be left to the ui-kits to implement and should not be built into the SC core. I believe that the author of a component should be in complete control of how their component is rendered, with the ability to delegate some of that control to the consumer if they so choose, and I think SC already achieves this elegantly. Adding a universal tagName prop would allow the component type to be changed by the consumer for any component, even ones where the component author doesn't desire it to be so, or doesn't support the tag type and could result in unusual and/or broken styles/behaviour.
I think fixing the whitelisting (as mentioned by @geelen), moving the suggested solution into an npm package and mentioning how to achieve this in the docs would be a great move in support for ui-kit authors. However, my vote would be strongly against adding a tagName or similar prop that limits my control as a component author, just to make authoring a subset of components easier.
So disappointing.
I think this decision will result in...
A big loss of accessibility to anything built using styled-components, because 90% of cases where a different tag+attribute would make apps usable by people who aren't the mainstream will be ignored, since it's not built in.
A loss of extra semantics to anything built using styled-components, where people won't use <article>, <date>, and other things that silently enable better tooling on the web.
Every single team that comes to styled-components from their existing BEM, Bootstrap, Smacss, Foundation, Pure, etc., etc., etc. UI Kit will end up running into this issue and be forced to solve it themselves, in continually diverging ways.
A loss in not-before-envisioned tooling in the UI Kit–building area, because people will end up implementing this without styled-components or in weird ways that are no longer composable because it isn't standardized.
This same issue will continue to be brought up in more threads as people try to offer accessibility or semantics, or build new types of flexible UI Kits with styled-components. Like this one, or me first encountering it in issue #723, or @steida's issue #246, or @diondiondion's issue #625, or @jeroenransijn's comment in #246.
It's weird that a custom theming syntax, which is surely used by less than 1% of users, and clearly shows styled-components's intention to be a truly reusable component-building framework, has made it into core before basic accessibility and semantics that are required for building websites, and that are available from every popular UI Kit out there.
It seems this stems from this use case being "strange" to the library authors—not that it's actually a dangerous feature to add. Because if you look at all of the discussions, the counter arguments seem to be:
Which is unfortunate when you consider @geelen's answer to a different feature request, arguing in favor of the power of the "affordance" of building something into core...
I don't quite understand why we should introduce additional API surface just to add functionality that's already trivial to do?
You could argue the same for Styled Components itself! 😉 Something being "trivial" for someone with a thorough understanding of the mechanics underneath is very different from something being afforded, even encouraged by the design of an interface.
I think attaching classnames to a styled component does deserve first-class treatment because of the architecture it permits. Small, atomic, static CSS as your "backbone" plus per-component CSS that's expressed as snippets of CSS. I think it's a real best-of-both worlds situation, not just for existing apps but for new projects too (i.e. I'd recommend this to a lot of teams that I've worked with recently)
Doing it manually seems like a lot of noise for not much signal:
const _X = styled.div` ... ` const X = ({ className, ...props }) => <_X className={`foo bar ${className}`} {...props} /> /* vs */ const X = styled.div.classes('foo bar')` ... `
But, oh well. I'm done.
I do hope another library becomes popular that solves this as a first-class concern.
I think it's a reasonably trivial feature for a ui-kit author to implement in their libraries. I just created react-create-component-from-tag-prop... hopefully that will make it even easier!
// my-library/View.jsx
import createComponentFromTagProp from 'react-create-component-from-tag-prop';
const TextFromTagProp = createComponentFromTagProp({
tag: 'p',
prop: 'as',
propsToOmit: ['size', 'bold', 'italic']
});
export default styled(TextFromTagProp)`
font-size: ${({size}) => size && `${size}px` || '12px'};
font-weight: ${({bold}) => bold && 'bold' || 'normal'};
font-style: ${({italic}) => italic && 'italic' || 'normal'};
`;
// my-app/View.jsx
import Text from './Text';
const Page = styled.section`
...
`;
const Form = styled.form`
...
`;
const Input = styled.input`
...
`;
export default () => (
<Page>
<Text as="h2" size={32} italic>Contact Us</Text>
<Form>
<Text as="label" size={10} bold htmlFor="name">Name</Text>
<Input id="name"/>
</Form>
</Page>
);
I think "anything built with styled-components" is a bit of an over generalisation. I think the use case for this feature is key to a subset of ui-kit features, like Heading, Button/Link and super generic utilities like Text or Box (which could be argued that SC mixins may suit super generic utils better). I'd love to know if there's many other real-world use-cases.
Nice one @jameslnewell! Adding a simple whitelist property to the constructor is a nice touch.
I think this thread has pretty much run its course—I think it's been useful as an explanation to future users why this apparently trivial feature is not implemented, but there's not much value in going around in circles.
The one thing I wanted to add is that my reasons for rejecting tagName are the same as supporting classes (now attrs). It's all about affordance—what practices do you want to guide people towards _every_ time they create an SC?
Thanks @jameslnewell, that looks like a nice little helper. :)
This same issue will continue to be brought up in more threads as people try to offer accessibility or semantics
I was one of those who opened an issue about this earlier, but that was before I knew about extendWith, which is a good enough solution to me. I actually come from a similar place as you and @colmtuite in that I was trying to build a bunch of super flexible base components that could be used to build any kind of layout within the constraints of my design system, but at some point in order to support the designs I was trying to build, these components reached a complexity that I wasn't comfortable with anymore. (I ended up writing something eerily similar to @jameslnewell's Spacing components which are great in their own way, but I decided not to go this route.)
Basically, what these components (and approaches like tachyons and bassCSS) do is abstract away the need to write CSS at all. But what I love about styled-components is how straightforward & easy to understand it makes the relationship between components and their styles! In other words, in my eyes it _removes the need to abstract away CSS_. In my projects, my JSX is already busy enough passing around props related to data, event handlers and display logic, so I now prefer to keep styling-related props to a minimum: mostly booleans, maybe a t-shirt size prop every now and then, but no "detailed" style assignments on CSS property level anymore like in some of your examples. I'd even consider code like this an anti-pattern:
<Spacing large margin top>
<Plane gray high>
<Spacing small padding>
<Text large black>A Modal Heading</Text>
<Text medium gray>A paragraph about whether you want to...</Text>
<Button solid blue>Accept</Button>
<Button hollow red>Cancel</Button>
</Spacing>
</Plane>
</Spacing>
In a component like this, the "source of thruth" of your design now lives in the props, and they're so generic that they don't really carry any meaning. Imagine if you had to create another component like this, with different content and less padding. If you copy & pasted this JSX and changed the props of Spacing, you'd now have to edit two separate components to change the properties they share, whereas with styled-components, you'd probably have something like this, where only props "relevant" to styling the modal are exposed, and you could easily duplicate the whole thing without negatively impacting maintainability:
<ModalWrapper>
<ModalWindow small>
<Heading>A Modal Heading</Heading>
<Message>A paragraph about whether you want to...</Message>
<Button primary>Accept</Button>
<Button>Cancel</Button>
</ModalWindow>
</ModalWrapper>
Just thought it was worth challenging the idea that styled-components' recommended approach to styling is somehow in opposition with consistent design systems.
But back to making tags configurable:
I quite like the idea of using JSX's dot notation instead of a normal prop to pick the tag you want a component to use. Not sure if attaching sub-components to other components like this is a bad idea (@geelen?), but it might be a good alternative if you know in advance which tag names you want to allow.
const Section = styled.section`
/* ...styles... */
`
Section.header = Section.extendWith('header')``
Section.footer = Section.extendWith('footer')``
const Heading = styled.h1`
/* ...styles... */
`
Heading.h2 = Heading.extendWith('h2')``
Heading.h3 = Heading.extendWith('h3')``
Heading.h4 = Heading.extendWith('h4')``
const TextLink = styled(Link)`
/* ...styles... */
`
TextLink.a = TextLink.extendWith('a')``
export default () => (
<Section>
<Heading>Section title (h1)</Heading>
<TextLink to="/home">React Router link</TextLink>
</Section>
<Section.footer>
<Heading.h2 size="xl">Footer title (h2)</Heading.h2>
<TextLink.a href="/home">Link using an a tag</TextLink.a>
</Section.footer>
)
Shipping in v2 as withComponent in https://github.com/styled-components/styled-components/pull/814
Any ideas why doesn't this code work (based on example from documentation)?
const LinkRed = Button.withComponent('a')`
color: tomato;
`
The error is: "TypeError: Cannot call a class as a function"
Is it only possible to use it this way?
const Link = Button.withComponent('a')
const LinkRed = Link.extend`
color: red;
`
I would prefer not to create additional component just for that.
@anikolaev you can chain both methods, so withComponent(X).extend
Please don't comment on unrelated issues, as it pings a lot of people at the same time. Feel free though to create a new issue :)
Most helpful comment
Hey, I'd like to jump back in here because I might be able to explain some of what @colmtuite is trying to get at, because we're both trying to achieve the same thing. (Him in a re-usable UI Kit way, and me inside my own app now that I've heard his ideas and realized they are really good.)
Before I start, I just want to say that I think the styled-components API is amazing. It's extremely simple to define components, in a way that feels almost like the "functional component" equivalent for the styling side of things. So the reason I'm arguing here is because I really want to make it the perfect fit for everything I'm doing, and I think this issue does that.
First off, what we're advocating for is not to allow, "free choice of tag for a styled component", as if there is no default to begin with. The API we'd like to see is actually extremely similar to the current styled-components API, but with the ability for the user to override the default
tagNamewhen creating the component element.Such that rendering a
Textcomponent will default to usingspan, since that's the least semantics-laden, and most useful for random UI contexts:But there are cases where you'll want to use that text in a place where the semantics are actually important, for example when using it for form-field labels...
So, the proposal isn't to allow creation of components that are _required_ to define their tag name at creation time, but instead to allow users to override the default tag name on creation, when semantics require it.
The second piece required for this argument is talking about _why_ we want to do this. And this is slightly harder to explain, because it's a different mentality for thinking about components than the usual styled-components codebase uses. It feels much more like the Bootstrap/Foundation-like, because it's a mentality geared towards creating extremely reusable styles, like those required for UI Kits that are used across huge (and/or multiple) code bases.
Right now many of the styled-components examples preach making tiny modules, where the styled and tag are tightly coupled. Usually you'll do this even in the same file, right above your "non-tiny" component that uses them.
This patterns works great—for certain use cases. It works amazingly for simple landing pages, for one-off product pages, for small open-source demos, etc.
However, it does not scale well when you're trying to create UI Kit–type frameworks, because in those cases you actually don't really want people defining one-off components all over the place, tailored to their exact needs. In those cases, what you want is to be able to define a series of tiny style abstractions, package them up in a reusable library, and then for the "users" to just compose those abstractions to create more complex apps.
This is a different way of thinking. I'm sure of it, because I'm currently transitioning an app from the old way to this new way.
The "composable" way of thinking of styling components is actually much more like the "HOC mentality" that React preaches. Whereas the "one-off/extend" way of thinking is like traditional OO inheritance. The composable approach results in flatter inheritance structures and much much fewer hidden dependency trees. Which leads to more manageable codebases.
This "composable" way is actually the "atoms" from Atomic Design. And here's a really good article that @colmtuite has written that explains the mentality.
In the "one-off" approach, you might do...
This is classic OO-style inheritance. And don't get me wrong, this works absolutely fine for small use cases. If I was building a single landing page for one of my open-source projects I would absolutely use this approach, it's simple, everything stays in one file, and it doesn't require massive forethought.
However, when you're getting into trying to build a UI Kit that can be re-used across your entire product—and often even across repositories—this doesn't scale. You end up with crazy styles added all over the places and the brand starts to fall apart because you're not consistent.
In those cases, what you want to be doing instead is using the "composable" approach. Which might instead look something like this:
Such that when you use your "atoms", later it looks like:
It's a different way of doing things. You're no longer extending components from each other and creating inheritance trees. Instead you're using them more like HOC components (except inline, so not exactly) to apply their very specific use case in each location.
Except the issue is that in this way of thinking, your styles are just simple "atoms" and shouldn't really care about what tag they are attached to. For example, you're just using a simple
<Text>component for a label, but right now there's not easy way to be like, "oh this invocation of<Text>should actually use a<label>tag, because that's what I'm using it for". If you were building it with the proper semantics, those examples might have looked like...Which is why we'd love to see something like
tagNameadopted. Because, in my opinion it's very very lightweight in terms of API footprint, but it opens up styled-components to entirely new use cases that aren't easily possible right now.Finally, of course, there are ways to get this functionality, as have been mentioned up-thread, but none that work without losing the whitelist, which results in a ton of React warnings...
But the other reason I'd like to see this in core, is because I think it is a core use case (in that many, many users will run into wanting to do this if they are building truly reusable components), and the I'd like to see it standardized at the library level, so that we don't end up with a ton of different ways to do it with different naming systems and approaches—since it's such a simple piece of logic to begin with.
And that said, given that React thinks
classNamevs.classwas a mistake, we should probably usetaginstead oftagNameto avoid making the same mistake just because they did it.Thanks for reading. Happy to discuss further.