Scoped CSS styles
When defining a component or LiveView, you can declare its CSS styles directly in the template
using <style>
or in a colocated .css
file. The Surface compiler will treat those styles as scoped
styles so any CSS declaration will apply only to the related component.
Some of the benefits are:
- Better code organization as each component style can be defined alongside its component
- Avoid conflicts between components rules as each declaration is scoped per component
- Prevent CSS rules from the parent components from leaking into child elements or other components.
-
Support injecting elixir expressions into the CSS declarations using
s-bind()
, allowing users to apply dynamic values to CSS properties based on the components' assigns. One great thing about this approach is that it keeps the CSS syntax valid so it doesn't affect tools that depend on valid CSS syntax like editor highlighters, linters, etc. -
Zero-configuration in
app.css
or any other file when importing new components, including components from dependencies. The compiler will automatically collect and process all component-related styles seamlessly.
Usage with colocated .css
file
Create a colocated .css
file with the same base name of the component. Like:
my_app_web/components
├── ...
├── card.ex
├── card.css
All CSS rules defined in that file will be scoped by the related component.
Usage with <style>
Place your CSS rules inside a <style>
tag as the first node of your template.
Example
def render(assigns) do
~F"""
<style>
.tag {
@apply bg-gray-200 rounded-full px-3 py-1 text-sm font-semibold text-gray-700 mr-2;
}
</style>
<div>
<span class="tag">#surface</span>
<span class="tag">#phoenix</span>
<span class="tag">#tailwindcss</span>
</div>
"""
end
As with colocated files, all CSS rules defined inside <style>
will be scoped and applies only to the
local HTML elements.
How does it work?
Scoped CSS is achieved by instructing the compiler to inject a data-s-*
attribute to elements
affected by the CSS declarations. The selectors are also translated so they can only apply to the
related elements.
In the last example, the translated CSS code would look something, like:
.tag[data-s-9651d1c] {
@apply px-3 py-1 mr-2 rounded-full font-semibold text-sm text-gray-700 bg-gray-200;
}
And the generated HTML code, something like:
<div>
<span data-s-9651d1c class="tag">...</span>
<span data-s-9651d1c class="tag">...</span>
<span data-s-9651d1c class="tag">...</span>
</div>
Deep selectors
By default, the scoped CSS declarations only apply to the elements defined by the component itself. However, sometimes it might be useful to bypass that rule. For instance, when you're creating a parent component that will be responsible for the layout of its children.
For those cases, you should use the :deep()
pseudo-class:
.parent :deep(.child) {
...
}
Global selectors
In order to apply styles to child components based on global classes, you need
to use the :global()
pseudo-class so instruct the compiler to not add scope
information to the related elements.
Common cases are theme-related and global status classes, e.g. .dark
,
.phx-connected
and .phx-loading
.
:global(.dark) .link {
...
}
Dynamic property values
You can inject elixir expressions into CSS property values using s-bind()
Example
.btn {
background-color: s-bind('@color');
}
Where @color
is a component assign but could be any valid elixir expression that will be evaluated at runtime.
Limitations
The CSS world is extremely large and there are many different valid solutions to solve the same problem. None of them are silver bullets. It's always about trade-offs. Most of the solutions out there implementing scoped CSS require extra tooling. Which is fine, especially if that tooling is already in your stack.
For Surface, we wanted to bring a solution that would cover the most common cases and depended only on its built-in compiler. Our current solution was inspired by Vue's SFC CSS Features.