Search by

techkodeninja / chassis

techkodeninja

Shared structure and UI building blocks for themes: SCSS tokens, layout objects, and components, with service providers for Backdrop (ClassicPress) and Novaris.

Package info

github.com/techkodeninja/chassis

Language:SCSS

pkg:composer/techkodeninja/chassis

Statistics

Installs: 6

Dependents: 0

Suggesters: 0

Stars: 0

Open Issues: 0

0.0.2 2026-11-06 07:16 UTC

README

Shared structure and UI building blocks for themes. One package, used the same way by ClassicPress themes (built on Backdrop) and Novaris sites:

  • SCSS: settings, a reset, base element styles, the page skeleton (header, mobile menu, content with sidebar, footer), layout objects, components, and utilities. Your project's own build (Vite + Sass) compiles it.
  • JS: the mobile menu toggle.
  • PHP: small helpers that print component markup, with a service provider for each framework.

New here? Build a theme from scratch walks through a complete ClassicPress theme that uses every part of the package.

How it's organized

The SCSS follows ITCSS: layers go from the most general to the most specific, and each layer can use everything above it.

The numbers are the load order, the same convention as novaris-dev/novaris. Each piece is a numbered folder with an _index.scss (scss/01.settings/02.colors/_index.scss).

Layer Folder What's in it Outputs CSS?
Settings scss/01.settings 01.breaks, 02.colors, 03.fonts, 04.sizings, 05.spacings, 06.design No
Tools scss/02.tools 01.breakpoints: up() / down() mixins; 02.functions: color() / space() / width() No
Generics scss/03.generics 01.tokens: :root custom properties; 02.resets Yes
Elements scss/04.elements 01.defaults, 02.headings, 03.links, 04.tables Yes
Objects scss/05.objects 01.containers, 02.sections, 03.grids Yes
Layouts scss/06.layouts 01.app, 02.header, 03.navigation, 04.content, 05.footer Yes
Components scss/07.components 01.cards, 02.badges, 03.buttons, 04.section-headers Yes
Utilities scss/08.utilities 01.a11y, 02.alignments, 03.display, 04.text Yes

What belongs in the package, and what stays in your theme: the package handles structure (how the header lays out, when the menu collapses, how a grid drops to one column). Your theme handles the look (colors, fonts, the style of posts and comments, anything unique). Colors and spacing are settings, so each theme sets its own.

Requirements

  • PHP 7.4 or later and Composer
  • Node with Vite and sass (or any Sass build) to compile the SCSS

Install

composer require techkodeninja/chassis

Then turn Chassis on with its service provider.

Styles

Chassis loads its own compiled CSS through the service provider, so a theme doesn't compile it. A theme's stylesheet loads after Chassis and changes it with custom properties:

// resources/scss/screen.scss
:root {
	--color-header-background: #1d3557;
	--color-accent: #e63946;
	--font-family-body: 'Merriweather', serif;
	--radius: 0.5rem;
}

You only list what you change, and everything else keeps its default. Breakpoints are fixed, because media queries can't read custom properties.

Use the tools in your own partials

Sass can read the package from vendor. Add loadPaths: [ 'vendor' ] to your Sass options, run composer install before you build, and load just the tools. They output no CSS, so this is safe in any partial.

// resources/scss/07.components/01.entry/_index.scss
@use 'techkodeninja/chassis/scss/02.tools' as ui;

.entry {
	padding: ui.space( m );                 // var( --space-m )
	border-bottom: var( --border-rule );

	@include ui.up( medium ) {              // 768px and wider
		padding: ui.space( xl );
	}
}

ui.color( name ), ui.space( name ), and ui.width( name ) return the matching var( --… ), and fail the build if the name doesn't exist.

Settings reference

These are the package's Sass settings, used to build dist/. A theme sets the matching custom property instead.

