Skip to content

Repository files navigation

MaverickWave

A lightweight, modern CSS framework for building responsive websites with elegance and speed. MaverickWave provides a clean foundation suitable for projects ranging from personal portfolios to corporate websites and e-commerce platforms.

While AI tools helped kickstart the development of some components and provided initial structure, approximately 80% of the codebase required manual refinement and customization. The framework has been meticulously crafted to ensure:

  • Consistent implementation of custom variables throughout the system
  • Proper integration between components and logical SCSS structure
  • Optimized specificity and selector hierarchy
  • Reliable responsive behavior across devices
  • Built-in dark mode support and accessibility considerations

The result is a framework that balances utility with simplicity, offering developers a solid foundation that can be easily customized.

➡️ View the Live Showcase & Documentation

Features

  • Responsive Grid System
  • 30+ UI Components: Buttons, Cards, Panels, Tabs, Accordions, Modals, Tiles, Alerts, Spinners, Progress Bars, Avatars, Tags, Badges, Dropdown, Ratings, Stepper, Skeleton Loader, Empty State, Price, Offer Cards, Divider, and more
  • Form Elements: Input, Select, Textarea, Checkbox, Radio, Toggle, Input Group, with mw-field wrapper pattern for Angular Reactive Forms
  • Utility Classes for spacing, flex, display, typography, text overflow, elevation and aspect ratio
  • A five-step elevation ramp and a motion scale, so every shadow and every transition in the framework comes from one place
  • Mobile as a first-class target: 44px touch targets on a coarse pointer, modals that become bottom sheets, press states on everything, and hover effects that do not latch after a tap
  • Easy Customization via CSS Custom Properties
  • Built-in Light & Dark Mode with optional theme switching
  • SCSS Source Files for advanced customization (Dart Sass, @use/@forward)
  • Minimal JavaScript footprint (single vanilla JS file, no dependencies)

Installation & Usage

1. CDN (jsDelivr)

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>My MaverickWave Project</title>
    <link
      rel="stylesheet"
      href="https://cdn.jsdelivr.net/npm/maverick-wave@4.25.0/maverick-wave.min.css"
    />
  </head>
  <body>
    <!-- Your content here -->
    <script src="https://cdn.jsdelivr.net/npm/maverick-wave@4.25.0/maverick-wave.min.js"></script>
  </body>
</html>

Always pin to a specific version in production for stability.

2. Direct Download

Download maverick-wave.min.css and maverick-wave.min.js from the latest release and include them manually:

<link rel="stylesheet" href="path/to/maverick-wave.min.css" />
<script src="path/to/maverick-wave.min.js"></script>

Customization

CSS Custom Properties

There are thirteen root color tokens. Everything else - hover tones, translucent backgrounds, borders, muted text - is derived from them at runtime with color-mix(), so overriding a root token is enough. Load your stylesheet after maverick-wave.min.css:

:root {
  /* brand */
  --mw-primary-color: #0f766e;
  --mw-secondary-color: #b45309;

  /* status */
  --mw-success-color: #15803d;
  --mw-warning-color: #a16207;
  --mw-danger-color: #b91c1c;
  --mw-info-color: #0e7490;

  /* neutrals and themes */
  --mw-gray-color: #64748b;
  --mw-dark-page-background: #172127;
  --mw-dark-text-color: #e8eef0;
  --mw-light-page-background: #f2f6f7;
  --mw-light-text-color: #172127;
  --mw-form-elements-background: #f5f9fa;

  /* text on every solid colored surface: buttons, table and panel headers.
     The one token that cannot be derived - a light brand color needs a dark
     label, a dark one a light label. Set it opposite your --mw-primary-color. */
  --mw-accent-text-color: #f2fafa;

  /* per color override of that label, for a palette that does not sit on one
     side of the lightness scale. Same token for secondary, success, warning,
     danger and info. */
  --mw-primary-accent-text-color: #0b0f0a;

  --mw-font-family-base: 'Your Font Name', sans-serif;
}

Setting --mw-primary-color alone also retunes --mw-primary-color-hover, --mw-primary-background, --mw-primary-background-hover, --mw-primary-text-color, --mw-border-accent and the whole --mw-header-* set - the bar is the primary darkened toward black, so the chrome follows the brand without a second value. The same holds for --mw-dark-page-background, which drives the dark card, footer and border tones. Each derived token can still be overridden individually if you want to break out of the scale.

The derivation needs color-mix() and relative colour syntax (oklch(from ...)): Chrome 119+, Safari 16.4+, Firefox 128+. Older browsers get no colours at all, not merely worse ones. The same floor is set as browserslist in package.json, which is what Autoprefixer and cssnano read when building dist/.

Elevation and motion

