@maxter-dev/design-system
v1.1.1
Published
Framework-neutral CSS and Sass foundations for Maxter Dev products
Readme
@maxter-dev/design-system
Framework-neutral CSS and Sass foundations for Maxter Dev products. The package provides primitive scales, semantic intent tokens, component aliases, and complete Cyberfab and MaxterDev light, dark, and automatic themes.
Version 1.0.0 is a clean-break release. It does not ship legacy theme assets, generic utility classes, or compatibility aliases. See the repository migration guide when upgrading from the historical Angular-owned styles.
CSS
Import the complete stylesheet:
@import "@maxter-dev/design-system/css";Focused CSS entry points are also available. They contain only the fixed layer declaration and their recipes; import tokens and a theme first:
@import "@maxter-dev/design-system/tokens.css";
@import "@maxter-dev/design-system/themes/default.css";
@import "@maxter-dev/design-system/components.css";
@import "@maxter-dev/design-system/layouts.css";
@import "@maxter-dev/design-system/motion.css";@maxter-dev/design-system/tokens.css@maxter-dev/design-system/themes/default.css@maxter-dev/design-system/components.css@maxter-dev/design-system/layouts.css@maxter-dev/design-system/motion.css
Every generated minified stylesheet has an explicit public entry point:
@maxter-dev/design-system/css.min.css@maxter-dev/design-system/tokens.min.css@maxter-dev/design-system/themes/default.min.css@maxter-dev/design-system/components.min.css@maxter-dev/design-system/layouts.min.css@maxter-dev/design-system/motion.min.css
Sass
Use the package importer supported by modern Sass:
@use "pkg:@maxter-dev/design-system/sass";
@use "pkg:@maxter-dev/design-system/sass/tokens";
@use "pkg:@maxter-dev/design-system/sass/tools";The Sass entry points emit no CSS on their own. They expose token maps such as $colors, $spacing, $font-sizes, $breakpoints, $containers, $cyberfab-light-theme, $cyberfab-dark-theme, $maxterdev-light-theme, and $maxterdev-dark-theme.
@use "pkg:@maxter-dev/design-system/sass" as maxterdev;
.dashboard {
@include maxterdev.breakpoint-up(48) {
display: grid;
}
@include maxterdev.container-up(30) {
grid-template-columns: 1fr 1fr;
}
}Breakpoint keys are 30, 48, 64, 80, and 96, expressed in rem. Container keys are 20, 30, 40, 48, 64, and 80.
Tokens
All public custom properties use the --maxterdev- prefix and follow a strict dependency hierarchy:
- Primitive scales contain raw values, for example
--maxterdev-color-cyberfab-brand-700,--maxterdev-space-16, and--maxterdev-font-size-16. - Semantic aliases describe intent, for example
--maxterdev-color-action-background,--maxterdev-color-focus-ring, and--maxterdev-font-body-size. - Component aliases are narrowly scoped decisions, for example
--maxterdev-button-primary-background,--maxterdev-field-focus-border,--maxterdev-surface-default-background, and--maxterdev-card-default-padding.
Dimensional tokens migrated from px use rem against the browser root (16px by default), while public token names and Sass keys retain their nominal labels. Border-radius tokens remain in px; the 0.02px subtraction in breakpoint-down is an intentional precision exception.
Use semantic or component aliases in product CSS instead of binding components directly to raw palette values:
.call-to-action {
color: var(--maxterdev-button-primary-foreground);
background: var(--maxterdev-button-primary-background);
border-radius: var(--maxterdev-button-radius);
}
.call-to-action:focus-visible {
outline: none;
box-shadow: var(--maxterdev-button-focus-shadow);
}Themes
The default theme stylesheet provides Cyberfab Light values on :root. Set data-maxterdev-theme on the document root or any nested scope where normal CSS inheritance applies:
<html data-maxterdev-theme="cyberfab-auto">
<body>
<section data-maxterdev-theme="maxterdev-dark">MaxterDev dark themed content</section>
</body>
</html>Supported values are cyberfab-light, cyberfab-dark, cyberfab-auto, maxterdev-light, maxterdev-dark, and maxterdev-auto. Automatic themes start with the complete light contract for their brand and replace it with the complete dark contract under prefers-color-scheme: dark; each mode also sets color-scheme for browser-provided UI.
The old light, dark, and auto selectors are removed. Primitive palette values use branded families such as --maxterdev-color-cyberfab-brand-600 and --maxterdev-color-maxterdev-brand-600.
Import tokens.css together with themes/default.css when using focused entries. The complete css entry already includes both exactly once in the fixed maxterdev.* cascade layer order.
Foundation
The reset and base styles are opt-in. Put maxterdev-foundation on the subtree that should receive box sizing, inherited form typography, base colors, and accessible focus defaults. maxterdev-sr-only visually hides accessible text, and maxterdev-skip-link becomes visible on keyboard focus.
<body class="maxterdev-foundation">
<a class="maxterdev-skip-link" href="#main">Skip to content</a>
<main id="main" tabindex="-1">...</main>
</body>Buttons
Use a native button whenever possible. Variants are primary, secondary, ghost, text, danger, icon, and selectable; sizes are sm, md, and lg. SVG icons inherit currentColor. Icon-only buttons require an accessible name. For non-native controls with aria-disabled="true", application code must also suppress activation and keyboard handling. Busy buttons suppress hover and active presentation while retaining their resting or pressed appearance and showing a progress cursor. aria-busy="true" is presentation-only; applications must suppress activation or also apply native/ARIA disabled state when a busy action must be inoperable.
<button class="maxterdev-button maxterdev-button--primary maxterdev-button--md" type="button">
<svg class="maxterdev-button__icon" aria-hidden="true" viewBox="0 0 24 24">...</svg>
<span class="maxterdev-button__label">Save</span>
</button>
<button class="maxterdev-button maxterdev-button--icon" type="button" aria-label="Close">
<svg class="maxterdev-button__icon" aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>
<button class="maxterdev-button maxterdev-button--selectable" type="button" aria-pressed="false">Pin</button>
<button class="maxterdev-button maxterdev-button--primary" type="button" aria-busy="true">
<svg class="maxterdev-button__icon maxterdev-button__icon--loading" aria-hidden="true" viewBox="0 0 24 24">...</svg>
<span class="maxterdev-button__label">Saving</span>
</button>Fields
Labels use for/id; descriptions and errors use aria-describedby. Set aria-invalid="true" only after validation and include the error id. Native :user-invalid, data-maxterdev-invalid, readonly, disabled, required, placeholder, and focus states are supported without marking an untouched optional empty field invalid.
<div class="maxterdev-field">
<label class="maxterdev-field__label" for="email">
Email <span class="maxterdev-field__required" aria-hidden="true">*</span>
</label>
<p class="maxterdev-field__description" id="email-description">Used for account notices.</p>
<input class="maxterdev-input" id="email" name="email" type="email" placeholder="[email protected]" required aria-describedby="email-description">
</div>
<div class="maxterdev-field" data-maxterdev-invalid>
<label class="maxterdev-field__label" for="message">Message</label>
<textarea class="maxterdev-textarea" id="message" aria-invalid="true" aria-describedby="message-error"></textarea>
<p class="maxterdev-field__error" id="message-error">Enter a message.</p>
</div>
<div class="maxterdev-field">
<label class="maxterdev-field__label" for="country">Country</label>
<select class="maxterdev-select" id="country" name="country">
<option value="">Choose a country</option>
<option value="sk">Slovakia</option>
</select>
</div>Custom Selectboxes
Use the native <select class="maxterdev-select"> shown above when native platform behavior meets the product requirement. Use the custom .maxterdev-selectbox recipe only when an application supplies the complete select-only combobox interaction and ARIA behavior; the design-system CSS provides visuals, not selection, focus, keyboard, or popup behavior. Angular applications should use <maxterdev-select> rather than recreate that behavior.
The 11 public selectors are .maxterdev-selectbox, .maxterdev-selectbox__trigger, .maxterdev-selectbox__value, .maxterdev-selectbox__placeholder, .maxterdev-selectbox__indicator, .maxterdev-selectbox__panel, .maxterdev-selectbox__listbox, .maxterdev-selectbox__option, .maxterdev-selectbox--sm, .maxterdev-selectbox--md, and .maxterdev-selectbox--lg.
<div class="maxterdev-selectbox maxterdev-selectbox--md">
<label class="maxterdev-field__label" id="country-label" for="country-trigger">Country</label>
<button
class="maxterdev-selectbox__trigger"
id="country-trigger"
type="button"
role="combobox"
aria-labelledby="country-label"
aria-haspopup="listbox"
aria-expanded="true"
aria-controls="country-listbox"
aria-activedescendant="country-sk"
>
<span class="maxterdev-selectbox__value">Slovakia</span>
<svg class="maxterdev-selectbox__indicator" aria-hidden="true" viewBox="0 0 24 24">...</svg>
</button>
</div>
<div class="maxterdev-selectbox__panel">
<ul class="maxterdev-selectbox__listbox" id="country-listbox" role="listbox" aria-labelledby="country-label">
<li class="maxterdev-selectbox__option" id="country-cz" role="option" aria-selected="false">Czechia</li>
<li class="maxterdev-selectbox__option" id="country-sk" role="option" aria-selected="true" data-maxterdev-active>Slovakia</li>
<li class="maxterdev-selectbox__option" id="country-disabled" role="option" aria-selected="false" aria-disabled="true">Unavailable</li>
</ul>
</div>CSS consumers own the ARIA relationships and all state changes. Keep DOM focus on the trigger while open; maintain role="combobox", aria-haspopup="listbox", aria-expanded, aria-controls, and aria-activedescendant; give the popup role="listbox"; and give each option role="option", a unique id, aria-selected, and aria-disabled when applicable. Apply aria-invalid="true" to an invalid trigger, aria-disabled="true" or native disabled to an inoperable trigger, and data-maxterdev-active only to the enabled active option. Render .maxterdev-selectbox__placeholder instead of .maxterdev-selectbox__value when no option is selected. Application or adapter code owns popup placement, width sampling, stacking, keyboard navigation, commit and cancel behavior, and cleanup of detached ID references.
Calendars
Calendar CSS is styling only and behavior-free. The default is inline; add .maxterdev-calendar--popup alongside .maxterdev-calendar for popup presentation. The exact 14-selector contract is .maxterdev-calendar, .maxterdev-calendar--popup, .maxterdev-calendar__header, .maxterdev-calendar__nav, .maxterdev-calendar__title, .maxterdev-calendar__icon, .maxterdev-calendar__weekdays, .maxterdev-calendar__weekday, .maxterdev-calendar__grid, .maxterdev-calendar__row, .maxterdev-calendar__cell, .maxterdev-calendar__day, .maxterdev-calendar__view-grid, and .maxterdev-calendar__view-option.
Use native buttons and valid grid, row, and gridcell ownership. Provide meaningful calendar and grid labels plus accessible names for icon-only navigation. Every day button needs a localized, full-date aria-label, including adjacent-month dates; put aria-current="date" on today's button. When a range interaction needs it, include localized range start and range end context. This abbreviated day, month, and year markup shows the required structure:
<section class="maxterdev-calendar" aria-label="Appointment date">
<header class="maxterdev-calendar__header">
<button class="maxterdev-calendar__nav" type="button" aria-label="Previous month">
<svg class="maxterdev-calendar__icon" aria-hidden="true" viewBox="0 0 24 24"></svg>
</button>
<button class="maxterdev-calendar__title" type="button">August 2026</button>
<button class="maxterdev-calendar__nav" type="button" aria-label="Next month">
<svg class="maxterdev-calendar__icon" aria-hidden="true" viewBox="0 0 24 24"></svg>
</button>
</header>
<div class="maxterdev-calendar__grid" role="grid" aria-label="August 2026">
<div class="maxterdev-calendar__weekdays" role="row">
<span class="maxterdev-calendar__weekday" role="columnheader">Sun</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Mon</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Tue</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Wed</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Thu</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Fri</span>
<span class="maxterdev-calendar__weekday" role="columnheader">Sat</span>
</div>
<div class="maxterdev-calendar__row" role="row">
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="true">
<button class="maxterdev-calendar__day" type="button" aria-label="August 11, 2026" aria-current="date">11</button>
</div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false">
<button class="maxterdev-calendar__day" type="button" aria-label="August 12, 2026">12</button>
</div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false" aria-disabled="true">
<button class="maxterdev-calendar__day" type="button" aria-label="August 13, 2026" disabled>13</button>
</div>
</div>
</div>
<div class="maxterdev-calendar__view-grid" role="grid" aria-label="Choose month" hidden>
<div class="maxterdev-calendar__row" role="row">
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="true"><button class="maxterdev-calendar__view-option" type="button">January</button></div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false"><button class="maxterdev-calendar__view-option" type="button">February</button></div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false"><button class="maxterdev-calendar__view-option" type="button">March</button></div>
</div>
</div>
<div class="maxterdev-calendar__view-grid" role="grid" aria-label="Choose year" style="--maxterdev-calendar-view-columns: 3;" hidden>
<div class="maxterdev-calendar__row" role="row">
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false"><button class="maxterdev-calendar__view-option" type="button">2025</button></div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="true"><button class="maxterdev-calendar__view-option" type="button">2026</button></div>
<div class="maxterdev-calendar__cell" role="gridcell" aria-selected="false"><button class="maxterdev-calendar__view-option" type="button">2027</button></div>
</div>
</div>
</section>Day, month, and year views use the same row, gridcell, and nested-button structure. Day rows have seven cells. Month and year rows default to three cells; consumers may set the local --maxterdev-calendar-view-columns hook and must render the matching number of cells per row.
aria-selected and aria-disabled belong on gridcells. Every committed range gridcell, including the start, every interior date, and the end, must set aria-selected="true". A disabled gridcell also contains a native disabled button. If a button uses aria-disabled="true" instead, consumer code must suppress pointer and keyboard activation. Apply data-maxterdev-outside-month for adjacent-month dates and data-maxterdev-active for the consumer-managed keyboard target. Committed ranges use data-maxterdev-range-start, data-maxterdev-in-range, and data-maxterdev-range-end; data-maxterdev-in-range keeps interior day buttons transparent over the connected band, while data-maxterdev-range-start and data-maxterdev-range-end retain circular endpoint fills. Pending ranges use data-maxterdev-range-preview, data-maxterdev-range-preview-start, and data-maxterdev-range-preview-end.
Range geometry uses logical half bands and stops at row edges. Committed states override preview states, single-day endpoints suppress their band, and disabled state wins. This visual state precedence makes contradictory attributes deterministic, but consumers remain responsible for applying semantically correct states.
The fixed supported inline size is 20rem: inline-size, min-inline-size, and max-inline-size all resolve to 20rem and preserve seven 2.5rem day targets (40px at the default 16px root). For containers narrower than 20rem, consumers must provide horizontal overflow or choose another presentation.
Consumers own date calculations, leap years, minimum and maximum constraints, disabled dates, and ranges. Consumers own localization and full labels. Consumers own roving focus or active-descendant behavior, keyboard interaction, view transitions, and live announcements; support Arrow keys, Home, End, Page Up, Page Down, Enter, Space, Escape, and Tab as appropriate. Consumers own popup placement, collision handling, dismissal, focus restoration, and stacking.
Selection Controls
Checkbox, radio, and switch recipes keep a native input in the accessibility tree. Wrap each input and visible label in the matching class. Set checkbox indeterminate state through the native DOM property (input.indeterminate = true). A switch is a checkbox with role="switch"; its checked state remains native and no icon font is required.
<label class="maxterdev-checkbox">
<input class="maxterdev-checkbox__input" type="checkbox" name="updates">
<span>Product updates</span>
</label>
<fieldset>
<legend>Delivery</legend>
<label class="maxterdev-radio">
<input class="maxterdev-radio__input" type="radio" name="delivery" value="standard">
<span>Standard</span>
</label>
<label class="maxterdev-radio">
<input class="maxterdev-radio__input" type="radio" name="delivery" value="express">
<span>Express</span>
</label>
</fieldset>
<label class="maxterdev-switch">
<input class="maxterdev-switch__input" type="checkbox" role="switch" name="notifications">
<span>Notifications</span>
</label>Surfaces And Cards
When using focused CSS entries, import tokens.css, then themes/default.css, then components.css as shown above. Apply .maxterdev-surface for the default presentation; .maxterdev-surface--default is an optional explicit modifier and is valid only alongside the base class. The five modifiers are default, subtle, outlined, raised, and overlay.
<section class="maxterdev-surface maxterdev-surface--outlined">...</section>
<article class="maxterdev-surface maxterdev-surface--raised maxterdev-card">
<header class="maxterdev-card__header">...</header>
<div class="maxterdev-card__body">...</div>
<footer class="maxterdev-card__footer">...</footer>
</article>Cards compose .maxterdev-card with the base surface class. Header, body, and footer are optional; when present they remain in deterministic grid rows 1, 2, and 3, so omitting one region does not move another into its track.
The global component defaults are grouped by purpose: surface visuals use --maxterdev-surface-default-background, --maxterdev-surface-default-foreground, --maxterdev-surface-default-border, --maxterdev-surface-default-radius, and --maxterdev-surface-default-padding; border widths use --maxterdev-surface-border-width-none and --maxterdev-surface-border-width-default; elevations use --maxterdev-surface-elevation-flat, --maxterdev-surface-elevation-raised, and --maxterdev-surface-elevation-overlay; and card padding uses --maxterdev-card-default-padding. The seven clean local surface hooks are --maxterdev-surface-background, --maxterdev-surface-foreground, --maxterdev-surface-border-color, --maxterdev-surface-border-width, --maxterdev-surface-radius, --maxterdev-surface-padding, and --maxterdev-surface-shadow. Card regions consume the local --maxterdev-card-padding hook. Override local hooks directly for product-specific decisions instead of adding utility classes:
<section
class="maxterdev-surface"
style="--maxterdev-surface-radius: var(--maxterdev-radius-4); --maxterdev-surface-padding: var(--maxterdev-space-24)"
>...</section>The base class and explicit default modifier reset all visual hooks to global component defaults. Every card also resets its local card-padding hook. A nested surface therefore restores default surface padding inside a zero-padded card, and a nested card restores default region padding instead of leaking a parent instance override:
<article class="maxterdev-surface maxterdev-card" style="--maxterdev-card-padding: 0">
<div class="maxterdev-card__body">
<section class="maxterdev-surface">This nested surface restores default surface padding.</section>
<article class="maxterdev-surface maxterdev-card">
<div class="maxterdev-card__body">This nested card restores default card padding.</div>
</article>
</div>
</article>In forced-colors mode, author-important direct declarations enforce the default component border width and CanvasText border color and remove shadows, even when inline or unlayered product CSS overrides local hooks. Surface and card recipes set no width, height, positioning, overflow, z-index, role, ARIA/state behavior, or interaction behavior; consumers own layout constraints, semantics, and interactions.
Layouts
Layout recipes use logical properties and prefixed custom-property hooks. Grid and split respond to their nearest ancestor inline-size query container, normally .maxterdev-container; they do not query themselves.
<main class="maxterdev-container" style="--maxterdev-container-max-inline-size: var(--maxterdev-container-64)">
<section class="maxterdev-stack" style="--maxterdev-stack-space: var(--maxterdev-space-24)">...</section>
<nav class="maxterdev-cluster" aria-label="Actions">...</nav>
<section class="maxterdev-grid" data-maxterdev-columns="3">...</section>
<section class="maxterdev-split">...</section>
<section class="maxterdev-center" data-maxterdev-center-text>...</section>
</main>The main hooks are --maxterdev-container-max-inline-size, --maxterdev-container-gutter, --maxterdev-stack-space, --maxterdev-cluster-space, --maxterdev-cluster-justify, --maxterdev-grid-space, --maxterdev-grid-min-inline-size, --maxterdev-split-space, --maxterdev-split-first, --maxterdev-split-second, --maxterdev-split-align, --maxterdev-center-max-inline-size, and --maxterdev-center-gutter.
Motion
Apply a transition base plus one property pattern, or one entrance/progress animation. Override duration, easing, and distance with prefixed hooks. Reduced-motion preferences make patterns immediate and preserve their visible final state.
<div class="maxterdev-transition maxterdev-transition--fade">...</div>
<div class="maxterdev-motion-fade">...</div>
<div class="maxterdev-motion-scale">...</div>
<div class="maxterdev-motion-slide" style="--maxterdev-motion-distance: var(--maxterdev-space-8)">...</div>
<div class="maxterdev-motion-collapse"><div>Expandable content</div></div>
<div class="maxterdev-motion-progress" role="progressbar" aria-label="Loading"></div>Development
Run npm run build -w @maxter-dev/design-system, npm run test -w @maxter-dev/design-system, and npm run validate -w @maxter-dev/design-system from the repository root.
Run npm run pack:design-system from the repository root to create the release candidate in release; package publication remains a separate explicit operation.
License
MIT, copyright 2026 Mato Makuch. The npm artifact includes LICENSE.
