Skip to content
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

CSS-in-HTML #6

Closed
wants to merge 1 commit into from
Closed

CSS-in-HTML #6

wants to merge 1 commit into from

Conversation

arxpoetica
Copy link
Member

@arxpoetica arxpoetica commented Nov 23, 2018

Rendered

Even though this is an incomplete proposal, there's been so much conversation in the #future Discord channel, I wanted to give us a place to record some of the conversation more inline with the (ongoing) proposal.

Expect some of those conversations to start to land.

If we find this PR to become too encumbered with old ideas, I'll close it and reopen a new one after incorporating some of the top / sanctioned ideas.

But for now, consider this just a good place to capture ongoing discussion.

@arxpoetica
Copy link
Member Author

arxpoetica commented Nov 25, 2018

Css vars via @lukeed:

<div class="demo">    
    <button on:click={() => value++}>Increment</button>
    <button on:click={() => value--}>Decrement</button>
</div>

<script>
    export let value = 0;
</script>

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
        /* this is dynamic */
        color: {value % 2 ? green : red};
    }
</style>

<!-- 
  Output (legacy:true)
    needs :yak: symbol because 
    it writes inline style properties
    but will overwrite anything meant to cascade
-->

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
    }
</style>

<div class="demo" style="color:green;">...</div>
<div class="demo" style="color:red;">...</div>

<!-- 
  Output (legacy:false)
    sets (--var) values inline, which
    will respect cascade bcuz does not 
    change the style-rules or their order
-->

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
        color: var(--color);
</style>

<div class="demo" style="--color:green;">...</div>
<div class="demo" style="--color:red;">...</div>

@arxpoetica
Copy link
Member Author

@timhall
Copy link

timhall commented Dec 7, 2018

Global

This has been discussed elsewhere, but I think building off of #4 with the multiple script tag approach (with context="...") could work well here:

  • By default, all <style> tags are scoped to the component and have the current svelte behavior
  • <style context="global"> is used to scope all contained styles globally
