Shimmer is a collection of Rails extensions that bring advanced UI features into your app and make your life easier as a developer.
Shimmer includes a suite of styled components that can be implemented in React and also in plain HTML or slim.
Stack is a reusable typed component that allows you to easily manage the layout of your app. You can define whether it should be displayed horizontally, vertically, and how much spacing there should be in between the child components. This component implements a mobile-first design and allows you to customize the display and spacing even on defined breakpoints (tablet, desktop, widescreen) should you need to.
To use it in a React project, you can just import and use it as you would in a normal React component:
import { Stack } from "@nerdgeschoss/shimmer/dist/components/stack";
<Stack gapTablet={4} gapDesktop={12} line>
<div></div>
<div></div>
<div></div>
</Stack>;
To use it in an HTML file, you can just import the css file directly from @nerdgeschoss/shimmer/dist/components/stack.css
and just implement the classes as they are in the stylesheet:
<div class="stack stack--line stack--tablet-4 stack--desktop-12">
<div></div>
<div></div>
<div></div>
</div>
type Justify =
| "start"
| "center"
| "end"
| "space-between"
| "space-around"
| "stretch"
| "equal-size";
type Align = "start" | "center" | "end" | "stretch" | "baseline";
Field | Type | Description |
---|---|---|
gap | number |
Space between elements |
gapTablet | number |
Gap size for screen starting on Tablet breakpoint |
gapDesktop | number |
Gap size for screen starting on Desktop breakpoint |
gapWidescreen | number |
Gap size for screen starting on WideScreen breakpoint |
line | boolean |
Stacks elements horizontally |
lineTablet | boolean |
Stacks elements horizontally starting on Tablet breakpoint |
lineDesktop | boolean |
Stacks elements horizontally starting on Desktop breakpoint |
lineWidescreen | boolean |
Stacks elements horizontally starting on WideScreen breakpoint |
align | Align |
Aligns element according to the main axis of the flex container |
alignTablet | Align |
Align on Tablet breakpoint |
alignDesktop | Align |
Align on Desktop breakpoint |
alignWidescreen | Align |
Align on Widescreen breakpoint |
justify | Justify |
Specifies how elements are distributed along main axis of the flex container |
justifyTablet | Justify |
Justify on Tablet breakpoint |
justifyDesktop | Justify |
Justify on Desktop breakpoint |
justifyWidescreen | Justify |
Justify on Widescreen breakpoint |
When using the CSS classes instead of react component we can generate the class names looking at the available props by using the prop names and BEM convention.
For example:
<Stack gapTablet={4} gapDesktop={12} line>
...
</Stack>
would translate to
<div class="stack stack--tablet-4 stack--destop-12 stack--line">...</div>
Pleae, note that there is not word "gap" in the class names since we use it implicitly, i.e. gapWidescreen={12}
is equivalent to stack stack--widescreen-12
.
Another thing to keep in mind when using CSS version of the component that the sizes are fixed - in contrary to the React one where we can add any number we want.
Here is the list of available sizes for CSS version:
$sizes: (
0: 0px,
2: 2px,
4: 4px,
8: 8px,
12: 12px,
16: 16px,
20: 20px,
22: 22px,
24: 24px,
32: 32px,
40: 40px,
48: 48px,
56: 56px,
64: 64px,
);
- Tablet: 640px
- Desktop: 890px
- Widescreen: 1280px
Shimmer offers an opiniated Rubocop base configuration. This configuration inherits itself from StandardRB and aim at remaining as close to it as possible. Why not only use StandardRB, since it is so fast and prevent bikeshedding? Well, sadly, it does not solve all problems and using Rubocop still integrates a lot easier in most toolsets. However, the idea is to still prevent bikeshedding our Rubocop configuration by making sure that every exception to what's configured in StandardRB is justified (with a comment over its configuration block in ./config/rubocop_base.yml
), reviewed, debated, and agreed upon before being merged.
Typically, a .rubocop.yml
file in projects using Shimmer looks like this.
inherit_gem:
shimmer: config/rubocop_base.yml
Then, if there are specific cops you want to use in the specific project you are working on, you still can easily add them. But at least, the base configuration is shared between projects and is itself as close to StandardRB as possible.
ActiveStorage
is great, but serving of files, especially behind a CDN, can be complicated to get right. Shimmer has your back.
It overrides image_tag
and automatically resizes your image and creates a static, cacheable URL.
# use an image tag
image_tag(user.avatar, width: 300, height: 400)
image_tag(user.avatar, width: 300) # This will keep the aspect ratio and infer height
If you need more control, you can also use image_file_path
and image_file_url
directly. They have a similar interface, but only return the path/URL.
image_file_path(user.avatar, width: 300, height: 400)
image_file_url(user.avatar, width: 300, height: 400)
Shimmer will only ever scale your images down, not up.
The URL of the image will point to the Rails app, where it will be generated on the fly. You should cache these routes with a CDN like Cloudflare.
This is in contrast to ActiveStorage variants, where the transformation happens when the URL for the image is built. This can slow down rendering or even prevent HTML to be generated if just one image can't be resized. With Shimmer, only the broken image will be broken.
Modals are the designer's best friend, but developers usually hate them for their complexity. Fear no more: Shimmer has you covered.
a href=modal_path(new_post_path) Create a new Post
This will open a modal on click and then asynchronously request the modal content from the controller. Modals can also be controlled via JavaScript via the global ui
variable:
ui.modal.open({ url: "/posts/new" });
ui.modal.close();
When modals are annoying to implement, popovers are even worse. Thankfully, Shimmer comes to the rescue:
a href=popover_path(new_post_path, placement: :left)
This will request new_post_path
and display it left of the anchor thanks to PopperJS.
Tip
If you want to make sure that your modal content is only available if requested through Shimmer, you can use the built in enfore_modal
method as a before_action
. It will return a 422 Unprocessable Content status if users (or bots) access the page directly.
before_action :enforce_modal, only: [:popover]
Remote navigation takes Hotwire to the next level with built-in navigation actions, integrated with modals and popovers.
def create
@post = current_user.posts.create! post_params
ui.navigate_to @post
end
This will automatically close the current modal or popover and navigate via Turbo Drive to the post's url - no redirects necessary.
The ui
helper comes with several built-in functions:
# run any kind of javascript upon request completion
ui.run_javascript("alert('hello world')")
# open or replace a modal's content (dependent on its ID)
ui.open_modal(new_post_path, size: :small)
# close an open modal
ui.close_modal
# same methods also available for popovers
ui.open_popover(new_post_path, selector: "#user-profile", placement: :left)
ui.close_popover
# navigate via Turbo Drive
ui.navigate_to(@post)
# manipulate the page's content
ui.append(@post, with: "comments/comment", comment: @comment)
ui.prepend("#user-profile", with: "users/extra")
ui.replace(@post)
ui.remove(@post)
Want to implement sitemaps, but the ephemeral filesystem of Heroku hates you? Here's a simple way to upload sitemaps:
- install the sitemap gem and configure the
sitemap.rb
as usual - use the shimmer adapter to automatically upload your sitemap to your configured ActiveStorage adapter
- use the shimmer controller to display the sitemap in your app
- (optional) tell sidekiq scheduler to regularly update your sitemap
# sitemap.rb
SitemapGenerator::Sitemap.adapter = Shimmer::SitemapAdapter.new
# routes.rb
get "sitemaps/*path", to: "shimmer/sitemaps#show"
# sidekiq.yml
:schedule:
sitemap:
cron: '0 0 12 * * * Europe/Berlin' # every day at 16:00, Berlin time
class: Shimmer::SitemapJob
As you might have noticed, Cloudflare SSL will cause some issues with your Rails app if you're not using SSL strict mode (rails/rails#22965). If you can't switch to strict mode, go for the standard flexible mode instead and add this middleware to your stack:
# application.rb
config.middleware.use Shimmer::CloudflareProxy
Can't reproduce an issue with your local test data and just want the production or staging data on your development machine? Here you go:
rails db:pull
This will drop your local database and pull in the database of your connected Heroku app (make sure you executed heroku git:remote -a your_app
before to have the git remote). But what about assets you might ask? Easy - assets are pulled from S3 as well via the AWS CLI automatically (make sure your environment variables in Heroku are correctly named as AWS_REGION
, AWS_ACCESS_KEY_ID
and AWS_SECRET_ACCESS_KEY
) and the database is updated to use your local filesystem instead.
If you don't want the asset support, you can also only pull the database or only the assets:
rails db:pull_data
rails db:pull_assets
To localize a page via urls, this will help you tremendously.
# routes.rb
Rails.application.routes.draw do
scope "/(:locale)", locale: /#{I18n.available_locales.join("|")}/ do
get "login", to: "sessions#new"
end
end
From now on you can prefix your routes with a locale like /de/login
or /en/login
and I18n.locale
will automatically be set. If there is no locale in the url (it's optional), this will automatically use the browser's locale.
You want to redirect from unlocalized paths? Add a before action to your controller:
before_action :check_locale
Trying to figure out which key a certain translation on the page has? Append ?debug
to the url and I18n.debug?
will be set - which leads to keys being printed on the page.
Integrate cookie consent and add tracking capabilities such as Google Tag Manager and Google Analytics to your application with the following steps:
Include Shimmer's Consent Module: Add the following line to your application_controller.rb
:
class ApplicationController < ActionController::Base
include Shimmer::Consent
end
Add Google Tag Manager and Google Analytics:
- If you wish to include Google Tag Manager or Google Analytics, insert either of the following lines to your
application.js
:
ui.consent.enableGoogleTagManager(GOOGLE_TAG_MANAGER_ID);
ui.consent.enableGoogleAnalytics(GOOGLE_ANALYTICS_ID);
Replace GOOGLE_TAG_MANAGER_ID
with your Google Tag Manager ID or GOOGLE_ANALYTICS_ID
with your Google Analytics ID.
User Consent: Shimmer::Consent
provides a stimulus controller for creating a cookie banner. When the 'statistic' option is submitted to the controller, the necessary tracking scripts are added to the page's head.
Add this line to your application's Gemfile:
gem "shimmer"
And then execute:
$ bundle install
Add some configuration to your project:
# routes.rb
resources :files, only: :show, controller: "shimmer/files"
# application_controller.rb
class ApplicationController < ActionController::Base
include Shimmer::Localizable
include Shimmer::RemoteNavigation
end
// application.ts
import { start } from "@nerdgeschoss/shimmer";
import { application } from "controllers/application";
start({ application });
This library is tested using RSpec.
bin/rspec
A system test suite is included and is performed against a demo Rails application located in in spec/rails_app
. This application can be started in development mode for "playing around" with Shimmer during its development and add more system tests. The bin/dev
script starts that demo application.
The first time, you want to initialize the database and seed it some data.
bin/setup
Then you can start the development server.
bin/dev
Bug reports and pull requests are welcome on GitHub at https://github.com/nerdgeschoss/shimmer. This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the code of conduct.
The gem is available as open source under the terms of the MIT License.
Everyone interacting in the Shimmer project's codebases, issue trackers, chat rooms and mailing lists is expected to follow the code of conduct.