CSS: Defaults, Overrides, and Structure
This guide explains how to customize SimplyPages styling safely without forking framework CSS.
Where Default CSS Comes From
The framework ships a default stylesheet at:
simplypages/src/main/resources/static/css/framework.css
It is included by ShellBuilder in generated HTML as:
<link rel="stylesheet" href="/css/framework.css">
ShellBuilder CSS Controls
Use ShellBuilder to control the stylesheet chain:
withFrameworkCss(boolean)toggles framework CSS on/off.withFrameworkCssPath(String)replaces the framework CSS href.withCustomCss(String)sets one custom stylesheet.withCustomCss(List<String>)sets an ordered list of custom stylesheets.addCustomCss(String)appends one custom stylesheet.
CSS Load Order
ShellBuilder.build() loads styles in this order:
- framework CSS (default
/css/framework.css, orwithFrameworkCssPath(...)) - custom stylesheets in configured order
If withFrameworkCss(false) is set, only custom stylesheets are included.
Shared Framework Tokens
framework.css exposes shared :root variables with --sp-* names.
Token model:
- Preferred semantic tokens:
--sp-theme-*(surfaces, text, borders, accents, status colors, gradients). - Component tokens:
--sp-banner-*,--sp-top-nav-*,--sp-home-*,--sp-hero-*for focused shell/module retheming. - Compatibility tokens:
--sp-color-*(retained so older overrides still work).
Start customization by overriding semantic tokens in your app CSS:
:root {
--sp-theme-accent: #0f766e;
--sp-theme-accent-hover: #115e59;
--sp-theme-surface-raised: #f8fafc;
--sp-gradient-surface-panel: linear-gradient(180deg, #ffffff, #f3f7ff);
--sp-space-md: 24px;
--sp-radius-lg: 12px;
}
If you already use --sp-color-* overrides, they remain valid. New projects should prefer --sp-theme-* for clearer intent and broader coverage.
Where You Put Your App CSS
For Spring Boot applications, place app CSS in:
src/main/resources/static/css/app.css(global app CSS)src/main/resources/static/css/pages/<page-name>.css(page-specific CSS)src/main/resources/static/css/modules/<module-name>.css(module-specific CSS)
Reference one or many files through ShellBuilder:
String html = ShellBuilder.create()
.withPageTitle("Portal")
.withCustomCss("/css/app.css")
.addCustomCss("/css/pages/analytics.css")
.addCustomCss("/css/modules/sales-summary.css")
.build();
Cross-App CSS vs Page CSS vs Module CSS
Recommended structure:
- Cross-app/global rules in
/css/app.css - Page-level rules in
/css/pages/... - Module-level rules in
/css/modules/...
Load order strategy:
- framework CSS
- app global CSS
- page CSS (if needed)
- module CSS (if needed)
Later files win on equal specificity.
How to Target Pages and Modules Safely
Use stable IDs/classes from your components and modules.
Examples:
Page page = Page.builder()
.addComponents(new Div().withId("analytics-page"))
.build();
ContentModule module = ContentModule.create()
.withModuleId("sales-summary")
.withClass("sales-summary-module");
Then in CSS:
/* page-level */
#analytics-page .module {
margin-bottom: 24px;
}
/* module-level */
#sales-summary .module-title,
.sales-summary-module .module-title {
letter-spacing: 0.02em;
}
Override and Precedence Rules
CSS precedence in practical terms:
- inline
styleattribute (highest) - stylesheet rules by specificity
- stylesheet rules by source order (later wins if specificity ties)
In SimplyPages code:
.withClass(...)/.addClass(...)adds classes for stylesheet-driven control..addStyle(...)and size helpers (withWidth,withMaxWidth,withMinWidth) write inline style.
Use inline style sparingly for runtime-calculated values.
Button Visibility and Contrast
The framework's .btn rule hides buttons with ordinary hidden using display: none,
so they are not painted or included in sequential keyboard navigation under the default
styles. It deliberately excludes hidden="until-found" (case-insensitive), leaving that
state to browser behavior rather than forcing display: none. Caller CSS or inline styles
can override the default display rule; check visibility and keyboard behavior when customizing
it. Hiding a control is not authorization.
The default secondary button uses white text over the --sp-button-secondary-bg gradient,
with a separate --sp-button-secondary-bg-hover gradient. The default Add Module secondary
control's normal text is intended to meet 4.5:1 contrast across the gradient. If you override
these tokens, text color, or button styles, check contrast across the resulting gradients and
states and preserve visible focus. This targeted default is not a certification of accessibility
for the whole framework or an application.
Should You Edit framework.css Directly?
Default answer: no.
- Preferred: override
--sp-*tokens and layer app selectors in your own CSS files. - Edit framework
framework.cssdirectly only when contributing upstream framework defaults. - If you fork framework CSS in an app, expect manual merge work on framework upgrades.
Minimal Practical Setup
- Keep framework defaults enabled.
- Add
/css/app.cssusingwithCustomCss(...)orwithCustomCss(List<String>). - Namespace page styles under a page root ID.
- Namespace module styles under module IDs/classes.
- Keep overrides in CSS, not scattered inline styles.
Mobile Defaults in Framework CSS
framework.css includes responsive defaults for shell and layout primitives:
- mobile sidebar off-canvas behavior (
.main-sidebar.mobile-open) - mobile sidebar toggle styling (
.mobile-sidebar-toggle) - row/column stacking for narrow screens
- text wrapping guards on columns/module content
- sticky sidebar mobile stacking/collapse hooks
Treat these as baseline behavior and layer app-specific responsive polish in custom CSS.
Advanced Mode: No Framework CSS
You can disable framework CSS:
String html = ShellBuilder.create()
.withFrameworkCss(false)
.withCustomCss("/css/app.css")
.build();
This is supported for advanced integrations. Without replacement styles, many built-in components may appear visually broken because they rely on framework class styling.
Next Reading
For internals and behind-the-scenes load behavior, read:
docs/core/06-shell-project-structure-and-asset-load-chain.md