<style context="global">
  body { background: #111; }
  h1 { font-size: 3rem; }

  /* Equivalent to:
  :global(body) { background: #111; }
  :global(h1) { font-size: 3rem }
  */
</style>
<style>
  a { color: white; } 
</style>

Cascade

For cascading styles (apply from component to children), .container :global(...) would likely still be needed since svelte doesn't have an explicit top-level element to attach styles to (i.e. need to define .container to attach cascading styles). I'd argue that for applying styles to child components, the child should have control over how those styles are applied. This could be through:

  1. Properties - Component exposes semantic properties that map internally to styles. This is generally preferred, but is certainly less flexible and only works for known cases
  2. Classes - Component uses specific classes to target parts of the component. This works currently, but relies on knowing what classes the component uses internally
  3. CSS Shadow Parts proposal - Use part="name" in html and Component::part(name) in css to target certain elements in the component. This allows the component to define an external css API that consumers can style, making it flexible while keeping the component in control
<style>
  section :global(.Post__title) { color: blue; }
  section Post::part(body) { line-height: 1.5; }

  /* Equivalent to
  section.svelte-id .Post__title { color: blue; }
  section.svelte-id [part="body"].svelte-Postid { line-height: 1.5; }
  */
</style>

<section>
  <Post primary title="..." body="..." />
</section>
<!-- Post.html -->
<h1 class="Post__title">>{title}</h1>
<!--       ^ 2. Classes -->

<div part="body" style="color: {primary ? 'blue' : 'grey'}">{body}</div>
<!-- ^ 3. Parts                 ^ 1. Properties -->

Edit: additionally, it looks like ::theme can also be used as sort of a :global for parts (see `::part and ::theme, an ::explainer)

<style context="global">
  :root::theme(body) { background: orange; }

  /* Equivalent to
  :root [part="body"] { background: orange; }
  */
</style>
<style>
  section::theme(body) { background: yellow; }

  /* Equivalent to
  section.svelte-id [part="body"] { background: yellow; }
  */
</style>

(I think that's how ::theme is proposed, can't find a spec for that aspect)


Edit: added some notes on the cascade options

@lukeed
Copy link
Member

lukeed commented Dec 7, 2018

RE @timhall's last point:

  • Not sure how I feel about introducing a new part attribute when we already have named slots

  • Nothing inside Post component indicated that .Post__title would not be scoped. How would we know (from the Parents' perspective) that that class name was a valid destination?

  • I think if we are to use the Component's name directly within style selectors, it might make more sense to change that syntax a bit:

    <style>
      section :global(...) { 
        /* something global */
      }
      section :Post(.Post__title) {
        /* stemming from Post's ".Post__title" class */
        /* I will work regardless of scoped/global output */
      }
      section :Post('slotname') {
        /* I am targeting the <slot name="slotname"/> element  */
        /* ...Or the "part"'s name, if we could not work with existing <slot> mechanics   */
      }
    </style>

    The idea here is that we follow the :global and :local convention (which are context/scoping flags, essentially) and using them to indicate a scope/context of the Component (:Component).

@arxpoetica
Copy link
Member Author

arxpoetica commented Dec 7, 2018

@timhall regarding scoping via separate <style> tags, that idea presents problems w/ preprocessors when one doesn't want to duplicate nested styles for the sake of separate tags. Illustrated example for clarity:

Method with scoped tags:

<style context="global">
	.component {
		font: inherit;
		h1 { margin: 0 0 0.2rem; }
		h2 { margin: 0; }
		p { color: goldenrod; }
	}
	/* precompiles to:
		.component { font: inherit; }
		.component h1 { margin: 0 0 0.2rem; }
		.component h2 { margin: 0; }
		.component p { color: goldenrod; }
	*/
</style>
<style>
	/* overriding locally */
	.component {
		color: pink;
		font: 1.6rem/1.2 Helvetica;
		h1 { margin: 0; }
		h2 { margin: 0 0 2rem; }
		p { color: inherit; }
	}
	/* precompiles to:
		.component {
			color: pink;
			font: 1.6rem/1.2 Helvetica;
		}
		.component h1 { margin: 0; }
		.component h2 { margin: 0 0 2rem; }
		.component p { color: inherit; }
	*/
</style>

Alternate proposal with scoped @global (🐃) selector:

<style>
	.component {
		color: pink;
		font: 1.6rem/1.2 Helvetica;
		h1 { margin: 0; }
		h2 { margin: 0 0 2rem; }
		p { color: inherit; }
		@global {
			font: inherit;
			h1 { margin: 0 0 0.2rem; }
			h2 { margin: 0; }
			p { color: goldenrod; }
		}
	}
	/* precompiles to:
		.component {
			color: pink;
			font: 1.6rem/1.2 Helvetica;
		}
		.component h1 { margin: 0; }
		.component h2 { margin: 0 0 2rem; }
		.component p { color: inherit; }
		@global {
			.component { font: inherit; }
			.component h1 { margin: 0 0 0.2rem; }
			.component h2 { margin: 0; }
			.component p { color: goldenrod; }
		}
	*/
</style>

It's a pedantic difference, but I find the latter grouping / clustering much more easy to deal w/, arrange, and (in part) keep DRY.

@arxpoetica
Copy link
Member Author

@lukeed > The idea here is that we follow the :global and :local convention

I am in favor of (if not abandoning) those microscopic selectors (they cause a lot of clutter), at least replacing with something that encapsulates better, i.e., a (🐃) @global and @cascade selector, or something better. Maybe we don't get rid of :global entirely since there are probably good one-off use cases, but there's still the problem of repeating :global multiple times when enclosure is better.

@lukeed
Copy link
Member

lukeed commented Dec 7, 2018

Was just following the rest of V3's approach of reusing existing syntax. I'm not familiar enough with latest improvements to CSS modules/PostCSS, but to me it's more appealing to construct CSS in a way that these tools can understand, in the event I'm porting styles to & from Svelte SFCs to standalone stylesheets.

@timhall
Copy link

timhall commented Dec 7, 2018

@arxpoetica I think your example is a good case for preprocess plus postcss-nested and postcss-global-nested and why building on the "standard"ish :global is good since we can leverage existing tools (which is the svelte way).

.component {
  color: pink;
  font: 1.6rem/1.2 Helvetica;
  h1 { margin: 0; }
  h2 { margin: 0 0 2rem; }
  p { color: inherit; }
  :global {
    font: inherit;
    h1 { margin: 0 0 0.2rem; }
    h2 { margin: 0; }
    p { color: goldenrod; }
  }
}

/* Transformed in preprocess using postcss-nested and postcss-global-nested to */

.component {
  color: pink;
  font: 1.6rem/1.2 Helvetica;
}
.component h1 { margin: 0; }
.component h2 { margin: 0 0 2rem; }
.component p { color: inherit; }
.component :global (
  font: inherit;
}
.component :global(h1) { margin: 0 0 0.2rem; }
.component :global(h2) { margin: 0; }
.component :global(p) { color: goldenrod; }

Alternatively, I've run into many issues with using global css frameworks like bootstrap or tailwindcss and getting about a thousand unused style warnings so it'd be nice to have an escape hatch for processing style blocks from a global context.


For overloading slot, that is an interesting possibility, but I can't think of when you'd want to apply styles directly to the slot as opposed to the containing element. Also, you may not want the consumer overriding what's in the slot, just want to give a place to attach styles.

<!-- Button.html -->
<button part="button"><slot name="button" /></button>
<Button>Howdy!</Button>

<style>
Button::part(button) {
  /* Applies to the button element itself */
}
:Button('button') {
  /* Applies to what is contained in the slot */ 
}
</style>

Edit: Fix preprocess (postcss) transform

@lukeed
Copy link
Member

lukeed commented Dec 7, 2018

My main hesitation with the proposed ::part(button) (or even :Button(button)) idea is that it's not obvious that this is a custom name. This is why I went for the string format.

In this last example, it looks okay because button is both the name & the DOM tag, so it matches up. But, from the Parent's perspective, it reads like you're applying to all <button>s inside the Button component.

Similarly, in the first example you provided (::part(body)) this reads like the <body> selector within Component. The syntax highlighting will also render it as such (as seen here in GitHub too).

I think we should be allowed to target DOM elements within the Component. But if we're to add styles to parts/slots, that those have to be passed thru as name strings for clarity & distinction.

@timhall
Copy link

timhall commented Dec 7, 2018

That's a very good point and I think a confusing part of the proposal, with part="value" using a string and ::part(value) using a bare identifier. I think I prefer using a string in both places for consistency, will have to check if there are any established cases with pseudo elements leaning one way or the other


Edit: Looks like it's not unprecedented to use the bare string in pseudo-classes with :lang(...) (from MDN), but when I played with it quoted is valid too, so it doesn't seem to matter from a spec point of view whether it's quoted or unquoted.

@lukeed
Copy link
Member

lukeed commented Dec 7, 2018

Aside

FWIW, before continuing, I should clarify my idea about using <slot>s...

For it to be useful, Svelte slots would have to operate like Vue's in that they can exist on their own (Svelte's current & only approach) or be applied onto an another element (much like @timhall's part="" idea:

<slot />
<slot name="foo" />
<div class="hello" slot="bar" />

If this were possible, then part="" would be redundant & should not be added IMO.

However, even if @Rich-Harris allowed this to happen (I don't think he's a fan? hence current implementation), I don't know if they realistically would be very enjoyable to use because you (as the Parent component) would need to know if the slot you're selecting is one that disappears or if it's named DOM Element.

I mean, either thru <slot> | slot="" or part="" you (the Parent) always require working knowledge of the Child components you're manipulating

🤔


I'd be happy with this & think it solves a lot of the conversation re: Component style inheritance & overwrites:

<!-- Actions.html -->
<div class="actions">
  <div slot="primary" />
  <div slot="secondary" />
  <button>Cancel</button>
</div>
<!-- Parent.html -->
<Actions>
  <button slot="primary">Submit</button>
  <button slot="secondary">Edit</button>
</Actions>

<style>
:Actions(button) {
  /* apply to all <button> within Actions */
  /* ==> button x3 (Submit, Edit, Cancel) */
}

:Actions('primary') {
  /* applies to the "div[slot=primary]" element directly */ 
  /* ==> <div> x1 */
}

:Actions('primary') button {
  /* applies to the "div[slot=primary] button" element directly */ 
  /* ==> <button> x1 (Submit) */
}

:Actions('secondary') button {
  /* applies to the "div[slot=secondary] button" element directly */ 
  /* ==> <button> x1 (Edit) */
}
</style>

@arxpoetica
Copy link
Member Author

@timhall (and @lukeed) there's a lot to unpack here, so let's just take it one step at a time.

How exactly is this a correct precompile translation:

.component {
  :global {
    font: inherit;
    h1 { margin: 0 0 0.2rem; }
    h2 { margin: 0; }
  }
}
/* Transformed in preprocess using postcss-nested and postcss-global-nested to */
:global(.component) {
    font: inherit;
}
.component :global(h1) { margin: 0 0 0.2rem; }
.component :global(h2) { margin: 0; }

Wouldn't it compile to:

.component :global { font: inherit; }
.component :global h1 { margin: 0 0 0.2rem; }
.component :global h2 { margin: 0; }

Or is their something special in postcss-nested* that compiles anything beginning with a : colon to parenthetical nestings?

@timhall
Copy link

timhall commented Dec 11, 2018

@arxpoetica Oops, fixed the .component :global { ... } transform in my comment, but the rest is the output of postcss-global-nested. I imagine it has special handling :global, transforming it to :global(...). I believe my point stands, in that svelte using the :global(...) primitive follows the css module "standard" and has support from existing tools that allow for nesting and other helpful behaviors.

@hybridwebdev
Copy link

Css vars via @lukeed:

<div class="demo">    
    <button on:click={() => value++}>Increment</button>
    <button on:click={() => value--}>Decrement</button>
</div>

<script>
    export let value = 0;
</script>

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
        /* this is dynamic */
        color: {value % 2 ? green : red};
    }
</style>

<!-- 
  Output (legacy:true)
    needs :yak: symbol because 
    it writes inline style properties
    but will overwrite anything meant to cascade
-->

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
    }
</style>

<div class="demo" style="color:green;">...</div>
<div class="demo" style="color:red;">...</div>

<!-- 
  Output (legacy:false)
    sets (--var) values inline, which
    will respect cascade bcuz does not 
    change the style-rules or their order
-->

<style>
    .demo {    
        padding: 10px;
        border-radius: 4px;
        -webkit-appearance: none;
        font-style: italic;
        color: var(--color);
</style>

<div class="demo" style="--color:green;">...</div>
<div class="demo" style="--color:red;">...</div>

JS variables inside style blocks is a TERRIBLE idea. The nightmare scenarios this would create in terms of debugging would just be astronomical. Better to keep the 2 separate. If you find a use case for something like this, then aphroditeJs or other CSS js frameworks would be more ideal.

@Rich-Harris
Copy link
Member

Closing this in favour of #13

@arxpoetica arxpoetica deleted the css-in-html branch October 31, 2020 18:55
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants