ViewComponent::ScopedStyles Gem Version

Scoped, colocated CSS for ViewComponent.

Avoids collisions by rewriting class selectors to stable, content-derived names.

E.g. .button becomes .button_a1b2c3d4

Table of Contents

Installation

Install the gem and add to the application's Gemfile by executing:

bundle add view_component-scoped_styles

Or add it to the Gemfile manually:

gem "view_component-scoped_styles"

If bundler is not being used to manage dependencies, install the gem by executing:

gem install view_component-scoped_styles

Usage

Include the module in any component class you would like to use with scoped CSS.

class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles
end

CSS can be written in two ways:

1. Using a sidecar stylesheet

Learn more about sidecar here.

bin/rails generate view_component:component Example title --sidecar

      create  app/components/example_component.rb
      invoke  test_unit
      create    test/components/example_component_test.rb
      invoke  erb
      create    app/components/example_component/example_component.html.erb

Then add a matching stylesheet in the sidecar directory:

/* app/components/example_component/example_component.css */

.component {
  position: relative;
}

2. Using a styles block in the component

# app/components/example_component.rb

class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  styles do
    <<~CSS
      .component {
        position: relative;
      }
    CSS
  end
end

NB: Using a styles block will take precedence over a sidecar stylesheet.

Referencing classes

Use the component_class helper inside component templates to refer to the scoped CSS classes:

<div class="<%= component_class %>">
  My component content
</div>

The default selector is .component but you can change this by defining component_css_class in your component:

class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  component_css_class "example"

  styles do
    <<~CSS
      .example {
        position: relative;
      }
    CSS
  end
end

component_class takes an optional string argument to reference other classes in the CSS:

class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  component_css_class "example"

  styles do
    <<~CSS
      .example {
        position: relative;
      }

      .inner {
        position: absolute;
      }
    CSS
  end
end
<div class="<%= component_class %>">
  My component content

  <div class="<%= component_class("inner") %>">
    Inner content
  </div>
</div>

Scoped class names are prefixed by default using the class name (e.g. .componentcomponent_a1b2c3d4). Set a global prefix in configuration, or override per component with css_class_prefix.

The prefix is a template string. Use {class_name} for the CSS class being scoped and {component_name} for the component name (namespaces joined by /, with a trailing Component suffix removed):

ViewComponent::ScopedStyles.configure do |config|
  config.css_class_prefix = "{class_name}_"  # default
end
class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  css_class_prefix "vc-{class_name}_"

  styles do
    <<~CSS
      .component { ... }  # becomes .vc-component_a1b2c3d4 in components.scoped.css
    CSS
  end
end

Namespaced components can include the component name in the prefix:

class Admin::UserCardComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  css_class_prefix "{component_name}_{class_name}_"

  styles do
    <<~CSS
      .component { ... }
      # Admin/UserCardComponent_component_a1b2c3d4 in templates;
      # Admin\/UserCardComponent_component_a1b2c3d4 in components.scoped.css
    CSS
  end
end

Upgrading from 0.4.x: set config.css_class_prefix = "c-" (and per-component overrides) to keep the previous c-a1b2c3d4 class names without updating templates.

Ignoring classes

Ignored classes are left unchanged in generated CSS:

class ExampleComponent < ViewComponent::Base
  include ViewComponent::ScopedStyles

  ignored_css_classes "is-open", "active"

  styles do
    <<~CSS
      .component { ... }
      .is-open { ... }  # stays .is-open in components.scoped.css
    CSS
  end
end

In your view, you can either reference the class directly:

<div class="<%= component_class %> is-open">

or via the component_class helper:

<div class="<%= component_class %> <%= component_class("is-open") %>">

Using the scoped CSS

All scoped CSS will be compiled into a single bundled stylesheet. By default that is app/assets/stylesheets/components.scoped.css; both the directory and filename are configurable (see Configuration).

You should import this stylesheet within your app:

/* app/assets/stylesheets/application.css */

@import url("./components.scoped.css");

Configuration

Run the install generator in your Rails app:

bin/rails generate view_component:scoped_styles:install

That creates config/initializers/view_component_scoped_styles.rb with the same defaults as ViewComponent::ScopedStyles::Configuration.

Or create the initializer manually:

ViewComponent::ScopedStyles.configure do |config|
  # Where ViewComponent classes live (relative to Rails.root). Default: "app/components"
  config.components_path = File.join("app", "components")

  # Where the bundled scoped stylesheet is written (relative to Rails.root). Default: "app/assets/stylesheets"
  config.assets_path = File.join("app", "assets", "stylesheets")

  # Filename of the bundled scoped stylesheet. Default: "components.scoped.css"
  config.stylesheet_name = "components.scoped.css"

  # Optional @layer name for the bundled scoped stylesheet (e.g. "components"). Default: nil.
  config.components_layer = nil

  # Prefix for scoped class names. Supports {component_name} and {class_name}. Default: "{class_name}_"
  config.css_class_prefix = "{class_name}_"
end
Option Default Description
components_path "app/components" Where ViewComponent classes live, relative to Rails.root.
assets_path "app/assets/stylesheets" Directory where the bundled scoped stylesheet is written, relative to Rails.root.
stylesheet_name "components.scoped.css" Filename of the bundled scoped stylesheet within assets_path.
components_layer nil When set, wraps generated CSS in @layer <name> { ... } for cascade control.
css_class_prefix "{class_name}_" Prefix template for scoped class names. Supports {class_name} and {component_name} (namespaces joined by /, Component suffix stripped; e.g. .componentcomponent_a1b2c3d4). Use "c-" to match 0.4.x behavior.

This gem was heavily inspired by Partials Fx, and indeed takes its foundations from it, modified to work with ViewComponent instead.

Development

After checking out the repo, run bin/setup to install dependencies. Then, run rake spec to run the tests.

To install this gem onto your local machine, run bundle exec rake install. To release a new version, update the version number in version.rb, and then run bundle exec rake release, which will create a git tag for the version, push git commits and the created tag, and push the .gem file to rubygems.org.

Contributing

Bug reports and pull requests are welcome on GitHub at https://github.com/chrise86/view_component-scoped_styles. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.

License

The gem is available as open source under the terms of the MIT License.

Code of Conduct

Everyone interacting in the ViewComponent::ScopedStyles project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.