NAME scss - SCSS file organization and class scoping conventions SYNOPSIS Action-scoped SCSS for SPA actions and Blade views: // rsx/app/frontend/dashboard/dashboard_index_action.scss .Dashboard_Index_Action { .card { ... } .stats-grid { ... } } Component-scoped SCSS for theme components: // rsx/theme/components/sidebar/sidebar_nav.scss .Sidebar_Nav { .nav-item { ... } .nav-link { ... } } Variables may be declared outside the wrapper for sharing: // rsx/app/frontend/frontend_spa_layout.scss $sidebar-width: 215px; $header-height: 57px; .Frontend_Spa_Layout { .sidebar { width: $sidebar-width; } } DESCRIPTION RSX enforces a class-scoping convention for SCSS files to prevent CSS conflicts and ensure styles are self-contained. Every SCSS file in rsx/app/ or rsx/theme/components/ must wrap ALL rules inside a single top-level class selector that matches its associated JavaScript class or Blade view ID. This is essentially manual CSS scoping - like CSS Modules but enforced by convention. The benefit is predictable specificity, no conflicts between pages/components, and self-documenting file organization. Key principle: The SCSS filename must match the filename of its associated .js (action/component) or .blade.php file. COMPONENT-FIRST PHILOSOPHY Every styled element should be a component. If an element needs custom styles, it deserves a name, a jqhtml definition, and scoped SCSS. This eliminates CSS spaghetti - generic classes like .page-header, .filter-bar, .action-buttons scattered across files, overriding each other unpredictably. Pattern Recognition: When building a page, ask: "Is this structure unique, or a pattern?" Pattern (shared structure): A datagrid page with toolbar, tabs, filters, and search appears on 8 different pages. Create Datagrid_Card once with slots, use it everywhere. Changes propagate automatically. Unique (one-off structure): A project dashboard with custom widgets specific to that page. Create Project_Dashboard for that page alone. Decision heuristic: If you're about to copy-paste structural markup, stop and extract a component. Slot-Based Composition: Use slots to separate structure from content. The component owns layout and styling; pages provide the variable parts via slots. // Datagrid_Card owns the structure
<%= content('toolbar') %>
<%= content('body') %>
// Page provides content via slots This keeps pages declarative and components reusable. What Remains Shared: Only primitives should be shared/unscoped styles: - Buttons (.btn-primary, .btn-secondary) - Spacing utilities (.mb-3, .p-2) - Typography (.text-muted, .fw-bold) - Bootstrap overrides Everything else - page layouts, card variations, custom UI patterns - should be component-scoped SCSS. SCOPING RULES Files in rsx/app/**/*.scss and rsx/theme/components/**/*.scss: Must be fully enclosed in a class matching either: - A Component subclass (Spa_Action, Spa_Layout, or direct Component) - A Blade view's @rsx_id value (server-rendered page styles) The SCSS filename must match the associated JS or Blade file's filename (with .scss extension instead of .js/.blade.php). Example - SPA Action: rsx/app/frontend/dashboard/Dashboard_Index_Action.js rsx/app/frontend/dashboard/dashboard_index_action.scss // dashboard_index_action.scss .Dashboard_Index_Action { // ALL styles nested here } Example - SPA Layout: rsx/app/frontend/Frontend_Spa_Layout.js rsx/app/frontend/frontend_spa_layout.scss // frontend_spa_layout.scss .Frontend_Spa_Layout { // ALL layout styles nested here .app-sidebar { ... } .app-content { ... } } Example - Blade View: rsx/app/login/login_index.blade.php // has @rsx_id('Login_Index') rsx/app/login/login_index.scss // login_index.scss .Login_Index { // ALL styles nested here } Example - Theme Component: rsx/theme/components/sidebar/sidebar_nav.js rsx/theme/components/sidebar/sidebar_nav.scss // sidebar_nav.scss .Sidebar_Nav { // ALL styles nested here } Files elsewhere: SCSS files outside these paths are not validated by this rule and can be organized as needed (e.g., global utilities, variables in rsx/theme/base/). SCSS VARIABLES SCSS variable declarations ($var: value;) are allowed OUTSIDE the wrapper class. This enables variables to be shared when the file is imported by other SCSS files. Example - Variables outside wrapper: // frontend_spa_layout.scss $sidebar-width: 215px; $header-height: 57px; $mobile-breakpoint: 991.98px; .Frontend_Spa_Layout { .sidebar { width: $sidebar-width; } .header { height: $header-height; } } The manifest scanner strips variable declarations before checking for the wrapper class, so they do not cause validation failures. VARIABLES-ONLY FILES Files containing ONLY variable declarations and comments (no actual CSS rules or selectors) are considered valid without a wrapper class. These are typically partial files intended to be imported by others. Example - Variables-only file (valid): // _variables.scss $primary-color: #0d6efd; $secondary-color: #6c757d; $border-radius: 0.375rem; Such files are marked with scss_variables_only in the manifest and skip wrapper validation entirely. SUPPLEMENTAL SCSS FILES When a single SCSS file becomes unwieldy, you can split styles into multiple files. Supplemental SCSS files may have different filenames as long as: 1. A primary SCSS file exists with the matching filename (e.g., frontend_spa_layout.scss for Frontend_Spa_Layout) 2. The supplemental file uses the SAME wrapper class as the primary This allows organizing styles by breakpoint, feature, or logical grouping while maintaining the scoping convention. Example - Splitting by breakpoint: rsx/app/frontend/ frontend_spa_layout.scss // Primary file (required) frontend_spa_layout_mobile.scss // Supplemental - mobile styles frontend_spa_layout_print.scss // Supplemental - print styles // frontend_spa_layout.scss (primary) .Frontend_Spa_Layout { .sidebar { width: 215px; } .header { height: 57px; } } // frontend_spa_layout_mobile.scss (supplemental) .Frontend_Spa_Layout { @media (max-width: 768px) { .sidebar { width: 100%; } .header { height: 48px; } } } // frontend_spa_layout_print.scss (supplemental) .Frontend_Spa_Layout { @media print { .sidebar { display: none; } .no-print { display: none; } } } The primary file MUST exist first. Without it, supplemental files will fail validation with a filename mismatch error. BENEFITS No CSS Conflicts: .notice-item in Dashboard_Index_Action won't affect .notice-item in Calendar_Index_Action because they're in different scope wrappers. Self-Documenting: File name tells you exactly which action/component it styles. Delete the action -> delete its SCSS -> no orphaned styles. Simple Class Names: Use .team-grid instead of .dashboard-index-action__team-grid. The wrapper provides the scoping automatically. Predictable Specificity: All page/component styles get the same specificity boost from being nested under their wrapper class. Safe Refactoring: Moving or renaming an action means moving/renaming its SCSS. No hunting through global stylesheets for related rules. HOW IT WORKS jqhtml components and Spa_Action classes automatically add their class name to the root DOM element. For example, a component defined as will have class="Sidebar_Nav" on its root. Blade views use @rsx_id('View_Name') which can be output to the DOM for the same scoping effect. The manifest scanner detects if an SCSS file is fully enclosed in a single class rule by: 1. Removing comments 2. Stripping SCSS variable declarations ($var: value;) 3. Checking if remaining content matches pattern: .ClassName { ... } 4. Verifying bracket balance (all content inside the wrapper) A code quality rule then validates: 1. The wrapper class exists (or file is variables-only) 2. It matches a valid Component subclass or Blade @rsx_id 3. The filename matches the associated file BEM CHILD CLASSES When using BEM notation inside component SCSS, child element class names must preserve the component's exact PascalCase class name as the prefix. Do NOT convert to kebab-case. The SCSS nesting syntax compiles &__element to the parent selector plus __element. Since the parent is .Component_Name, the result is .Component_Name__element - and HTML must use that exact class. Correct: // SCSS .DataGrid_Kanban { &__loading { ... } &__board { ... } &__column { ... } } // HTML (jqhtml template)
Wrong: // HTML - kebab-case does NOT match compiled CSS
// No styles applied!
// No styles applied! This is a common mistake when following general web conventions where BEM uses kebab-case. In RSX, component class names are PascalCase, so BEM children must also be PascalCase. NO EXEMPTIONS There are NO exemptions to this rule for files in rsx/app/ or rsx/theme/components/. Every SCSS file in these directories must be scoped to its associated action, layout, component, or view. If a file cannot be associated with any of these (extremely rare), it likely belongs elsewhere: - rsx/theme/base/ for global utilities and variables - rsx/theme/layouts/ for shared layout styles - A dedicated partial imported via @use Moving files outside the enforced directories requires explicit developer approval and should be carefully considered. In 99% of cases, the SCSS file should be properly scoped. VALIDATION The scoping rule is enforced at manifest build time. Violations produce errors like: SCSS file 'rsx/app/frontend/dashboard/dashboard.scss' must be fully enclosed in a single class rule matching a Component or Blade @rsx_id. Expected: .Dashboard_Index_Action { ... } Found: No wrapper class detected Or for wrapper class mismatches: SCSS wrapper class 'Frontend_Dashboard' does not match any Component class or Blade @rsx_id Or for filename mismatches: SCSS filename 'styles.scss' must match associated Component file 'dashboard_index_action' EXAMPLES Correct - SPA Action Styles: // rsx/app/frontend/invoices/invoices_view_action.scss .Invoices_View_Action { .invoice-header { display: flex; justify-content: space-between; } .line-items { .item-row { border-bottom: 1px solid #eee; } } @media (max-width: 768px) { .invoice-header { flex-direction: column; } } } Correct - Layout with Variables: // rsx/app/frontend/frontend_spa_layout.scss $sidebar-width: 215px; $header-height: 57px; .Frontend_Spa_Layout { .app-sidebar { width: $sidebar-width; position: fixed; } .app-header { height: $header-height; } } Correct - Component Styles: // rsx/theme/components/modal/rsx_modal.scss .Rsx_Modal { .modal-header { border-bottom: 1px solid var(--border-color); } .modal-body { padding: 1.5rem; } &.modal-lg { .modal-dialog { max-width: 800px; } } } Incorrect - Multiple Top-Level Rules: // BAD: Multiple selectors at top level .Dashboard_Index_Action { .card { ... } } .sidebar { // ERROR: This is outside the wrapper width: 200px; } Incorrect - No Wrapper: // BAD: No wrapper class .card { padding: 1rem; } .stats-grid { display: grid; } SEE ALSO spa - SPA routing and actions jqhtml - Component template system coding_standards - General naming conventions code_quality - Code quality rule system