Two scales that are not colours, and both are tokens for the same reason: a framework where every component picks its own shadow and its own timing looks assembled rather than designed.

:root {
  /* Elevation. Each level is two shadows - a tight contact layer that gives
     the box weight, and a wide ambient one that says how high it floats.
     Declared per theme, because the tones they are mixed from differ. */
  --mw-elevation-1: /* resting: inputs, tags, small controls */;
  --mw-elevation-2: /* raised: cards and panels at rest */;
  --mw-elevation-3: /* floating: a card under the pointer */;
  --mw-elevation-4: /* overlay: dropdowns, popovers, drawer */;
  --mw-elevation-5: /* modal */;

  /* Motion */
  --mw-duration-instant: 90ms; /* a press */
  --mw-duration-fast: 150ms; /* a colour swap under the pointer */
  --mw-duration-base: 240ms; /* the default */
  --mw-duration-slow: 400ms; /* something crossing the screen */
  --mw-duration-slower: 700ms; /* a slider, a progress bar */
  --mw-ease-out: cubic-bezier(0.22, 1, 0.36, 1); /* things arriving */
  --mw-ease-in-out: cubic-bezier(0.65, 0, 0.35, 1); /* A to B and back */
  --mw-ease-spring: cubic-bezier(0.34, 1.56, 0.64, 1); /* a pop */

  /* The two ready-made transitions every component uses */
  --mw-transition: /* the explicit paint-only property list, base duration */;
  --mw-transition-fast: /* the same list, fast duration */;

  /* Control sizes - one height per step, shared by input, select, textarea
     and button, so a field and the button beside it line up */
  --mw-control-height-sm: 2rem;
  --mw-control-height: 2.25rem;
  --mw-control-height-lg: 2.5rem;
  --mw-control-font-sm: 0.8rem;
  --mw-control-font: 0.9rem;
  --mw-control-font-lg: 1rem;
  --mw-control-line-height: 1.35;

  /* Focus */
  --mw-focus-ring-width: 2px;
  --mw-focus-ring-offset: 2px;
  --mw-focus-ring-color: var(--mw-primary-text-color);
  --mw-focus-halo-size: 3px; /* the soft ring a form field gets instead */
  --mw-focus-halo-opacity: 28%;
}

--mw-transition deliberately lists its properties rather than saying all: a width that changes at a breakpoint should snap, not crawl. Anything that really does want to animate a size says so itself. --mw-duration-zoom (650ms) is the one for a large surface actually travelling - an image scaling inside a card, a slider track crossing its frame - because the eye reads speed as distance over time, and the 300ms that feels right on a 40px button feels snatched on a 300px photo.

The control scale is what makes a form row line up. Each field used to work its own height out from its own font size, its own line-height and its own padding, and no two of them agreed: an input, a select and a button side by side measured 32.2, 34.4 and 37.2 pixels. They all measure the same now, and the fields share one font scale instead of the select sitting a step below the input next to it.

Under prefers-reduced-motion: reduce the duration tokens all drop to 1ms and a blanket rule catches anything that names its own timing - including whatever you wrote yourself. 1ms rather than 0, so a script waiting on transitionend still gets one.

To retune from SCSS instead, the same values are $duration-*, $ease-*, $control-height* / $control-font*, $focus-ring-* and $shadow-near-* / $shadow-far-* in abstracts/_variables.scss, all !default.

Fill or ink

Every brand and status colour comes in two tokens, and picking the right one is the whole trick:

  • --mw-primary-color is the exact colour, for anything it fills - buttons, badges, bars, progress. The label on top is --mw-primary-accent-text-color.
  • --mw-primary-text-color is the same colour tuned to the active theme, for anything drawn on a theme surface - text, icons, focus rings, accent borders and dividers.

The second one exists because a colour picked to carry a label is by definition too dark or too light to be read on the page it sits on. The ink token is derived by clamping OKLch lightness and keeping hue and chroma:

--mw-dark-primary-text-color: oklch(
  from var(--mw-primary-color) max(l, 0.68) c h
);
--mw-light-primary-text-color: oklch(
  from var(--mw-primary-color) min(l, 0.55) c h
);

That is a bound, not a target, which is what makes it work for any palette. A dark brand colour gets lifted, a very light one gets deepened, and a colour already inside the range passes through untouched - a neon green stays neon on the dark page and only turns into a real green on the light one. The bounds are $ink-lightness-dark and $ink-lightness-light in SCSS. --mw-secondary-text-color and --mw-success/warning/danger/info-text-color are derived the same way.

The label on a fill

--mw-accent-text-color is the text on every solid coloured surface, and one value for the whole palette only holds while all six colours sit on the same side of the lightness scale. A neon green primary next to a deep blue secondary needs a dark label on the one and a light label on the other, so every colour carries its own override:

:root {
  --mw-primary-color: #39ff14;
  /* the shared label stays light for the rest of the palette ... */
  --mw-accent-text-color: #f2f6fc;
  /* ... only the neon primary gets a dark one */
  --mw-primary-accent-text-color: #0b0f0a;
}

--mw-secondary-accent-text-color and --mw-success/warning/danger/info-accent-text-color work the same way, and each defaults to --mw-accent-text-color, so nothing changes until you set one.

Everything that fills a surface with one of the six colours reads the matching token: buttons and mini buttons, the burger button, table and panel headers, card badges and ribbons, segmented and tab items, stepper dots, calendar selection, timeline dates, accordion headers and progress labels. The burger button switches to --mw-secondary-accent-text-color while the drawer is open, because its surface does the same - --mw-header-burgerbutton-color and --mw-header-burgerbutton-open-color override the two states individually.

--mw-text-muted-color is built the other way round: a true gray at a fixed lightness with $muted-tint (12%) of the primary colour mixed in, so the gray belongs to the palette without carrying its saturation. Stepping the theme's text colour back instead would hand its tint straight through - a mint white page would end up with mint green secondary text. Set $muted-tint: 0% for a neutral gray.

Tinted surfaces follow one rule as well: --mw-*-background is 20% of the colour, --mw-*-background-hover 45%. Both stay close enough to the surface underneath that --mw-text-color keeps working on top, which is what makes an alert, badge or tag readable in either theme.

The surface stack

Card, footer and border are derived from the page background by scaling its OKLch lightness and chroma by one factor, keeping the hue:

--mw-dark-card-background: oklch(
  from var(--mw-dark-page-background) calc(l * 0.75) calc(c * 0.75) h
);

Both themes step a card away from their text colour - down into the dark theme, up into the light one - so content always sits on the cleaner of the two surfaces. Chroma rides along with lightness because that is what a hand-picked stack does: a darker surface of the same hue carries less colour, not the same colour at a lower lightness.

The one thing to know when picking --mw-dark-page-background: it needs headroom underneath. On a near-black page the surfaces below it have nowhere to go and cards collapse into the background, leaving only the border to separate them. The default sits at OKLch lightness 0.24 for that reason.

Each factor is a !default SCSS knob, so how far a card sits from the page is one number per theme:

Knob Default Effect
$card-surface-dark 0.85 Card in the dark theme - lower means a darker, more separated card
$card-surface-light 1.05 Card in the light theme - higher means a lighter card
$footer-surface-dark 0.75 Footer and header chrome, dark theme
$footer-surface-light 0.95 Footer and header chrome, light theme

Below 1 steps toward black, above 1 toward white, so read 0.85 as "the card sits at 85% of the page's lightness". Move both card knobs toward 1 for a flat, borderless look; push them apart for cards that read as raised panels. The footer knobs go further out than the card in the dark theme and the other way in the light one, so the band under the page reads as chrome rather than as another card - keep that ordering if you retune the card, or the footer stops looking like a footer. The rule between the surfaces keeps its own fixed factor: it runs against the card, or it disappears into what it separates.

SCSS Source

For full control, clone the repository and integrate src/scss/main.scss into your Sass build (Dart Sass required). MaverickWave uses modern @use/@forward syntax. Pass your overrides through with:

// your styles.scss

@use 'path/to/maverick-wave/src/scss/main' with (
  // Optional: choose theme mode ('switchable' | 'dark' | 'light')
  $mw-theme-mode: 'switchable',

  // Override root colors
  $primary-color: #0f766e,
  $secondary-color: #b45309,
  $dark-background: #172127,
  $light-background: #f2f6f7,

  // Optional: the derivation knobs
  $ink-lightness-dark: 0.68,
  $ink-lightness-light: 0.55,
  $muted-tint: 12%,

  // Optional: how far card and footer sit from the page background
  $card-surface-dark: 0.85,
  $card-surface-light: 1.05,
  $footer-surface-dark: 0.75,
  $footer-surface-light: 0.95
);

Plain variable assignments before @use have no effect - the framework declares its root colors with !default, which only @use ... with feeds.

Importing Only What You Need

Every layer forwards one module per file and no @extend crosses a file boundary, so components can be imported individually. Applications typically have no use for the marketing components (blog-post, gallery, content-slider, techstack-bucket, tiles, coming-soon, ratings, home), and Angular bundle budgets notice the difference.

// styles.scss - configuration first, then pick
@use 'maverick-wave/src/scss/abstracts/variables' with (
  $primary-color: #0f766e
);