Setting Defaults Custom properties
$breakpoints small 40rem (640px), medium 48rem (768px), large 64rem (1024px), wide 80rem (1280px) none (media queries can't use custom properties)
$colors See Colors --color-*
$spacing xs .25rem, s .5rem, m .75rem, l 1.125rem, l-plus 1.5rem, xl 2.5rem --space-*
$widths wide 80rem, medium 64rem, narrow 50rem, sidebar 18rem --width-*
$font-family-body, $font-family-heading, $font-family-code system fonts --font-family-*
$font-family-meta the body font --font-family-meta (dates and small labels)
$font-size-base, $line-height-base, $line-height-heading 1rem, 1.7, 1.25 same names
$border-width 0.063rem --border-width, --border-dotted, --border-solid, --border-rule

Colors

Colors are grouped by where they appear. Copy this block as a starting point, and keep only the ones you change. Everything else keeps its default.

:root {
	// Global
	--color-page-background: #ffffff;  // behind everything
	--color-link:            #000000;  // links
	--color-link-hover:      #5c7079;  // links on hover
	--color-accent:          #000000;  // buttons and badges
	--color-accent-text:     #ffffff;  // text on buttons and badges

	// Header
	--color-header-background: #000000;  // the top bar
	--color-header-text:       #ffffff;  // site title, tagline, menu links

	// Content
	--color-content-background: #ffffff;  // post boxes and cards
	--color-content-border:     #000000;  // borders around those boxes
	--color-content-heading:    #000000;  // post titles, h1 to h6
	--color-content-text:       #5c7079;  // paragraphs
	--color-content-muted:      #555555;  // captions, quieter text
	--color-content-divider:    #cccccc;  // lines between things
	--color-content-alt:        #f5f5f5;  // muted boxes, code blocks

	// Widget: these follow the content colors unless you set them
	--color-widget-background: #ffffff;  // sidebar boxes
	--color-widget-border:     #000000;  // borders around them
	--color-widget-title:      #000000;  // "Categories", "Archives"
	--color-widget-text:       #5c7079;  // text inside them

	// Footer
	--color-footer-background: #000000;  // the bottom bar
	--color-footer-text:       #ffffff;  // footer text and links
}

--color-black and --color-white are available too.

Design settings

These control the look of the shared pieces, so each theme can have its own design while the package stays the same. The defaults keep the package's standard look.

Setting Default Custom property
$radius 0 --radius (cards, buttons, badges)
$shadow none --shadow (cards)
$box-border-style, $box-border-bottom-style dotted, solid --border-box, --border-box-bottom
$font-weight-heading 700 --font-weight-heading
$heading-sizes h1 2.25rem, h2 1.75rem, h3 1.375rem, h4 1.125rem, h5 1rem, h6 0.875rem --font-size-h1 to --font-size-h6
$header-padding var( --space-m ) --header-padding
$header-title-size, $header-title-size-large 1.5rem, 1.75rem --header-title-size, --header-title-size-large
$header-tagline-size 0.9375rem --header-tagline-size
$nav-link-padding var( --space-s ) var( --space-m ) --nav-link-padding
$button-border-width, $button-padding 0.125rem, var( --space-s ) 1.25rem --button-border-width, --button-padding
$badge-font-size, $badge-letter-spacing, $badge-padding, $badge-text-transform 0.75rem, 0.08em, 0.2rem 0.6rem, uppercase --badge-font-size, --badge-letter-spacing, --badge-padding, --badge-text-transform
$pagination-padding var( --space-xs ) var( --space-m ) --pagination-padding
$content-padding var( --space-xl ) --content-padding (above and below the content)
$sidebar-gap var( --space-xl ) --sidebar-gap (between the content and sidebar)
$entry-padding var( --space-l ) --entry-padding (inside post boxes)
:root {
	--color-page-background: #0f1117;
	--color-content-text: #d7dae0;
	--color-content-border: #2d3446;
	--radius: 0.5rem;
	--border-box: var( --border-width ) solid var( --color-content-border );
	--header-padding: var( --space-s );
}

Dark themes should set --color-content-border, because it defaults to black.

Layouts: the page skeleton

<body>
<a class="skip-link screen-reader-text" href="#main">Skip to content</a>
<div class="app">

	<header class="app-header">
		<div class="app-header__inner container">
			<div class="app-header__branding">
				<a class="app-header__title" href="/">Site Title</a>
				<span class="app-header__tagline">Tagline</span>
			</div>
			<nav class="app-nav" aria-label="Primary">
				<button class="app-nav__toggle" type="button" aria-expanded="false" aria-controls="app-nav-menu">Menu</button>
				<ul class="app-nav__menu" id="app-nav-menu">
					<li><a href="/" aria-current="page">Home</a></li>
					<li><a href="/blog">Blog</a>
						<ul><li><a href="/blog/news">News</a></li></ul>
					</li>
				</ul>
			</nav>
		</div>
	</header>

	<div class="app-content app-content--sidebar-right container">
		<main id="main" class="app-main">…</main>
		<aside class="app-sidebar">…</aside>
	</div>

	<footer class="app-footer">
		<div class="app-footer__inner container">
			<p class="app-footer__credit">© 2026 Site Title</p>
		</div>
	</footer>

</div>
</body>
Class What it does
.app Wraps the page; keeps the footer at the bottom on short pages
.app-header (__inner, __branding, __title, __tagline) · --full Branding left, menu right. Tagline hides on phones. --full lets the row run the full width of the screen
.app-nav (__toggle, __menu) · --bottom Phones: Menu button opens a panel below the header. medium and up: a row of links with dropdowns. --bottom moves the phone button to the bottom left, with the menu above it
.app-content · --sidebar-right · --sidebar-left Content area; the sidebar sits beside the main column from large up and stacks below it before that
.app-main, .app-sidebar The two columns
.app-footer (__inner, __credit) Footer band

The menu is styled by its structure (li, a, nested ul), so the markup from ClassicPress's wp_nav_menu() and Novaris's menu works once the container has .app-nav and the list has .app-nav__menu. In Novaris, pass those as container_class and menu_class. The button can be .app-nav__toggle or the .menu-toggle that Novaris prints. The current page is underlined, marked by aria-current="page", .current-menu-item, or .menu-items__item--current.

Objects

Class What it does
.container · --medium · --narrow Centered, max-width, side padding
.section · --muted Full-width band with vertical padding; --muted is light gray
.grid · --2 · --3 · --4 One column on phones; 2 or 3 columns from medium; --4 is 2 then 4 from large. Works on lists
.gallery · --3 · --4 A grid of images that share the same shape. Two columns on phones

Components

Class What it does
.card · --muted Dotted box; its last child (a link or button) sits at the bottom
.badge · --large Small uppercase label in the accent color
.button · --primary, .buttons Outlined / filled button; a wrapping row of buttons
.section-header (__title, __description) Centered heading block
.pagination (__items, __item, __anchor) Numbered page links. Novaris's default pagination-items classes work too
.widget, .widget-title Sidebar boxes, as printed by Novaris and ClassicPress
.code-copy The Copy button added to code blocks in post content
.app-main > article, .entry__header, __metadata, __title, __title-anchor, __thumbnail, __content, __more-link, __terms Posts and pages, in a box that matches the widgets
.social-menu A row of icon links
<section class="section section--muted">
	<div class="container">
		<header class="section-header">
			<h2 class="section-header__title">Latest Posts</h2>
		</header>
		<ul class="grid grid--3">
			<li class="card">
				<span class="badge">News</span>
				<h3>Title</h3>
				<p>Summary…</p>
				<a class="button" href="#">Read more</a>
			</li>
		</ul>
	</div>
</section>

Platforms

The core works on Novaris and ClassicPress. ClassicPress prints some markup that themes can't rename, so that lives in an add-on, dist/chassis-classicpress.css. The Backdrop provider loads it for you.

It covers captions, sticky posts, threaded comments and the comment form, the search form, the calendar widget, the_posts_pagination(), entry-* classes, and the [gallery] shortcode. It uses the same settings as the core, so a theme's colors and shapes apply here too. Novaris sites don't need it.

Utilities

Class What it does
.screen-reader-text Hidden visually, still read by screen readers
.skip-link With .screen-reader-text: appears on keyboard focus (any .screen-reader-text does)
.alignleft, .alignright, .aligncenter, .alignwide, .alignfull The editor's alignment classes; .alignwide uses --width-wide
.hide-mobile, .hide-desktop Hide below / from the medium breakpoint
.text-left, .text-center, .text-right, .text-muted, .text-small Text helpers

JavaScript

dist/chassis.js runs the mobile menu and the Copy button on code blocks, and the service provider loads it. The menu toggles .app-nav--open and aria-expanded, closes on Escape (returning focus to the button), on a click outside, and when the screen widens past the mobile breakpoint. Links with a submenu get a button that opens it, which helps on touch screens.

Turning Chassis on

Chassis doesn't load until a site registers its service provider. Nothing else turns it on.

Novaris. Register the provider in config/app.php:

'providers' => [
	Techkodeninja\Chassis\Providers\Novaris::class,
],

Then print the files in the theme's views, with styles() before the theme's own stylesheet. Check for Chassis first, so the site still loads without it:

<?= app( 'chassis' ) ? app( 'chassis' )->styles() : '' ?>   // in the head
<?= app( 'chassis' ) ? app( 'chassis' )->scripts() : '' ?>  // at the end of the page

The provider copies the built files into public/assets/chassis, because browsers can't reach vendor. If it can't write there, it logs a warning and prints nothing.

ClassicPress. Register Providers\Backdrop in the theme's app/framework.php. It loads the CSS, the ClassicPress add-on, and the JavaScript for you.

A theme sets its design in its own stylesheet, which loads after Chassis:

:root {
	--color-page-background: #0f1117;
	--color-accent: #7c9cff;
}

Colors, spacing, fonts, and widths all work this way. Breakpoints are fixed at the defaults, because media queries can't read custom properties. A theme can still use the Sass helpers, which print no CSS of their own:

@use 'techkodeninja/chassis/scss/02.tools' as ui;

PHP

Novaris

The helpers are for sites, not distributed themes. An exported theme doesn't include vendor, and a site's providers setting replaces the theme's, so a theme should write the class names directly. To use the helpers on a site, require the package in that site's composer.json, then register the provider in config/app.php:

'providers' => [
	Techkodeninja\Chassis\Providers\Novaris::class,
],
<?= app( 'chassis' )->button( 'Download', $url, [ 'primary' ] ); ?>

The provider also adds template tags, so Novaris views can use $engine->button( 'Download', $url, [ 'primary' ] ) and $engine->badge( 'Featured' ).

ClassicPress (Backdrop)

Register the provider in your theme's app/framework.php:

$theme->provider( Techkodeninja\Chassis\Providers\Backdrop::class );
<?= Backdrop\App::resolve( 'chassis' )->badge( 'Featured' ); ?>

Each project only loads its own provider, so a ClassicPress theme never needs Novaris and a Novaris site never needs Backdrop.

Which platform is running

The provider a site registers decides the platform, so nothing has to guess. platform() returns novaris, backdrop, or plain when no provider is in use:

if ( 'novaris' === app( 'chassis' )->platform() ) {
	// Novaris version
}

ClassicPress without a framework

Techkodeninja\Chassis\Chassis has no framework dependency, so a theme can load Composer's autoloader and create it directly:

$ui = new Techkodeninja\Chassis\Chassis();
echo $ui->button( 'Download', $url, [ 'primary' ] );

Helpers

Method Returns
badge( $text, $modifiers = [] ) <span class="badge …">
button( $text, $url, $modifiers = [] ) <a class="button …">
classes( $block, $modifiers = [] ) 'button button--primary'
scssPath( $append = '' ), jsPath( $append = '' ) Paths to the package's SCSS and JS

Text and URLs are escaped for you.

License

GPL-2.0-or-later. See license.md.