426 lines
14 KiB
Plaintext
426 lines
14 KiB
Plaintext
# General concepts
|
||
|
||
## Component styling
|
||
|
||
Out of the box, these components are completely unstyled. This has some big advantages: you're free to make them look however you want, without having to fight against any pre-existing styles that don't fit in with the look and feel of your site. Instead of being constrained by an existing design system like Material Design or Carbon Components, you can fully customize your components' appearance--but letting the library take care of implementing all the keyboard and mouse interactions, accessibility features, and so on.
|
||
|
||
The flipside of unstyled components, however, is that you need to, well, style them. There are a number of ways of doing this.
|
||
|
||
### Styling children: slot props
|
||
|
||
You can style the descendants of components from this library with the help of <a href="https://svelte.dev/tutorial/slot-props">slot props</a>:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption value="startup" let:checked>
|
||
<span class:checked>Startup</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption value="business" let:checked>
|
||
<span class:checked>Business</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption value="enterprise" let:checked>
|
||
<span class:checked>Enterprise</span>
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
|
||
<style>
|
||
/* Note that using global styles this way is bad practice in larger applications; see below for more */
|
||
:global(.checked) {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
### Styling the components themselves: `class` and `style`
|
||
|
||
Slot props can be used to style any descendent components, but they cannot be used to style the same component that defines them. To work around this, any component that defines slot props will also optionally accept a **function** for its `class` and/or `style` props. These functions will be **called with the slot props as an argument**:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption
|
||
value="startup"
|
||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||
>
|
||
<span class:checked>Startup</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="business"
|
||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||
>
|
||
<span class:checked>Business</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="enterprise"
|
||
class={({ checked }) => (checked ? "checked" : undefined)}
|
||
>
|
||
<span class:checked>Enterprise</span>
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
|
||
<style>
|
||
/* Note that using global styles this way is bad practice in larger applications; see below for more */
|
||
:global(.checked) {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
See the individual component documentation to learn about the slot props for each component.
|
||
|
||
You may also pass a string for `class` or `style`, or omit them entirely, of course.
|
||
|
||
What follows below is some guidance on the different ways to use the `class` and `style` props to style your components.
|
||
|
||
#### Using Svelte's `<style>` tags
|
||
|
||
Svelte comes with an out-of-the-box solution for component styling: using the `<style>` tag inside your components. This generates CSS that is scoped to an individual component instance, letting you do things like this without worrying about your CSS selectors conflicting with other components:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption value="startup" let:checked>
|
||
<span class:checked>Startup</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption value="business" let:checked>
|
||
<span class:checked>Business</span>
|
||
</RadioGroupOption>
|
||
<RadioGroupOption value="enterprise" let:checked>
|
||
<span class:checked>Enterprise</span>
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
|
||
<style>
|
||
.checked {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
This works great for styling HTML elements. However, it (unfortunately) does **not** work for styling _components_. If you try to create the same example without the `<span>`s, it won't work:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption
|
||
value="startup"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Startup
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="business"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Business
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="enterprise"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Enterprise
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
|
||
<!-- This doesn't work! -->
|
||
<style>
|
||
.checked {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
(Note that we also can no longer use the `class:checked` shorthand on a component)
|
||
|
||
Why doesn't this work? When Svelte compiles our component, it adds in a special unique class to the CSS rules and to the HTML elements rendered by this component that are affected. But it does _not_ add this special class to any child _components_. The result is that the `.checked` CSS rule will be generated with a second class that the `<RadioGroupOption>` components do not have, and it will not match.
|
||
|
||
Unfortunately there is no way in Svelte right now to pass this special class down to the `<RadioGroupOption>` components, and the framework does not have a good solution in general for this. Hopefully one day the Svelte maintainers will add some way of solving this problem. Until then, you can still style your components using one of the approaches below.
|
||
|
||
#### The `:global()` modifier
|
||
|
||
The `:global()` modifier opts your rule out of the default component style scoping. CSS rules inside `:global()` are treated as "normal" CSS, so the following will work:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption
|
||
value="startup"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Startup
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="business"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Business
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="enterprise"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Enterprise
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
|
||
<!-- WARNING: This works, but may not be desirable; see below -->
|
||
<style>
|
||
:global(.checked) {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
However, global CSS has one critical downside: it's global! The `.checked` rule above will now apply to _any_ `.checked` CSS class in your application, even if it's in other components. In a larger application, this can get difficult to maintain.
|
||
|
||
You can scope this more narrowly if you have a wrapper element:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<div>
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption
|
||
value="startup"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Startup
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="business"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Business
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="enterprise"
|
||
class={({ checked }) => (checked ? "checked" : "")}
|
||
>
|
||
Enterprise
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
</div>
|
||
|
||
<!-- This will only apply to .checked elements that descend from this component -->
|
||
<style>
|
||
* > :global(.checked) {
|
||
background-color: rgb(191 219 254);
|
||
}
|
||
</style>
|
||
```
|
||
|
||
In this case, this works perfectly.
|
||
|
||
When using this scoped `* > :global()` approach, keep in mind these principles:
|
||
|
||
- Your Headless UI components must be contained inside some element **that is rendered by your component**, like the wrapper `<div>` above. You need an element for the Svelte scoped class to be attached to.
|
||
- Your rule will be scoped to your component **and any descendant**. In the example above this is not a problem, since the component has no `<slot>`s. But if your component _does_ have `<slot>`s, you will still need to be careful for collisions.
|
||
- This rule won't work properly inside a portal, which means that it won't work properly for a `<Dialog>`. To style the `<Dialog>`, you will need to either 1) use global styles without the `* >` scoping, 2) put a wrapper element inside the `<Dialog>` and style that instead, or 3) use one of the other styling approaches.
|
||
|
||
#### Inline styles
|
||
|
||
You can style any component using inline styles using the `style=...` prop:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
RadioGroup,
|
||
RadioGroupLabel,
|
||
RadioGroupOption,
|
||
RadioGroupDescription,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
let plan = "startup";
|
||
</script>
|
||
|
||
<div>
|
||
<RadioGroup value={plan} on:change={(e) => (plan = e.detail)}>
|
||
<RadioGroupLabel>Plan</RadioGroupLabel>
|
||
<RadioGroupOption
|
||
value="startup"
|
||
style={({ checked }) =>
|
||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||
>
|
||
Startup
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="business"
|
||
style={({ checked }) =>
|
||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||
>
|
||
Business
|
||
</RadioGroupOption>
|
||
<RadioGroupOption
|
||
value="enterprise"
|
||
style={({ checked }) =>
|
||
checked ? "background-color: rgb(191 219 254)" : undefined}
|
||
>
|
||
Enterprise
|
||
</RadioGroupOption>
|
||
</RadioGroup>
|
||
</div>
|
||
```
|
||
|
||
As mentioned above, this prop accepts a function whose input will be the component's slot props, allowing you to apply conditional styles.
|
||
|
||
One downside of inline styles is that you cannot use them to style CSS psuedoselectors, like `:focus` and `:hover`. In general this library will provide you with states that make these psuedoselectors unnecessary, however.
|
||
|
||
#### Using a CSS framework
|
||
|
||
You can always use the `class` prop as normal alongside a CSS framework like Bootstrap, Bulma, Tailwind, etc. The original version of this component library was written by the same team that maintains Tailwind CSS.
|
||
|
||
## Event forwarding
|
||
|
||
This library will forward all events to the underlying elements, so you can add your own event handlers if necessary:
|
||
|
||
```svelte
|
||
<script>
|
||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||
let enabled = false;
|
||
</script>
|
||
|
||
<Switch
|
||
checked={enabled}
|
||
on:change={(e) => (enabled = e.detail)}
|
||
on:click={() => console.log("Clicked!")}
|
||
>
|
||
Toggle me!
|
||
</Switch>
|
||
```
|
||
|
||
Note that the library already handles all of the keyboard and mouse events you'd need to implement the component functionality. You don't need to add a handler to e.g. open or close a popover when the popover button is clicked.
|
||
|
||
You can also use event modifiers, but they must be separated by ! instead of the normal |, for technical reasons:
|
||
|
||
```svelte
|
||
<script>
|
||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||
let enabled = false;
|
||
</script>
|
||
|
||
<Switch
|
||
checked={enabled}
|
||
on:change={(e) => (enabled = e.detail)}
|
||
on:click!once={() => console.log("Clicked!")}
|
||
>
|
||
Toggle me!
|
||
</Switch>
|
||
```
|
||
|
||
## Using Svelte actions
|
||
|
||
You can use [actions](https://svelte.dev/tutorial/actions) with this library as well. The Svelte `use:` directive is not supported for Components, so you need to use the `use` prop instead:
|
||
|
||
```svelte
|
||
<script>
|
||
import { Switch } from "@rgossiaux/svelte-headlessui";
|
||
import { action1, action2, action3 } from "./my-library";
|
||
let action1options = {};
|
||
let action3options = { foo: true };
|
||
|
||
let enabled = false;
|
||
</script>
|
||
|
||
<Switch
|
||
checked={enabled}
|
||
on:change={(e) => (enabled = e.detail)}
|
||
use={[[action1, action1options], [action2], [action3, action3options]]}
|
||
>
|
||
Toggle me!
|
||
</Switch>
|
||
```
|
||
|
||
The `use` prop must be a list of `[action]` or `[action, options]` items. These actions will be applied to the underlying element that the component eventually renders.
|
||
|
||
## Customizing the elements rendered
|
||
|
||
Each component in this library renders as some specific HTML element. By default, this element will be something that makes sense for that particular component: a `Label` component renders as `<label>`, a `Button` component renders as a `<button>`, etc. These defaults can be found on each component's page.
|
||
|
||
You're free to change the element that any component renders as using the `as` prop, which is available on every component. This prop can take a string referring to an HTML element to use instead:
|
||
|
||
```svelte
|
||
<script>
|
||
import {
|
||
Menu,
|
||
MenuButton,
|
||
MenuItems,
|
||
MenuItem,
|
||
} from "@rgossiaux/svelte-headlessui";
|
||
</script>
|
||
|
||
<Menu>
|
||
<MenuButton>More</MenuButton>
|
||
<!-- Render a `ul` instead of a `div` -->
|
||
<MenuItems as="ul">
|
||
<!-- Render a `li` instead of an `a` -->
|
||
<MenuItem as="li" let:active>
|
||
<a href="/account-settings" class={`${active ? "active" : "inactive"}`}
|
||
>Account settings</a
|
||
>
|
||
</MenuItem>
|
||
<!-- ... -->
|
||
</MenuItems>
|
||
</Menu>
|
||
```
|
||
|
||
Due to technical limitations in Svelte, each one of these elements must be handled individually by the library. Most elements should be supported, but some uncommon ones might not be. If you find yourself wanting to use an element that is _not_ currently supported, please [file a GitHub issue](https://github.com/rgossiaux/svelte-headlessui/issues).
|