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. -
Auto generate Taiwind variants based on
prop
/data
using thecss_variant
option. -
Zero-configuration in
app.css
or any other file when adding new Surface components, including those from dependencies. The compiler will automatically collect and process all Surface 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.
Scopes by module
By default, styles defined in a colocated .css
file are scoped by module, which means any function
components defined in the same module will belong to the same scope. If you need a function component
to define its own scope, use <style>
in that component instead.
How does it work?
Scoped CSS is achieved by instructing the Surface compiler to inject a 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[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 s-9651d1c class="tag">...</span>
<span s-9651d1c class="tag">...</span>
<span s-9651d1c class="tag">...</span>
</div>
Deep selectors
By default, the scoped CSS declarations only apply to the elements defined in the same module. 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.
Generating CSS variants (Tailwind only)
CSS variants provide a declarative way to conditionally apply certain CSS classes according
to their values. You can instruct the Surface compiler to automatically generate tailwind variants
based on defined assigns. In order to define CSS variants for your templates, you can use the css_variant
option, which is available for both, prop
and data
.
Note: If you have initialized your project using
mix surface.init
, you should have CSS variants already enabled. If that's not the case, you can anable it by following the instructions in Surface compiler's options.
Example
prop loading, :boolean, css_variant: true
prop size, :string, values: ["small", "large"], css_variant: true
By declaring css_variant
, a set of default variants will be automatically available in your templates
and can be used directly in any class
attribute. By default, all variants' names will
start with the @
prefix.
Example
<button class="@loading:opacity-75 @size-small:text-sm @size-large:text-lg">
Submit
</button>
CSS variants naming convention
Depending on the type defined for your prop/data, as well as the options passed, a different set of variants may be generated for each possible value.
Here's the default naming convention for each type:
Boolean
-
The
assign-name
itself, iftrue
. -
not-[assign-name]
, iffalse
.
Example:
<div class="@loading:opacity-75 @not-loading:opacity-100">
...
</div>
Enumerables (:list
, :map
and :mapset
)
-
has-[assign-name]
- if the enumerable has items. -
no-[assign-name]
- if the enumerable is empty ornil
.
<div class="@has-items:text-blue-500 @no-items:text-red-500">
...
</div>
Strings, atoms and integers defining values
or values!
-
[assign-name]-[value]
for eachvalue
invalues
.
Example:
<div class="@size-small:text-sm @size-medium:text-base @size-large:text-lg">
...
</div>
All other types
-
The
assign-name
itself, if the value is notnil
. -
not-[assign-name]
, if the value isnil
.
Customizing CSS variants
If you need to perform further customization, like changing the generated variants' names or prefix, please see the Customizing variants' names section in the docs.
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.