@use 'maverick-wave/src/scss/base'; // required: :root tokens, reset, typography

@use 'maverick-wave/src/scss/layout/grid';
@use 'maverick-wave/src/scss/layout/page-header';
@use 'maverick-wave/src/scss/components/buttons';
@use 'maverick-wave/src/scss/components/cards';
@use 'maverick-wave/src/scss/components/modals';
@use 'maverick-wave/src/scss/components/tables';
@use 'maverick-wave/src/scss/components/tags';
@use 'maverick-wave/src/scss/form-elements';
@use 'maverick-wave/src/scss/utilities';

A typical application subset like the one above compiles to roughly 85 kB raw / 14 kB gzipped, against 196 kB / 30 kB for the full build.

base is not optional. It carries the :root custom properties - without it every component renders without colors. If you bring your own reset, use @use 'maverick-wave/src/scss/base/base' for the token block alone. The with (...) configuration has to be the first statement in the file, because every module loads abstracts/variables itself.

Angular Integration

Reference the stylesheet in angular.json (or import the SCSS source as shown above):

"styles": ["src/assets/maverick-wave.min.css", "src/styles.scss"]

Do not add maverick-wave.min.js to a SPA. It wires everything up once on DOMContentLoaded and writes straight into the DOM: components rendered later are never initialized, and the theme toggle mutates the DOM behind Angular's change detection. The behaviors it covers (accordion, tabs, modal close, mobile nav, scroll spy, theme toggle, progress bars, sliders, alerts, galleries) are a few lines each in a component - the framework's state classes are the whole contract. Theme switching, for example, is a single class on <body>:

// theme.service.ts
document.body.classList.toggle('mw-theme-light', isLight);

The mw-field wrapper groups label, control, hint and error. Bind the error state yourself - the framework does not style Angular's ng-invalid / ng-touched classes:

<div
  class="mw-field"
  [class.mw-field-has-error]="email.invalid && email.touched"
>
  <label class="mw-field-label mw-required" for="email">Email</label>
  <input id="email" type="email" class="mw-input" formControlName="email" />
  @if (email.invalid && email.touched) {
  <span class="mw-field-error">
    <i class="fas fa-exclamation-circle"></i> Please enter a valid email
    address.
  </span>
  }
</div>

A complete usage guide for AI coding assistants - every component, token, integration pattern and example - ships with the package as a Claude Code skill in .claude/skills/mw-maverick-wave/.

Development

Prerequisites

  • Node.js v18 or newer
  • npm
  • Git

Setup

git clone https://github.com/m1well/maverick-wave.git
cd maverick-wave
npm install

Development Commands

npm run start     # Start Gulp watcher (recompiles on changes)
npm run showcase  # Serve the built dist/ on http://localhost:8888
npm run build     # Full clean build → dist/
npm run format    # Format all source files with Prettier
npm run verify    # Check dist/ for unused/undefined tokens and unknown classes
npm run release   # Build non-minified release files to project root

npm run verify runs against dist/, so build first. It fails when a token is referenced but never defined, when a token is defined but never used, or when the showcase or the docs use a mw-* class the CSS does not generate.

Project Structure

maverick-wave/
├── dist/                   # Compiled output (CSS, JS, HTML showcase)
├── src/
│   ├── assets/             # Logos, favicons
│   ├── js/                 # JavaScript source (single file)
│   ├── partials/           # HTML partials for the showcase
│   └── scss/
│       ├── abstracts/      # Variables, mixins, functions
│       ├── base/           # Reset, typography
│       ├── components/     # Component styles (one file per component)
│       ├── form-elements/  # Input, select, checkbox, toggle, etc.
│       ├── layout/         # Grid, container, header, footer
│       ├── utilities/      # Spacing, flex, display helpers
│       └── main.scss       # SCSS entry point
├── .claude/
│   ├── commands/mw.md      # Claude Code slash command (Angular quick reference)
│   └── skills/
│       └── maverick-wave/  # Claude Code skill: full usage guide + examples
├── scripts/verify.js       # Class and token consistency check (npm run verify)
├── .prettierrc.json        # Prettier configuration
├── gulpfile.js             # Gulp tasks configuration
├── index.html              # Showcase entry point
└── package.json

Contribution

Contributions are welcome!

  1. Fork the repository.
  2. Create a new branch for your feature or fix.
  3. Make your changes and run npm run format, then npm run build && npm run verify.
  4. Commit using Conventional Commits (e.g., feat: add new component).
  5. Push and open a Pull Request.

License

MIT License

Author

Created by m1well

About

A lightweight, modern CSS framework for building responsive websites with elegance and speed.

Resources

Stars

1 star

Watchers

1 watching

Forks

Releases

Packages

Used by

Contributors

Languages