Repository: svelte
Issue: Component style exposure mechanism
Labels: css

## Describe the ~~problem~~ context


I've been playing with Svelte for nearly a year now and it has been a major game changer for me. I'm absolutely in love with it 😍.
However, I'm still struggling when it comes to style generic components.

### **Long story short** 

Couple of month ago, I've started to build a component library for myself. Everything was going great until the question of styling. How do I allow future library users to style each component to benefit from mechanics but adapt them entirely to their application interface.
Styling is a big part of frontend development and so far, despite some funny stereotypes, I find CSS a pretty cool tool to achieve beautiful and intuitive interfaces. However, with svelte and scope-style, I can't use css out of the box to style the component wherever I use them. I have to either use javascript/typescript to expose style purpose variables, expose classes to do in DOM styling or break style scope (or multiply css variables in a specific and tricky way, I'll talk about it further as a workaround).

First, I really wish to keep styles scoped as much as possible. Second, exposing multiple css variables as component props is such a bad idea as it won't work without javascript, induces hard-to-read structure and is just unpleasant. So the only way to achieve components styling is to expose classes in a tailwind like way... Which kind of break style scoping, is unpleasant in my humble opinion and to summarize NOT CSS.

This is an issue that many svelte users are facing, and many ideas were proposed, mainly based on allowing component to be styled from outside. All these proposals were rejected, and rightly so. A list of some proposals and discussions is available below but I think that [**RFCs @pngwn comment**](https://github.com/sveltejs/rfcs/pull/22#issuecomment-664047806) beautifully summarizes maintainers opinion and the problems with the approaches so far proposed (**I strongly suggest you to read it**).


### **Long story LONG**

I do understand the desire of maintainers to keep svelte **opiniated** to avoid misuse.

> "Make the right thing easy, and the wrong thing possible." - [RFCs @pngwn comment](https://github.com/sveltejs/rfcs/pull/22#issuecomment-664047806)

Also, I share the vision that components should remain "their own bosses" meaning self-explainatory and aware of its exposed part. A component variable setting must be explicitly exposed by the component. This way, users can easily understand how the component works and how it should be used.

Therefore I completely agree with the negative response to the last feature requests regarding the opening of components to CSS class injection or similar concepts.

That said, I strongly disagree with the statement "component should not accept styles from outside". Waging war against parent styling children is waging war against theming and more generally genericity. Not having parent able to style components is drastically limiting component reuse which is one of the main advantage of Svelte and similar frameworks. CSS and styling of a component can't be isolated from context. 

In the current state svelte does not allow to expose the styles of a component in a comfortable way. HTML structure can be exposed using slot API. JS code can be exposed using props. But CSS styles have no exposure mechanism; understand self-explainatory, explicit and comfortable mechanism.

Of course we can still expose styles by exposing props to use in style attribute. It works but it present major drawbacks :
* it does not work without javascript,
* it's big boilerplate,
* it induces big component structure, multiplying props with a majority for styling purpose and not logic,
* it makes reading of styles harder,
* it implies working outside of the `<style>` tag whereas styling should just be modifiying `<style>` tag only.

Another way around is to use `:global()`. After reading many comments on the subject, at least everyone seems to agree that this is not a good and practical thing to do : This is equivalent to using global naming and does not take advantage of Svelte scoping. That's a problem everybody seems aware of.

> "This API (component styling API) should not open the flood gates of global cascading CSS, and it should offer excellent ergonomics, preferably taking advantage of the recent language tooling developments. We need a theming solution." - @pngw

**Related issues :**
 * [Introduce `<style module>` #6972](https://github.com/sveltejs/svelte/issues/6972)
 * [Passing class to components #2870](https://github.com/sveltejs/svelte/issues/2870)
 * [Support classes on nested components #2888](https://github.com/sveltejs/svelte/pull/2888)
 * [Allow scoped class to be passed to the child components #4843](https://github.com/sveltejs/svelte/issues/4843)
 * [Styles get uncorrectly removed / how to pass down styles? #4281](https://github.com/sveltejs/svelte/issues/4281)
 * [Allow parent to impact child component CSS rfcs#22](https://github.com/sveltejs/rfcs/pull/22)
 * [Provide classname utility for style encapsulation between parent/child component](https://github.com/sveltejs/svelte/issues/7776)
 * [CSS Component Selector](https://github.com/sveltejs/svelte/issues/7694)

**Related comments :**
 * [@pngwn comment, please read it 😂](https://github.com/sveltejs/rfcs/pull/22#issuecomment-664047806)
 * [@tncrazvan comment about exposing css](https://github.com/sveltejs/svelte/issues/6972#issuecomment-987711868)
 * [@geirmarius comment about css styles requiring parent control](https://github.com/sveltejs/svelte/pull/2888#issuecomment-554782656)
 * [@idoros comment about bypassing component style encapsulation](https://github.com/sveltejs/rfcs/pull/13#issuecomment-653936666)
 * [@aradalvand comment about recent hopes and potential solution](https://github.com/sveltejs/svelte/issues/6972#issuecomment-1371679372)
 * [@AdaptingAFM comment on slot style tag](https://github.com/sveltejs/rfcs/pull/66#issuecomment-1372325987)

---

## Describe the proposed ~~solution~~ concept


Inspired by [@tncrazvan idea](https://github.com/sveltejs/svelte/issues/6972#issuecomment-987711868), multiple comments and my own researches, I here present what I think would be the ideal mechanism to adress those problems. Keep in mind that by "Ideal mechanism" I only suggest that it addresses the previously presented problems while respecting Svelte opiniated philosophy.
The main idea is to allow a component to describe explicitly and precisely through CSS which styles are exposed to parent(s).

In `component.svelte`
```html
<div id="root" class="container">
	<span class="item">Hi</span>
	<span class="item everyone">Everyone !</span>
</div>

<style>

	#root{
		position : absolute;
		top : 50%;
		left : 50%;
		transform : translate(-50%,-50%);
	}

	span.item{
		font-family : Arial;
	}

// The css styles inside the :expose directive are exposed to parent styling

	:expose(span.item){
		color : red;
	}
	
	:expose(#root){
		width : 20vw; 
		height : 20vw;
	}
</style>
```

Note that we could also imagine styles completely exposed with a `<style expose>` tag in similar way of `<style global>`.

In `parent.svelte`
```html

	<div id="parent">
		<Component>
	</div>

	<style>

		#parent div{
			width : 13vw;
			height : 13vw; // Overrides the root exposed styles
			position : fixed; // Is ignored as position is not exposed
		}

		// What's important is the selected DOM element not the selector
		#parent #root{
			left : 43%; // Still ignored
			height : 14vw; // Still overriding default 20vw
		}

		#parent span.item.everyone{
			color : blue; // Overriding the color just for the second span with everyone class
		}
	</style>
```

Of course the exposed/overrided styles would benefit from scoping and hash naming. I don't know exactly how to achieve that at compilation but I do understand that it requires additional and potentially complex logic. However I could imagine compilation to give something like below, with overriding based on CSS rules position. 


```html

	<div id="parent">
		<div id="root" class="container hash1 hashExposed1">
			<span class="item hash2">Hi</span>
			<span class="item everyone hash2 hashExposed2">Everyone !</span>
		</div>
	</div>

	<style>
		hash1{
			position : absolute;
			top : 50%;
			left : 50%;
			transform : translate(-50%,-50%);
			width : 20vw; 
			height : 20vw;
		}

		hash2{
			font-family : Arial;
			color : red;
		}

		hashExposed1 {
			width : 13vw;
			height : 14vw;
		}

		hashExposed2{
			color : blue;
		}
	</style>
```

Or we could imagine an even more precise overriding based on the css rules comparison, which would require even more complex compilation logic, but no pain no gain 😁

I would be happy to work on this more deeply when time comes, but I think this is sufficient to understand the concept of my proposal.

This solution would allow CSS exposition through CSS which make a lot of more sense than exposing CSS variables through props.

---

## Describe the Alternative


### Description

In this journey of svelte css styling, I gradually developed an alternative solution based on native css variables. Although it works in most use cases, it is quite heavy.
 It looks more like a best practice guide.

> "This *-the solution-* does not exist today. **You can do an awful lot with css variables, more than people give them credit for**, but this isn't a solved problem" - @pngw

Whenever I create a component destined to be used in multiple context with different styles, I use css native variables to expose the css property that I consider stylable. For each 'exposed' css property, I define 2 native variable, one to be used outside by parent and one default variable which can store default value (quite handy for theming).

It gives me a pretty simple component that I can style from parent with default styles depending on global template. Paired with exposed class, we can also define multiple default templates.

In `component.svelte`
```html
<div id="root" component="component" class="container">
	<span class="item">Hi</span>
	<span class="item everyone">Everyone !</span>
</div>

<style>

	#root{
		position : var(--componentname-position, var(--default-componentname-position));
		top : var(--componentname-top, var(--default-componentname-top));
		left : var(--componentname-left, var(--default-componentname-left));
		transform : translate(-50%,-50%);
	}

	span.item{
		font-family : Arial;
	}
</style>
```

In a template global file `template.css`
```css

div[component="component"]{
	--default-componentname-position : relative;
	--default-componentname-left : 50%;
	--default-componentname-right : 50%;
}

```

In `parent.svelte`
```html

	<div id="parent">
		<Component>
	</div>

	<style>

		#parent{
			--componentname-top: 40%; // Overriding the default top value of component
		}
	</style>
```

This technique has numerous advantages :
* uses native features,
* do not break css scoping,
* do not use `:global()` in most cases,
* allows css rule inheritance,
* only concerns CSS and `<style>` tag,
* lets the component control its exposition,
* offers new ways of defining theme (through default variables). 

These are the main reasons that pushed me to introduce this solution as a pretty neat alternative to a css exposure mechanism. HOWEVER, some limitations remain (remember when I said *"an almost complete solution"* 😁)

#### Limitations

This alternative perfectly works when we use components inside native DOM elements but things get tricky when you wan't to use your stylable component inside other stylable component without having to add DOM elements.

#### Parent context

The first limitation is about parent context. If I use my component inside another and I want to apply a specific style depending on its parent context (focused, hovered or with a special class), I need to anticipate all of that inside the child component :

Say I have an `Icon` and a `Button` components.

`Icon.svelte`
```html

<svg class="icon" ... />

<style>
	svg{
		stroke : var(--icon-stroke, var(--default-icon-stroke));
	}
</style>
```

`Button.svelte`
```html
<div class="button">
<slot />
</div>

<style>
	.button{
		color : var(--button-color, var(--default-button-color));
	}

	.button:hover{
		color : var(--button-hover-color, var(--default-button-hover-color, var(--button-color), var(--default-button-color))) 
		// Note the ability to define fallbacks... 
	}
</style>
```

I can change the `Button` color depending on its context : hover or not. But if I wan't to use an `Icon` directly inside the `Button`, I can't change its stroke color depending on `Button` context.

`anywhere`
```html
<div class="root">
	<Button>
		<Icon />
	</Button>
</div>

<style>
	.root{
		--button-color : white;
		--button-hover-color: grey;
		--icon-stroke : white;
	}
</style>
```

I actually found a solution which works for 1 level but kind of break the idea of component "independancy" and agnostic of its context :

`Icon.svelte`
```html
<style>
	svg{
		stroke : var(--icon-stroke, var(--default-icon-stroke));
	}

	:global(*:hover) > svg{
		stroke : var(--icon-parent-hover-stroke, var(--default-icon-parent-hover-stroke));
	}
</style>
```

`anywhere`
```html
<style>
	.root{
		--button-color : white;
		--button-hover-color: grey;
		--icon-stroke : white;
		--icon-parent-hover-stroke : grey;
	}
</style>
```

Note that it won't work as such if Icon is not a direct children of Button and also that this solution adds a ton of global css rules...


#### Component variants

Similarly, multiple questions arise when we wan't to use stylable components inside others without adding native DOM element. For example when I wan't to create a new stylable component based on a combination of others such as a `Button` variant with an Icon, several questions remain about how to treat their css variables :

`IconButton.svelte`
```html
<Button>
	<Icon />
	<slot />
</Button>
```

* Should I expose Icon prop class to be able to apply specific template to it ?
* Should default styles of Icon written inside IconButton selector ?
* Should I override Icon css variables ? By wrapping them.

Most of the time the answer depends on the usecase and induce tradeoffs.

Also, the idea of default styles works great for 1 level composition but gets harder when using mutliple ones...

This limitation in composition can be avoided with additional native html tag addition which is not catastrophic but not ideal either. It might also disappear with the apparition of `:has()` css selector.


## Importance

would make my life easier. However, crucial to talk about at least.

## Final words

I don't pretend to offer the perfect solution. But the mechanism I propose has the advantage of meeting a specification that respects the vision of the maintainers while enhancing developer experience :

* Components control their exposition.
* Styles are scoped.
* CSS style has its own exposition API.

This mechanism would drastically enhances svelte potential by unblocking component genericity and, at the same time, reduces bad practices such as using `:global()`.

Note that my goal is not to revive aggressive debates ! I do not have the energy to reply to aggressive thoughts. I'm not particularly attached to my proposal either, I just want to refocus debates on finding an acceptable solution to this aknowledged problem of styling... Maybe the solution relies in the alternative I've presented which I would happily discuss. I've opened a new issue as I wanted to regroup discussions about styling issues while providing a fresh start and stepping out of the aging huge general issue [#6972](https://github.com/sveltejs/svelte/issues/6972). However, if the maintainers prefer, I can close it and change it as an answer.