Scoped CSS
Installation
- Add to your Gemfile:
gem 'scoped_css'
- Run
bundle install
- Include the helper in your views:
module ApplicationHelper
include ScopedCss::Helper
# other helpers...
end
- Use the helper in templates:
<% style_string, styles = scoped_css do %>
<style>
.header { font-weight: bold; }
.content { margin: 10px; }
<style>
<% end %>
<h1 class="<%= styles[:header] %>">Title</h1>
<main class="<%= styles[:content] %>">Content here</main>
<%= style_string %>
Usage with ViewComponent
app/components/section_component.rb
class SectionComponent < ViewComponent::Base
end
app/components/section_component.html.erb
<% style_string, styles = helpers.scoped_css do %>
<style>
.section {
border: none;
scroll-snap-align: center;
color: purple;
}
.heading {
font-size: 2rem;
}
</style>
<% end %>
<section class="<%= styles[:section] %>">
<h2 class="<%= styles[:heading] %>">Section</h2>
<%= content %>
</section>
<%= style_string %>
Attribute Splatting
Sometimes you want to apply html attributes to a component from the parent template.
app/components/section_component.rb
class SectionComponent < ViewComponent::Base
def initialize(attributes: {})
@attributes = attributes
end
end
app/views/home/index.html.erb
<% style_string, styles = scoped_css do %>
<style>
.section {
margin: 10px;
}
.heading {
font-size: 3rem;
}
</style>
<% end %>
<h1 class="<%= styles[:heading] %>">Title</h1>
<%= render SectionComponent.new(attributes: { id: "important-section", class: styles[:section] }) do %>
<p>Section 1</p>
<% end %>
<%= style_string %>
app/components/section_component.html.erb
<% style_string, styles = helpers.scoped_css do %>
<style>
.section {
border: none;
scroll-snap-align: center;
color: purple;
}
.heading {
font-size: 2rem;
}
</style>
<% end %>
<section <%= helpers.splat_attributes(@attributes, styles[:section]) %>>
<h2 class="<%= styles[:heading] %>">Section</h2>
<%= content %>
</section>
<%= style_string %>
:info: Note that <section class="<%= styles[:heading] %>" <%= helpers.splat_attributes(@attributes, styles[:section]) %>>
is not used. Instead, the splat_attributes
helper is used to apply the class attribute to the component. The splat_attributes
helper takes CSS class names after the attributes argument and concatenates these CSS class names with any class names in the attributes hash.
This will generate the following HTML:
<!-- app/views/home/index.html.erb -->
<h1 class=".atge5q2e-heading">Title</h1>
<!-- app/components/section_component.html.erb -->
<section class="agkd94j4-section atge5q2e-section" id="important-section">
<h2 class="agkd94j4-heading">
</section>
<section class="agkd94j4-section">
<h2 class="agkd94j4-heading">
<p>Section</p>
</section>
<style>
.agkd94j4-section {
border: none;
scroll-snap-align: center;
color: purple;
}
.agkd94j4-heading {
font-size: 2rem;
}
</style>
<!-- app/views/home/index.html.erb -->
<style>
.atge5q2e-section {
margin: 10px;
}
.atge5q2e-heading {
font-size: 3rem;
}
</style>
CSS Specificity
In previous examples, we applied the CSS class .section
to the <section>
element. It has the property color: purple;
. But what if we want one instance of the SectionComponent to have a different color? In the parent template we can define a CSS class for the section component with the different color and apply it to the component.
app/components/section_component.rb
class SectionComponent < ViewComponent::Base
def initialize(attributes: {})
@attributes = attributes
end
end
app/components/section_component.html.erb
<% style_string, styles = helpers.scoped_css do %>
<style>
.section {
border: none;
scroll-snap-align: center;
color: purple;
}
.heading {
font-size: 2rem;
}
</style>
<% end %>
<section <%= helpers.splat_attributes(@attributes, styles[:section]) %>>
<h2 class="<%= styles[:heading] %>">Section</h2>
<%= content %>
</section>
<%= style_string %>
app/views/home/index.html.erb
<% style_string, styles = scoped_css do %>
<style>
.section {
margin: 10px;
color: darkgreen;
}
.heading {
font-size: 3rem;
}
</style>
<% end %>
<h1 class="<%= styles[:heading] %>">Title</h1>
<%= render SectionComponent.new(attributes: { class: styles[:section] }) do %>
<p>Section 1</p>
<% end %>
<%= render SectionComponent.new() do %>
<p>Section 2</p>
<% end %>
<%= style_string %>
The reason this works is because the we render the style_string in each template at the bottom of the template. Any nested components style tag will be rendered before the parent style tag. The last declared selector has precedence. And because a scoped CSS class name is passed to the component only that instance of the component will use the new color.
<!-- app/views/home/index.html.erb -->
<h1 class=".atge5q2e-heading">Title</h1>
<!-- app/components/section_component.html.erb -->
<section class="agkd94j4-section atge5q2e-section">
<h2 class="agkd94j4-heading">
<p>Section 1</p>
</section>
<section class="agkd94j4-section atge5q2e-section">
<h2 class="agkd94j4-heading">
<p>Section 2</p>
</section>
<style>
.agkd94j4-section {
border: none;
scroll-snap-align: center;
color: purple;
}
.agkd94j4-heading {
font-size: 2rem;
}
</style>
<!-- app/views/home/index.html.erb -->
<style>
.atge5q2e-section {
margin: 10px;
color: darkgreen;
}
.atge5q2e-heading {
font-size: 3rem;
}
</style>