Lookbook gives ViewComponent-based projects a ready-to-go development UI for navigating, inspecting and interacting with component previews.
It uses (and extends) the native ViewComponent preview functionality, so you don't need to learn a new DSL or create any extra files to get up and running.
Lookbook uses RDoc/Yard-style comment tags to extend the capabilities of ViewComponent's previews whilst maintaining compatability with the standard preview class format, so you can add or remove Lookbook at any time without having to rework your code.
- Tree-style navigation menu with live search/filter
- Resizable preview window for responsive testing
- Highlighted preview source code and HTML output
- Auto-updating UI when component or preview files are updated (Rails v6.0+ only)
- Use comment tag annotations for granular customisation of the preview experience
- Fully compatible with standard the ViewComponent preview system
- In-browser live-editable preview parameters (similar to basic Storybook Controls/Knobs)
- [Experimental] Markdown-powerered documentation pages with embeddable previews
If you want to have a quick play with Lookbook, the easiest way is to give the demo app a spin. It's a basic Rails/ViewComponent app with a few test components included to tinker with.
Online demo: https://lookbook-demo-app.herokuapp.com/lookbook
If you'd rather dig in a bit more and run the demo app locally, the demo repo contains instructions on how to get it up and running.
Add Lookbook to your Gemfile
somewhere after the ViewComponent gem. For example:
gem "view_component", require: "view_component/engine"
gem "lookbook"
You then need to mount the Lookbook engine (at a path of your choosing) in your routes.rb
file:
Rails.application.routes.draw do
if Rails.env.development?
mount Lookbook::Engine, at: "/lookbook"
end
end
The at
property determines the root URL that the Lookbook UI will be served at.
Then you can start your app as normal and navigate to http://localhost:3000/lookbook
(or whatever mount path you specified) to view your component previews in the Lookbook UI.
You don't need to do anything special to see your ViewComponent previews and examples in Lookbook - just create them as normal and they'll automatically appear in the Lookbook UI. Preview templates, custom layouts and even bespoke preview controllers should all work as you would expect.
If you are new to ViewComponent development, checkout the ViewComponent documentation on how to get started developing your components and creating previews.
Lookbook parses Yard-style comment tags in your preview classes to customise and extend the standard ViewComponent preview experience:
# @label Basic Button
# @display bg_color "#fff"
class ButtonComponentPreview < ViewComponent::Preview
# Primary button
# ---------------
# This is the button style you should use for most things.
#
# @label Primary
def default
render ButtonComponent.new do
"Click me"
end
end
# Button with icon
# ----------------
# This example uses dynamic preview parameters
# which can be edited live in the Lookbook UI
#
# @param text
# @param icon select [heart, cog, alert]
def icon(text: "Spread the love", icon: "heart")
render ButtonComponent.new(icon: icon) do
text
end
end
# Inverted button
# ---------------
# For light-on-dark screens
#
# @display bg_color "#000"
def secondary
render ButtonComponent.new(style: :inverted) do
"Click me"
end
end
# Unicorn button
# ---------------
# This button style is still a **work in progress**.
#
# @hidden
def secondary
render ButtonComponent.new do
"Click me"
end
end
# @!group More examples
def short_text
render ButtonComponent.new do
"Go"
end
end
def long_text
render ButtonComponent.new do
"Click here to do this thing because it's the best way to do it"
end
end
def emoji_text
render ButtonComponent.new do
"๐๐"
end
end
# @!endgroup
end
Tags are just strings identified by their @
prefix - for example @hidden
. Tags are always placed in a comment above the relevant preview class or example method.
The following Lookbook-specific tags are available for use:
Used to replace the auto-generated navigation label for the item with <text>
.
@label <text>
Available for preview classes & example methods.
# @label Preview Label
class FooComponentPreview < ViewComponent::Preview
# @label Example Label
def default
end
end
The @display
tag lets you pass custom parameters to your preview layout so that the component preview can be customised on a per-example basis.
# @display bg_color "#eee"
class FooComponentPreview < ViewComponent::Preview
# @display max_width 500px
# @display wrapper true
def default
end
end
The @display
tag can be applied at the preview (class) or at the example (method) level, and takes the following format:
@display <key> <value>
<key>
must be a valid Ruby hash key name, without quotes or spaces<value>
will be parsed using the Ruby YAML parser to resolve the value
Note: Ruby YAML does not (generally) require quoting of string values. However in some cases it is required due to the presence of indicator characters (such as
#
,:
etc) - hence why the hex color code in the example above is surrounded by quotes. It's perfectly ok to quote all string values if you prefer.
These display parameters can then be accessed via the params
hash in your preview layout using params[:lookbook][:display][<key>]
:
<!DOCTYPE html>
<html style="background-color: <%= params[:lookbook][:display][:bg_color] %>">
<head>
<title>Preview Layout</title>
</head>
<body>
<div style="max-width: <%= params[:lookbook][:display][:max_width] || '100%' %>">
<% if params[:lookbook][:display][:wrapper] == true %>
<div class="wrapper"><%= yield %></div>
<% else %>
<%= yield %>
<% end %>
</div>
</body>
</html>
By default ViewComponent will use your default application layout for displaying the rendered example. However it's often better to create a seperate layout that you can customise and use specifically for previewing your components. See the ViewComponent preview docs for instructions on how to set that up.
Any @display
params set at the preview (class) level with be merged with those set on individual example methods.
Global (fallback) display params can be defined via a configuration option:
# config/application.rb
config.lookbook.preview_display_params = {
bg_color: "#fff",
max_width: "100%"
}
Globally defined display params will be available to all previews. Any preview or example-level @display
values with the same name will take precedence and override a globally-set one.
For smaller components, it can often make sense to render a set of preview examples in a single window, rather than representing them as individual items in the navigation which can start to look a bit cluttered.
You can group a set of examples by wrapping them in @!group <name>
/ @!endgroup
tags within your preview file:
class HeaderComponentPreview < ViewComponent::Preview
def standard
render Elements::HeaderComponent.new do
"Standard header"
end
end
# @!group Sizes
def small
render Elements::HeaderComponent.new(size: 12) do
"Small header"
end
end
def medium
render Elements::HeaderComponent.new(size: 16) do
"Small header"
end
end
def big
render Elements::HeaderComponent.new(size: 24) do
"Small header"
end
end
# @!endgroup
end
The example above would display the Sizes
examples grouped together on a single page, rather than as indiviual items in the navigation:
You can have as many groups as you like within a single preview class, but each example can only belong to one group.
๐ท `@hidden`
Used to temporarily exclude an item from the Lookbook navigation. The item will still be accessible via it's URL.
Can be useful when a component (or a variant of a component) is still in development and is not ready to be shared with the wider team.
Available for both preview classes & example methods.
# @hidden
class FooComponentPreview < ViewComponent::Preview
# @hidden
def default
end
end
The @param
tag provides the ability to specify editable preview parameters which can be changed in the Lookbook UI in order to customise the rendered output on the fly, much like the Controls (knobs) addon for Storybook.
Each @param
will have an associated form field generated for it. The values for each field will be handled as dynamic preview params when rendering the example.
The @param
tag takes the following format:
@param <name> <input_type> <opts?>
<name>
- name of the dynamic preview param<input_type>
- input field type to generate in the UI<opts?>
- YAML-encoded field options, used for some field types
The following input field types are available for use:
๐ Text-style inputs - Single line fields, useful for short strings of text or numbers.
@param <name> text
@param <name> email
@param <name> number
@param <name> url
@param <name> tel
The above types only differ in the validation constraints they impose on the input field.
๐ Textarea - Multi-line textarea field for longer-form content.
@param <name> textarea
๐ Select box - Dropdown select field for selecting from a list of known options.
@param <name> select <options>
<options>
should be a YAML array of options which must be formatted in the same style as the input for Rails' options_for_select
helper:
# Basic options:
# @param theme select [primary, secondary, danger]
# With custom labels (each item itself an array of [label, value]):
# @param theme select [[Primary theme, primary], [Secondary theme, secondary], [Danger theme, danger]]
# With empty option (`~` in YAML)
# @param theme select [~, primary, secondary, danger]
Note: In most cases YAML does not require quoting of strings, however if you are running into issues check out the Ruby YAML docs for a complete syntax reference.
๐ Toggle - On/off switch for toggling boolean values.
@param <name> toggle
Default values are specified as part of the preview example method parameters in the usual Ruby way:
def button(content: "Click me", theme: "primary", arrow: false)
# ...
end
These will be used as the default values for the param fields.
Note that the default values are not evaluated at runtime, so you cannot use method calls to generate the defaults. Only static default values are supported.
Most dynamic param values are passed to the example method as strings, with the following exceptions:
toggle
input - values are cast to booleansnumber
input - values are cast to integers
In some cases, you may want to type cast the parameter value to something else (for example a Symbol
) before using it when initializing the component.
To help with this, a type
option can be specified in the @param
definition to automatically cast the dynamic value to a different type:
# @param <name> [<type>] <input_type> <opts?>
In the example below, the value of the theme
param (by default a string) will be automatically cast to a Symbol, ready for use in instantiating the component.
# @param theme [Symbol] select [primary, secondary, danger]
def default(theme: :primary)
render Elements::ButtonComponent.new(theme: theme) do
"Click me"
end
end
The supported types to cast to are:
String
- default for all excepttoggle
inputsBoolean
- default fortoggle
inputsSymbol
Date
DateTime
Integer
Float
The following structured types are also available but should be considered experimental - you may run into bugs!
Hash
- value string converted to Hash using the Ruby YAML parserArray
- value string converted to Array using the Ruby YAML parser
class ButtonComponentPreview < ViewComponent::Preview
# The params defined below will be editable in the UI:
#
# @param content text
# @param theme select [primary, secondary, danger]
# @param arrow toggle
def default(content: "Click me", theme: "primary", arrow: true)
render Elements::ButtonComponent.new(theme: theme, arrow: arrow) do
content
end
end
end
All comment text other than tags will be treated as markdown and rendered in the Notes panel for that example in the Lookbook UI.
# @hidden
class ProfileCardComponentPreview < ViewComponent::Preview
# Profile Card
# ------------
# Use the default profile card component whenever you need to represent a user.
def default
end
end
If you need to add more long-form documentation to live alongside your component previews you can do so using Lookbook's markdown-powered pages
system.
โ ๏ธ This feature is currently flagged as an experimental feature which requires feature opt-in to use. Its API and implementation may change before it is released.To enable support for pages in your project, add
config.lookbook.experimental_features = [:pages]
into your application configuration file.
For an example of some pages in Lookbook, check out the example pages in the Lookbook demo app and the associated page files in the demo repo.
By default, pages should be placed in the test/components/docs
directory (although this can be customised) and can be nested in directories as deeply as required.
Pages must have either a .html.erb
or a .md.erb
file extension. All pages are rendered as ERB templates but .md.erb
files will also additionally be run through a markdown parser.
Pages can optionally make use of a YAML frontmatter block to customise the behaviour and content of the page itself.
An example page might look like this:
---
title: An example page
label: Nice example
---
This is an example page. If it has a `.md.erb` file extension its
contents will be run through a Markdown parser/renderer before display.
Fenced code blocks are fully supported and will be highlighted appropriately.
ERB can be used in here.
The template will be rendered **before** being parsed as Markdown.
You can can access data about the page using the `@page` variable.
The title of this page is "<%= @page.title %>".
Pages can use an optional YAML frontmatter block to configure the behaviour of the page and to provide custom data, if required.
The following page options can be customised via frontmatter:
id
- a custom page ID that can be used for linking to it from other pageslabel
- The name of the page that will be displayed in the navigation (auto-generated from the file name if not set)title
- The main page title displayed on the page (defaults to the label value if not set).hidden
- Iffalse
the page will not appear in the navigation but will still be accessible at it's URL (useful for pages that are still in development) [default:true
]landing
- Set totrue
to use the page as the Lookbook landing page [default:false
]header
- Set tofalse
to hide the page header containing the page title [default:true
]footer
- Set tofalse
to hide the page footer containing the previous/next page links [default:true
]data
- Optional hash of custom data to make available for use in the page - see info on page variables below. [default:{}
]
You can set global default values for page options in the application configuration:
# config/application.rb
config.lookbook.page_options = {
footer: false,
data: {
brand_colors: {
red: "#ff0000"
}
}
}
These will be merged with any page-level frontmatter data. Options set in the frontmatter will override those set at the global level (apart from data
, which will be deep-merged with the any globally defined data).
All pages have the following variables available for use in the page template:
@page
- The current page object@next_page
- The next page object (if available)@previous_page
- The previous page object (if available)@pages
- Collection of all pages
Page objects have access to frontmatter variables:
The page title is <%= @page.title %>
Our brand color hex value is <%= @page.data[:brand_colors][:red] %>
If you want to enforce a specific order for pages and directories in the Lookbook navigation you can prefix the file/directory basename with an 'order number' integer value followed by an underscore or hyphen.
For example: 01_example_page.md.erb
will be displayed first in the navigation (01
) within the directory it is in.
The integer value will be parsed out from the filename so that it doesn't appear in navigation labels or URLs, and the value itself will be used as a 'position' number when sorting the navigation items.
For example, an ordered directory of pages might look like:
test/components/docs/
โโโ 01_overview.md.erb
โโโ 02_implementation_notes/
โ โโโ 01_slots.md.erb
โ โโโ 02_html_attributes.md.erb
โโโ 03_helpful_examples/
โโโ 01_basic_components.md.erb
โโโ 02_complex_components.md.erb
Without the number prefixes on the file names the pages may not have appeared in the navigation in the desired order.
You can get the path to a page using the page_path
helper. This accepts a page id
(as a Symbol
) or a page object:
Visit the [about page](<%= page_path :about %>)
Go to the [next page](<%= page_path @next_page %>)
Page ids can be set in the YAML frontmatter block for that page:
---
id: about
---
This is the about page.
You can embed preview examples from your project directly into the documentation pages using the embed
helper, which renders an iframe with the rendered preview in it at any point in your document.
The output looks like this:
To specify which preview example to render, the helper accepts a preview class and a method name (as a symbol), like this:
<%= embed Elements:ButtonComponentPreview, :default %>
If you have configured your examples to accept preview params (see the @param
docs), then you can supply values for those params when rendering the embedded preview:
<%= embed Elements:ButtonComponentPreview, :default, params: {
icon: "plus",
text: "Add new"
} %>
You can use language-scoped fenced code blocks in the markdown file to render nicely highlighted code examples.
However, if you are not using Markdown, or need a little more control, you can use the code
helper instead:
<%= code do %>
# code goes here
<% end %>
The default language is ruby
. To highlight a different language you need to specify it's name as an argument:
<%= code :html do %>
<!-- code goes here -->
<% end %>
Lookbook uses Rouge for syntax highlighting. You can find a full list of supported languages here.
You can break up your page's content into tabs. If your avatar page is named 01_avatar.md.erb
, by declaring a page named 01_avatar[web].md.erb
it will create a web tab on the page. Tabs like normal Pages can contain embedded previews and code examples.
test/components/docs/
โโโ 01_avatar.md.erb
โโโ 01_avatar[design].md.erb
โโโ 01_avatar[mobile].md.erb
โโโ 01_avatar[web].md.erb
By declaring the label
frontmatter you can change the label shown on the tab:
---
label: Website
---
If you want the tabs in a different order, you can use the position
frontmatter:
---
label: Web
position: 1
---
These options can be set in your application configuration files to customise the pages behaviour.
An array of directories to look for pages in.
Default: ["test/previews/docs"]
config.lookbook.page_paths = ["path/to/my/pages"]
The URL segment used to prefix page routes.
Default: pages
config.lookbook.page_route = `docs`
Lookbook is intended to be a tool for aiding the ViewComponent development process, and so is usually restricted to running only when the app is in development
mode.
However, it is possible to run Lookbook in a production environment if you wish.
By default, Lookbook will behave a little differently in production than it does in development:
- Watching files for changes is disabled
- Parsing preview files for annotations does not happen at runtime. Instead the preview files must be pre-parsed via a Rake task before starting the app (much like asset precompilation).
Run the following command to pre-parse the preview files annotations:
rake lookbook:previews:preparse
If you wish to run this as part of your existing assets precompilation step, you can add the following into your app's Rakefile
:
if Rails.env.production?
Rake::Task['assets:precompile'].enhance do
Rake::Task["lookbook:previews:preparse"].invoke
end
end
The pre-parsing of preview files will then take place every time rake assets:precompile
is called and so will not need to be run separately.
You will also need to make sure that the following configuration changes have been made when deploying to production:
- Make sure ViewComponent is configured to show previews in production (by default it is disabled when not in development):
# config/environments/production.rb
config.view_component.show_previews = true
- Remove any environment checking from around the Lookbook mounting declaration (if added as per install instructions):
# config/routes.rb
Rails.application.routes.draw do
# if Rails.env.development? <- remove
mount Lookbook::Engine, at: "/lookbook"
# end
end
If for some reason you wish to enable file watching or runtime preview annotation parsing in production, you can always override the default behaviour using thie following config options:
# config/environments/production.rb
# enable file-change listening
config.lookbook.listen = true
# enable runtime preview parsing
config.lookbook.runtime_parsing = true
Lookbook will use the ViewComponent configuration for your project to find and render your previews so you generally you won't need to configure much else separately.
However the following Lookbook-specific configuration options are also available:
โ ๏ธ UI auto-refresh is only supported in Rails v6.0+
Disable/enable the auto-updating of the Lookbook UI when files change. Enabled by default.
config.lookbook.auto_refresh = false # default is true
By default Lookbook will listen for changes in any preview directories as well as in the components directory itself.
If you wish to add additional paths to listen for changes in, you can use the listen_paths
option:
config.lookbook.listen_paths << Rails.root.join('app/other/directory')
If you want to change the favicon used by the Lookbook UI, you can provide a path to your own (or a data-uri string) using the ui_favicon
option:
config.lookbook.ui_favicon = "/path/to/my/favicon.png"
To disable the favicon entirely, set the value to
false
.
Specify a project name to display in the top left of the UI (instead of the default "Lookbook"):
config.lookbook.project_name = "My Project"
If you don't want to display a project name at all, set the value to
false
.
Some features may occasionally be released behind a 'experimental' feature flag while they are being tested and refined, to allow people to try them out and provide feedback.
โ ๏ธ Please note: Experimental features should be considered to be subject to extensive change and breaking changes to them may be made within point releases - these features are not considered to be covered by semver whilst flagged as 'experimental'.โ ๏ธ
To opt into individual experimental features, include the name of the feature in the experimental_features
config option:
config.lookbook.experimental_features = ["feature_name"]
The current experimental features that can be opted into are:
pages
: Markdown-powered documentation pages with embeddable previews
If you want to live life on the bleeding-edge you can opt-in to all current and future experimental features (usual caveats apply):
config.lookbook.experimental_features = true
Lookbook provides a few keyboard shortcuts to help you quickly move around the UI.
f
- move focus to the nav filter boxEsc
[when focus is in nav filter box] - Clear contents if text is present, or return focus to the UI if the box is already emptys
- Switch to Source tab in the drawern
- Switch to Notes tab in the drawerv
- Switch to the rendered previewo
- Switch to the code previewr
- Refresh the preview (useful if using something like Faker to generate randomised data for the preview)w
- Open the standalone rendered preview in a new window
Lookbook is very much a small hobby/side project at the moment. I'd love to hear from anyone who is interested in contributing but I'm terrible at replying to emails or messages, so don't be surprised if I take forever to get back to you. It's not personal ๐
The quickest way to get a development version of Lookbook up and running is to use the lookbook-demo app and link it to a local version of the Lookbook gem:
- Clone this repository somewhere on your machine -
git clone [email protected]:allmarkedup/lookbook.git
- Also pull down the lookbook-demo repository to your machine
- In the
Gemfile
of thelookbook-demo
repository, replacegem "lookbook", '>= 0.1', git: "https://github.com/allmarkedup/lookbook", branch: "main"
withgem "lookbook", path: "../path/to/lookbook"
(use the path to your local copy of lookbook) - Install dependencies - from the root of the parent project run
bundle install
- From within the
lookbook
root directory run the comandnpm run dev
(this will make sure the CSS/JS is recompiled if/when you make changes to the UI) - From within the
lookbook-demo
root directory runnpm run start
- this will start a server and build the demo assets
Point your browser to http://localhost:3000/lookbook to see the UI. You can then make and test changes to the Lookbook code in your local copy of lookbook repo. PRs are welcome if you add anything useful :-)
Note that changes to files in the Lookbook
lib/
directory will require a server restart in order to have them applied.
You can run the tests from within the lookbook
root directory with the rake test
command.
The gem is available as open source under the terms of the MIT License.