Design System
This guide is for developers who want to use the Easy8 design system in Vue.js and Rails.
Overview
The design system has two main layers:
- Vue.js components in
app/frontend/src/design_system/ - Rails helpers and custom-element wrappers in
app/easy_design/
In practice, Rails renders easy8-ds-* custom elements and the frontend entrypoint registers the matching Vue components.
Related paths:
app/frontend/src/design_system/: source of DS Vue componentsapp/frontend/src/shared_modules/: higher-level shared components built on top of the DSapp/easy_design/design_system/: Ruby wrappers and helpers for server-rendered usageeasy_storybook/: Storybook configuration and development setup
Discover Components
Start with Storybook before writing UI code.
- Built Storybook in Rails:
/easy_storybook - Local Storybook:
yarn storybook - Local DS-only Storybook:
yarn storybook-ds- it starts Storybook only with design system styles and without easy styles, for performance optimization (easy_styles.css is too big)
Local Storybook runs on http://localhost:6006.
If you need to rebuild the static Storybook served by Rails, use:
This builds Storybook into public/easy_storybook/. Use local Storybook on port 6006 when you want to inspect current changes without rebuilding the static bundle.
Each DS component has its own lowerCamelCase story file in app/frontend/src/design_system/**/<component>.stories.ts, for example dsRadioGroup.stories.ts.
Use the Design System in Vue.js
Import components from the barrel export when possible:
<template>
<DSButton variant="primary" size="md" prefix-icon="plus">
Create project
</DSButton>
<DSTextField
v-model="name"
name="project_name"
label="Project name"
placeholder="Enter project name"
/>
</template>
<script setup lang="ts">
import { ref } from "vue";
import { DSButton, DSTextField } from "@/src/design_system";
const name = ref("");
</script>
Guidelines:
- Prefer
@/src/design_systemexports for DS components - Check the matching
*.stories.tsfile before guessing prop names - Prefer shared modules from
app/frontend/src/shared_modules/for higher-level use cases - For entity autocomplete, prefer the shared
Autocompletemodule instead of assemblingDSSelectmanually
Use the Design System in Rails
For server-rendered UI, prefer DS helpers such as ds_button, ds_alert, ds_select, and ds_datepicker.
These helpers render DS custom elements such as easy8-ds-button. The registration happens in app/frontend/entrypoints/easy_ds_custom_elements.ts.
The mapping is intentionally close across layers, for example DSButton in Vue.js, ds_button in Rails, and <easy8-ds-button> as the custom element.
Example:
<%= ds_button(
variant: :primary,
size: :md,
prefix_icon: "plus"
) do %>
Create project
<% end %>
<%= ds_alert(
variant: :warning,
header: "Unsaved changes",
body: "Your changes are not saved yet.",
closeable: true
) %>
Guidelines:
- Prefer
ds_*helpers over raw HTML when a DS component already exists - Prefer
ds_*helpers over writing raweasy8-ds-*tags directly - Check the component story for the Rails equivalent in
railsCodeTemplate
Use the Design System in Rails Forms
For DS-backed forms, prefer easy_form_for or easy_form_tag. They use EasyFormBuilder and render DS form fields automatically.
Example:
<%= easy_form_for model: @project do |f| %>
<%= ds_grid do %>
<%= ds_grid_layout(distribution: :cols_2) do %>
<%= ds_grid_item do %>
<%= f.text_field :name, required: true %>
<% end %>
<%= ds_grid_item do %>
<%= f.datepicker :start_date, clearable: true %>
<% end %>
<% end %>
<% end %>
<%= f.check_box :is_public, label: "Public project" %>
<%= f.submit "Save" %>
<% end %>
Guidelines:
- Prefer
f.*builder methods insideeasy_form_for - Use
ds_grid,ds_grid_layout, andds_grid_itemfor multi-field layouts - Do not mix classic Rails form helpers with DS form helpers unless there is a real compatibility reason
CSS Variables
The design system relies on CSS variables and shared tokens.
To check whether all used CSS variables are defined, run:
What it does:
- scans SCSS files in
app/frontend/src/design_system/ - checks variables used by DS components
- compares them with variables defined in
app/frontend/src/design_system/shared/stylesheets/ - reports variables that are used but not defined
This is useful after adding or refactoring component styles.
Registry Snapshot and Contract Tests
The Vue components and the Ruby wrappers are two views of the same component API. Contract tests keep them in sync, so a prop added on one side cannot silently drift from the other.
The contract is mediated by a committed snapshot:
app/easy_design/design_system/registry_snapshot.json: generated description of every DS custom element (props with types, prop type hints, model value type, exposed methods, and slots)
The snapshot is derived from the Vue components. Never edit it by hand. Regenerate it with:
Three test layers check the contract:
- A vitest freshness guard (
app/frontend/src/tests/tests/design_system/tests/dsRegistrySnapshot.spec.ts) fails when the committed snapshot no longer matches the Vue components - A vitest registry contract (
dsRegistryContract.spec.tsin the same folder) verifies that the custom elements registry (DS_CUSTOM_ELEMENTS_REGISTRYinapp/frontend/src/design_system/shared/constants/customElements.ts) describes real components: every Boolean, Number, and Date prop has aPROP_TYPE_HINT(without one,data-*attributes reach Vue as raw strings and"false"is truthy), exposed methods exist on a mounted instance, and the model value type matches - An RSpec contract (
spec/easy_design/design_system/registry_contract_spec.rb) renders every Ruby DS component with sentinel values derived from the snapshot and checks the emitteddata-*attributes and<script data-prop>props against the Vue runtime props. Every Vue prop must be settable through a Ruby component param, and every component param must be accepted by itsds_*helper
Props that are intentionally not exposed on the Ruby side (for example frontend-only validation rules) are exempted in spec/easy_design/design_system/registry_contract_config.yml. Every exemption needs a documented reason.
Typical workflow when changing a DS component API:
- Change the Vue component props, slots, or exposed methods, and update the registry entry in
shared/constants/customElements.ts - Run
yarn ds:registry:exportand commit the regenerated snapshot - If the RSpec contract reports a missing param, update the Ruby wrapper in
app/easy_design/design_system/components/and itsds_*helper, or add a documented exemption toregistry_contract_config.yml
Rules of Thumb
- Use a DS component whenever one already exists
- Check Storybook first
- Treat story files as the source of truth for component API
- In Vue, import from
@/src/design_systemunless you need a more specific path - In Rails, prefer
ds_*helpers andeasy_form_for - Prefer shared modules for higher-level functionality
- Do not hard-code DS colors or spacing values in component styles
- After changing a component's public API, run
yarn ds:registry:exportand commit the regenerated snapshot