Design System Styling
This guide documents styling rules for Easy8 Design System components.
Scope
- Applies to
app/frontend/src/design_system/**/stylesheets/. - Covers SCSS, BEM classes, tokens, variants, sizes, states, and visual state selectors.
- Does not cover legacy Easy styles, Vuetify overrides, Rails wrappers, or JavaScript business logic.
Styling Layer
- Keep Design System styles next to the owning component.
- Shared Design System styling primitives belong in
app/frontend/src/design_system/shared/stylesheets/. - Do not move legacy UI into Design System styling unless the task is an intentional migration.
- Do not patch legacy Easy styles when a Design System component owns the behavior.
File Structure
- Main component stylesheet:
stylesheets/_<component>.scss. - Local abstracts:
stylesheets/_abstracts/. - Use local abstracts for variables, sizes, states, palettes, and schemes.
- Do not put final component styles in
stylesheets/temp/; temporary styles are ignored by Stylelint.
BEM And Classes
- Root class:
ds-<component>. - Element class:
ds-<component>__<element>. - Modifier class:
ds-<component>--<modifier>. - State class:
is-*, for exampleis-disabled,is-error,is-selected. - Keep reusable class names in
constants/classes.ts. - Keep
constants/classes.tsdeclarative; do not put class-building logic there.
Example:
export const DS_BUTTON_CLASSES = {
Root: "ds-button",
VariantPrimary: "ds-button--primary",
FullWidth: "ds-button--full-width",
AlignLeft: "ds-button--left",
} as const;
SCSS Factory Mixins
- Scope component styles with
factory.component(...); it adds[data-design-system]namespacing automatically. - Use
factory.variantsWithStates,factory.variants,factory.sizes, andfactory.states. - Validate new variants, sizes, and states against
shared/stylesheets/variables/_validators.scss. - Map variants, sizes, and states 1:1 from Figma or the component API.
Example:
@use 'sass:map';
@use '../../../shared/stylesheets/utils/mixins' as factory;
@use '_abstracts/button.variables' as v-button;
@include factory.component(v-button.$name) {
box-sizing: border-box;
display: inline-flex;
align-items: center;
@include factory.variantsWithStates(v-button.$variants, v-button.$name) using($name, $props) {
color: map.get($props, 'text');
background-color: map.get($props, 'background');
}
@include factory.sizes(v-button.$sizes) using($name, $props) {
height: map.get($props, 'height');
padding: map.get($props, 'padding');
}
}
BEM elements and modifiers stay inside the scoped component block:
@include factory.component(v-input.$name) {
&__input-wrapper {
display: flex;
align-items: center;
}
&__input {
flex: 1;
min-width: 0;
background: transparent;
border: none;
}
&--textarea {
align-items: stretch;
.#{v-input.$name}__input-wrapper {
align-items: stretch;
}
}
}
Variables, Schemes, And Palettes
- Keep component token aliases in
_abstracts/_<component>.variables.scss. - Store
$name,$sizes,$variants,$states, and token aliases in variables files. - Use maps for values consumed by factory mixins.
- Use nested
statesinside a variant map when hover, focus, selected, or disabled values depend on the variant. - Use a local
*.scheme.scssor*.palette.scssmixin when a repeated visual color set cannot be expressed cleanly through factory maps. - Keep scheme and palette mixins small; they should apply visual declarations, not layout or behavior.
- Scheme and palette values must still come from Design System tokens or component variables.
Example:
@use 'sass:map';
@use 'button.variables' as v-button;
@mixin set($palette) {
&.#{v-button.$name} {
color: map.get($palette, 'text');
background-color: map.get($palette, 'background');
border-color: map.get($palette, 'border');
&:hover {
background-color: map.get($palette, 'backgroundHover');
}
&:focus-visible,
&:focus-within {
background-color: map.get($palette, 'backgroundFocus');
}
&.is-disabled {
color: map.get($palette, 'textDisabled');
background-color: map.get($palette, 'backgroundDisabled');
border-color: map.get($palette, 'borderDisabled');
}
}
}
Prefer placing disabled styles after interactive states instead of repeating :not(.is-disabled) on every manual hover or focus selector. Use :not(.is-disabled) only when disabled styles cannot reliably override an interactive selector, or when an existing component pattern requires it. Factory-generated states follow the factory implementation.
Tokens And Values
- Use CSS variables from
shared/stylesheets/themes/*.generated.scss. - Do not hard-code Design System colors, spacing, radius, shadows, or semantic visual values.
- Use
px()where Design System SCSS expects converted sizes. - Direct
pxvalues are allowed only where Stylelint permits them, such as borders, box-shadows, and outlines. - Reuse existing utilities, mixins, and foundations before adding new ones.
- Use
// ignore design_system_css_variables.rakeonly for intentional fallback or external variables, not as a normal escape hatch.
Selectors And Nesting
- Prefer class selectors.
- Avoid ID selectors and universal selectors.
- Keep selectors flat where possible.
- Do not use
!important. - Keep nesting shallow; Stylelint enforces stricter Design System nesting rules.
Visual States
- Style disabled, error, selected, pressed, hover, focus, and focus-visible states deliberately.
- Keyboard focus must stay visible.
- Hover must not be the only indication of selected, pressed, invalid, or disabled state.
- Visual state selectors must match the template state source.
State Coupling
.is-disabledmust match disabled behavior and disabled styling..is-errormust match invalid or error styling..is-selected,[aria-selected="true"], and[aria-pressed="true"]must not drift from the real component state.- If an ARIA attribute is used as a styling hook, confirm that the ARIA semantics are valid for the widget pattern.
Example:
<li
:class="{
[DS_OPTION_LIST_CLASSES.item]: true,
[DS_OPTION_LIST_CLASSES.selected]: isSelected,
[DS_OPTION_LIST_CLASSES.disabled]: disabled,
}"
role="option"
:aria-selected="isSelected"
:aria-disabled="disabled || undefined"
>
...
</li>
The class controls the visual state, while ARIA exposes the semantic state. Both must use the same source of truth.
Verification
- Run
yarn stylelintfor Design System SCSS changes when feasible. - Run
bundle exec rake easyproject:design_system_css_variablesafter adding or changing CSS variable usage. - Check the affected Storybook story when visual states change.
Useful commands:
yarn stylelint checks Design System CSS/SCSS and ignores stylesheets/temp. The CSS variable rake task is useful after adding or changing var(...) usage.