frontend-conventions

star 305

Bike Index's frontend conventions — Tailwind class prefixing (`tw:`), the standard `twinput`/`twlabel`/`twlink` form/link classes, the `number_display` helper for numbers, and ViewComponent rules (keyword arguments, instance variables, `helpers.` prefix in templates). Trigger when adding or modifying views (`.html.erb`), view components, Stimulus controllers, Tailwind classes, or any frontend code that touches styling or interactivity. **Also trigger before any `mcp__playwright__browser_take_screenshot` call** — this skill defines the required `tmp/` filename rule so screenshots don't land in the project root. Stimulus.js is the JavaScript framework; SCSS and CoffeeScript files exist but are deprecated.

bikeindex By bikeindex schedule Updated 5/28/2026

name: frontend-conventions description: >- Bike Index's frontend conventions — Tailwind class prefixing (tw:), the standard twinput/twlabel/twlink form/link classes, the number_display helper for numbers, and ViewComponent rules (keyword arguments, instance variables, helpers. prefix in templates). Trigger when adding or modifying views (.html.erb), view components, Stimulus controllers, Tailwind classes, or any frontend code that touches styling or interactivity. Also trigger before any mcp__playwright__browser_take_screenshot call — this skill defines the required tmp/ filename rule so screenshots don't land in the project root. Stimulus.js is the JavaScript framework; SCSS and CoffeeScript files exist but are deprecated.

Frontend conventions

This project uses Stimulus.js for JavaScript interactivity and Tailwind CSS for styling. There are SCSS styles and CoffeeScript files, but they are deprecated — don't add to them.

The bin/dev command handles building and updating Tailwind and JS.

Format ERB before committing. After editing any .html.erb, run bin/lint <file> — it runs herb-format, which sorts tw: classes and reflows long class attributes onto multiple lines. CI's lint_and_scan job runs herb-format --check as a step separate from standardrb/rubocop, so hand-edited ERB that skips formatting fails CI even when the Ruby is clean.

Tailwind classes and helpers

  • Tailwind classes have the prefix tw: (e.g. tw:text-blue, tw:flex, tw:gap-4).
    • The tw: prefix comes before variant modifiers, not after. Use tw:dark:bg-gray-800, tw:hover:bg-blue-600, tw:sm:flex, tw:focus-visible:ring-2. Never dark:tw:bg-gray-800 — variant prefixes layer on top of tw:.
  • Form fields should use the twinput class.
  • Labels should use the twlabel class.
  • Basic links should use the twlink class.
  • Every number should be rendered with number_display(number). This applies even when a number is composed into a string with non-numeric values — wrap the number itself, not the surrounding string.
    • Good: [number_display(@bike.year), @bike.mnfg_name].join(" ")
    • Bad: [@bike.year, @bike.mnfg_name].join(" ")
    • "Number" includes years, counts, prices, distances, IDs — anything numeric, even when it reads like a label.
  • Currency amounts use amount_display(obj) instead of number_display directly. It takes an object that responds to amount_cents, amount, currency_symbol, and currency_name (e.g. a MarketplaceListing), and renders the symbol + number_display(amount) together. Don't reach for number_to_currency or roll your own.

No dead hooks in markup

Only add an id or non-utility class when something concrete consumes it — a CSS rule, a JS/Stimulus selector, a test fixture, an accessibility attribute. Don't keep or invent "structural identifier" hooks "in case something needs them later," and don't replace a removed hook with a renamed one out of inertia.

When deleting an id/class, grep the repo for the name before deciding what to do with it:

  • Zero consumers: delete it, don't rename it.
  • Consumers exist: either update them, or leave the hook in place — the consumers are the reason it earns its spot in the markup.

ViewComponent rules

This project uses the ViewComponent gem to render components.

  • Prefer view components to partials.
  • Generate a new view component with rails generate component ComponentName argument1 argument2.
  • View components must initialize with keyword arguments. Everything the component needs must be passed in explicitly by the caller — never reach into controller state from inside a component (e.g. controller.instance_variable_get(:@bike)). If the component needs @bike, the caller renders Component.new(bike: @bike).
  • In view components, use instance variables directly — don't add attr_reader/attr_accessor. Reference @foo everywhere, including in the template (@current_user, not current_user).
  • In ViewComponent templates, use the helpers. prefix for view helpers (e.g. helpers.time_ago_in_words).
    • Rule of thumb: try the bare call first. Only add helpers. if it fails with NoMethodError — route helpers (new_bike_path) and ActionView tag/url builders (tag.span, content_tag, link_to) are mixed into ViewComponent::Base directly, so they don't need it.
  • Never nest a component inside a folder that already holds a component.rb. Each component lives in app/components/<path>/component.rb (and spec/components/<path>/component_spec.rb); siblings go in sibling folders, not subfolders. If you have search/everything_combobox/component.rb and need a related component, place it at search/everything_combobox_options/component.rb (module Search::EverythingComboboxOptions), not search/everything_combobox/options/component.rb.
  • Converting a partial to a component is a faithful move, not a cleanup. Carry the markup over verbatim — including comments and commented-out code. Those lines are often a deliberate stash (a link that's temporarily disabled, a snippet someone expects to restore), so dropping them silently loses intent and surprises the reviewer, who expects the diff to read as "same content, new home." The only changes a conversion should introduce are the mechanical ones the move requires: t(".x")translation(".x"), adding helpers. where a helper now needs it, and the like. If you spot something that genuinely looks like dead code worth removing, that's a separate judgment call — raise it with the user or do it in its own commit, don't fold it into the move.

Playwright screenshot filenames

Every mcp__playwright__browser_take_screenshot call must pass a filename: that starts with tmp/ (e.g. tmp/tooltip-hover.png). The MCP tool's default root is the project root — a bare filename like tooltip.png lands in the working tree, shows up in git status, and has to be cleaned up by hand. tmp/ is gitignored, so screenshots there stay out of commits and don't pollute the diff. This rule applies to ad-hoc visual verification, not just PR-screenshot capture.

Install via CLI
npx skills add https://github.com/bikeindex/bike_index --skill frontend-conventions
Repository Details
star Stars 305
call_split Forks 76
navigation Branch main
article Path SKILL.md
More from Creator