mp-design-system
v2.1.62
Published
Malvern Panalytical's design system for the commercial marketing websites — the `*.malvernpanalytical.com` subdomains. Internally codenamed **"Franklin"**.
Readme
mp-design-system
Malvern Panalytical's design system for the commercial marketing websites — the
*.malvernpanalytical.com subdomains. Internally codenamed "Franklin".
The repository serves two purposes from a single codebase:
- A documentation / brand-book website built with Astro. It presents
the brand guidelines, foundations (colour, space, grid, typography), the component library,
patterns and prototype pages. It is deployed to Cloudflare Pages (project
mp-brandbook). - A published npm package (
mp-design-system) of compiled CSS and bundled JavaScript that the production websites consume via<link>and<script>tags.
What it is (and what it isn't)
- It ships class-based CSS plus vanilla-JavaScript behaviours — not React or Vue components. Consumers write plain HTML with the documented BEM classes; the CSS styles it and the JS auto-initialises interactive behaviours (accordions, carousels, tabs, modals, off-canvas, etc.).
- The
.astrocomponents in this repo exist only to render the documentation site and act as the canonical markup reference for each component. They are not published and are not imported by consumers. - The styling is a bespoke SCSS system. It does not use AutomaticCSS (ACSS) or Tailwind. It
borrows familiar concepts — fluid type/space and CSS custom properties — but implements its own
tokens (
--color-{palette}-{step},--space-*,--step-*) from scratch.
Tech stack
| Concern | Tooling |
| ------------------------ | ---------------------------------------------------- |
| Site framework | Astro 6 (@astrojs/mdx, @astrojs/vue) |
| Styles | Dart Sass (sass), compiled by a custom Node script |
| Scripts | esbuild (bundling + minification) |
| Carousels | @splidejs/splide |
| Code highlighting (docs) | prismjs + Astro/Shiki |
| Formatting | Prettier (+ prettier-plugin-astro) |
| Deploy | Wrangler → Cloudflare Pages |
There is no test framework and no ESLint configured.
Consuming the design system
Every subdomain has its own build. Each build is one main stylesheet, one legacy stylesheet, and one script bundle:
| Subdomain | Main CSS | Legacy CSS | Script |
| ---------------------------------------------------- | ---------------- | ----------------------- | --------------- |
| www.malvernpanalytical.com | mp-www.css | mp-www-legacy.css | mp-www.js |
| www2.malvernpanalytical.com (Pardot landing pages) | mp-www2.css | mp-www2-legacy.css | mp-www2.js |
| academy.malvernpanalytical.com | mp-academy.css | mp-academy-legacy.css | mp-academy.js |
| brand.malvernpanalytical.com | mp-brand.css | mp-brand-legacy.css | mp-brand.js |
| events.malvernpanalytical.com | mp-events.css | mp-events-legacy.css | mp-events.js |
| mystore.malvernpanalytical.com | mp-mystore.css | mp-mystore-legacy.css | mp-mystore.js |
| support.malvernpanalytical.com | mp-support.css | mp-support-legacy.css | mp-support.js |
Via CDN (recommended)
unpkg serves the built assets straight from the published npm package. Include
the legacy stylesheet, the main stylesheet, and the script matching the subdomain, replacing
<version> with the version you want to pin (the current version is shown in the top-right of the
documentation site). Example for www2:
<link
rel="stylesheet"
href="https://unpkg.com/mp-design-system@<version>/dist/build/styles/mp-www2-legacy.css"
/>
<link
rel="stylesheet"
href="https://unpkg.com/mp-design-system@<version>/dist/build/styles/mp-www2.css"
/>
<!-- Place near the closing </body> tag — it will not run correctly in <head>. -->
<script src="https://unpkg.com/mp-design-system@<version>/dist/build/scripts/mp-www2.js"></script>Self-hosted via npm
If you want to bundle or self-host the assets, install the package:
npm install mp-design-systemThe same compiled files are then available under
node_modules/mp-design-system/dist/build/styles/ and .../dist/build/scripts/ to copy or bundle.
Note: only the compiled output is published — the package
filesfield shipsdist/build/**anddist/assets/**. The SCSS/JS source undersrc/is not included in the package, so you cannot@importthe raw SCSS fromnode_modules. Consume the compileddist/buildassets.
Usage in HTML
Write markup with the documented BEM classes, scoped by a leading .mp class so the styles stay
namespaced when injected into an external (e.g. WordPress) site:
<a class="mp c-button c-button--red" href="/contact">Contact sales</a>Class prefixes follow ITCSS conventions: c- for components, o- for layout objects, u- for
utilities, with BEM __element and --modifier suffixes.
Architecture
Two outputs from one repo
- The documentation site —
astro buildrenders the static site intodist/. - The publishable assets —
build-assets-to-publish.mjscompiles the SCSS and bundles the JS intodist/build/. This is what npm publishes.
Repository layout
| Path | Purpose |
| ----------------------------------- | ----------------------------------------------------------------------------------------------------- |
| src/components/ | 60+ UI components, each in its own folder (see the component contract below), plus registry.ts |
| src/patterns/ | Higher-level compositions (Form, GridOfCards, Sections, …) + registry.ts |
| src/prototype/ | Full example pages (home, product, blog, contact, support, …) |
| src/layouts/ | Astro page layouts (BaseLayout, DocsLayout, ComponentDocsLayout, …) |
| src/pages/ | File-based routes for the docs site (brand/, components/, patterns/, prototype/, guide pages) |
| src/styles/ | All SCSS — the per-subdomain entry files and the refresh/ + legacy/ trees |
| src/assets/scripts/ | Client-side JavaScript — per-subdomain entry points and feature modules |
| build-assets-to-publish.mjs | Compiles SCSS → compressed CSS and bundles JS → minified, into dist/build/ |
| dev-assets-to-compile.mjs | Dev-time JS bundling (unminified) into public/assets/scripts/ for the docs site |
| astro.config.mjs, tsconfig.json | Astro config (Vue + MDX, @ → src) and TS options |
| bitbucket-pipelines.yml | CI: build + deploy to Cloudflare on pushes to master |
Styles — ITCSS layers
The website ecosystem is being redeveloped in phases — an ongoing "refresh". As each part is
rebuilt it moves into the refresh system, so src/styles/ contains two coexisting systems:
refresh/— parts of the ecosystem that have been redeveloped as part of the phased refresh. A 7-layer ITCSS structure:1-settings(tokens) →2-tools(functions/mixins) →3-generic(reset, base, token emitters) →4-elements→5-objects→6-components→7-utilities.legacy/— the parts not yet redeveloped, still served in their pre-refresh form. Each subdomain ships a-legacystylesheet alongside its main one.
Per-subdomain entry files (src/styles/mp-www.scss, mp-academy.scss, …) @use the layers in
order and set a site identity variable.
Design tokens
Tokens are a two-layer pipeline:
- Authored as SCSS maps/variables in
refresh/1-settings/— e.g. the seven brand colours incolors.scss($f-mp-petrol: #005461,$f-mp-green: #13aa13, …), each expanded into a stepped scale (step--2…step-3); the fluid$spacesscale inspace.scss; and the fluid type$stepsinfluid-type.scss. - Emitted as CSS custom properties in
refresh/3-generic/—color.scssgenerates--color-{palette}-{step}(e.g.--color-green-step-0),space.scssgenerates--space-*and fluid--fc-*pairs, and the type scale generates--step-*.
Components consume only the CSS custom properties, never the raw Sass. Type and space are fluid (Utopia-style), scaling smoothly between 320px and 1332px viewports.
Component contract
Each component lives in src/components/<Name>/ with a consistent set of files:
Button/
Button.astro # markup — builds BEM class strings from props
button.types.ts # TypeScript prop types
button.variants.ts # catalogue of variants for the docs gallery
index.ts # barrel exportEvery component is registered in src/components/registry.ts, which
maps a kebab-case id to { name, category, Component, variants, … } and drives the dynamic docs
routes (src/pages/components/[component]/…). The src/components/-NewComponent/ folder is a
scaffold/template for adding a new component.
Client-side JavaScript
src/assets/scripts/ holds plain vanilla-JS modules (no framework). Each per-subdomain entry point
(mp-www2.js, mp-academy.js, …) imports feature modules from imports/ and imports/refresh/
(accordion, carousel, tabs, modal, off-canvas, scroll-spy, hero video, hotspot image, …) and runs
an initializeApp() that auto-runs on load.
Prerequisites
- Node.js 22 — matches the CI build image (
bitbucket-pipelines.yml). The version is not pinned in-repo (there is no.nvmrcorenginesfield, andsandbox.config.jsonstill says Node 14, which is stale) — use Node 22. - npm (used in CI as
npm ci) and git. - Access to the Bitbucket repository (
bitbucket.org/mp-website/mp-design-system, workspacemp-website). - To cut releases you additionally need npm publish rights for the
mp-design-systempackage and Cloudflare Pages access for themp-brandbookproject.
Local development
git clone https://bitbucket.org/mp-website/mp-design-system.git
cd mp-design-system
npm install
npm run devOpen http://localhost:4321 to browse the documentation site. Each component is viewable at
/components/<id> (e.g. /components/button) — those routes are generated from the component
registry (see below), so a new component appears in the docs simply by being registered.
npm run dev first bundles the client JS into public/assets/scripts/ (unminified) and then starts
astro dev. Use npm start if you only need the Astro dev server without re-bundling the JS.
Contributing: adding or changing a component
Remember there are two tracks (see Architecture): the .astro files + registry
power the docs/reference, while the compiled SCSS and JS are what actually ship to
subdomains. Adding a component usually touches both.
Per component (do once)
- Copy the scaffold. Duplicate
src/components/-NewComponent/tosrc/components/<PascalName>/(e.g.Button/). Rename the four files (<Name>.astro,<name>.types.ts,<name>.variants.ts,index.ts) and find/replace theNewComponent/newComponent/NewComponentPropsidentifiers. - Types — in
<name>.types.ts, define thePropsinterface and any variant/size unions. - Markup — in
<Name>.astro, destructureAstro.props, build thec-<name>BEM class strings (the codebase uses the[...].filter(Boolean).join(" ")idiom), and prefix the root class withmpso it stays namespaced when injected into external sites. - Variants — in
<name>.variants.ts, populate the<name>Variantsarray. Every entry needs a truthy kebab-caseid— empty-id entries generate no docs route. - Barrel — confirm
index.tsexports the component, itsPropstype, and the variants array. - Register — in
src/components/registry.tsadd an import from the barrel and a<kebab-id>entry with the fieldsid,name,category,Component,variants,example,defaultProps, and optionalfigma/bitbucketlinks. This alone makes the component appear in the docs routes. - Styles — create
src/styles/refresh/6-components/<name>.scss, starting with@use "../_tools" as *;(which forwards all tokens, functions and mixins). Use tokens and mixins, never hardcoded values (see Coding conventions). - Interactive behaviour (if any) — create a module in
src/assets/scripts/imports/refresh/as a default-exported factory that queries its selector and returns early if the element is absent (safe to call on every page).
⚠️ Per subdomain (the easy-to-miss step — repeat for each subdomain that should ship it)
There is no glob/auto-include: src/styles/refresh/6-components/index.scss is empty and does
nothing. Each subdomain entry file lists its components individually, and the subsets deliberately
differ (mp-www.scss and mp-www2.scss do not carry the same components).
- CSS — add
@use "./refresh/6-components/<name>.scss" as *;to each relevant refresh entry file:mp-www.scss,mp-www2.scss,mp-academy.scss,mp-brand.scss,mp-events.scss,mp-mystore.scss,mp-support.scss. - JS — for interactive components, add the
importand call<Name>()insideinitializeApp()in each relevantsrc/assets/scripts/mp-*.jsentry.
Forgetting a subdomain means the component renders unstyled (or inert) on that subdomain with no error — this is the most common mistake, so double-check every entry file you intend to ship to.
Then run npm run dev to preview, and npm run build to produce the publishable assets.
Coding conventions
- CSS naming — ITCSS + BEM:
c-for components,o-for layout objects,u-for utilities, with BEM__elementand--modifiersuffixes. Prefix a component's root class with.mpso styles stay namespaced inside external (e.g. WordPress) pages. - Tokens, not literals: colours are always
var(--color-{palette}-{step})(e.g.--color-green-step-0) — never raw Sass colour values. Use the_toolsmixins/functions for spacing and type:space()/padding()/margin()/gap(),flow($size),step($n),ls("base"), plus helpers likeclickable-parentandfocus-parent. - Spelling follows the system: refresh code is American (
color); legacy code is British (colour). Match the system you are editing. - File/id naming: kebab-case ids in registries, PascalCase component folders/names,
camelCase+Variantssuffix for the variants export. - Formatting: Prettier (with
prettier-plugin-astro). Runnpx prettier --write .before committing. There is no ESLint and no test suite.
Git workflow
master is the deploy branch — pushing to it automatically rebuilds and redeploys the docs site
(see Releasing).
- Today: the project has a single maintainer who commits directly to
master. - Recommended going forward: work on a feature branch and merge (or open a Bitbucket pull
request) into
master, somasterstays deployable and changes are reviewable.
Scripts
| Command | What it does |
| --------------------------- | ---------------------------------------------------------------------------------------------------------------------------------- |
| npm run dev | Compile JS to public/assets/scripts (unminified) then start astro dev |
| npm start | Start astro dev only |
| npm run build | astro build (docs site → dist/) then build-assets-to-publish.mjs (16 CSS compressed + 8 JS minified bundles → dist/build/) |
| npm run preview | Preview the built site |
| npm run deploy:cloudflare | Deploy dist/ to Cloudflare Pages (project mp-brandbook) via Wrangler |
Releasing
There are two independent release acts — one does not imply the other.
1. Deploy the docs site (automatic). Pushing/merging to master triggers Bitbucket Pipelines
(bitbucket-pipelines.yml), which runs npm ci → npm run build → npm run deploy:cloudflare and
publishes the site to Cloudflare Pages (project mp-brandbook).
2. Publish the npm package that production sites consume (manual). CI does not publish to npm — you must do it locally:
- Be on an up-to-date
masterwith no uncommitted changes. - Bump
versioninpackage.json(semver: first number = breaking, middle = feature, last = fix). The current version is2.1.60. - Commit and push the version bump.
- Run
npm publish. TheprepublishOnlyscript wipesdistand rebuilds first, so only freshly compileddist/build/**assets are published. - Consumers pick up the change by bumping the pinned version in their unpkg
<link>/<script>tags.
Important: deploying the docs site is not the same as publishing the package. Merging to
masterupdates the brand-book site but does not ship anything to the production websites — for that you must bump the version andnpm publish. Because both steps are manual, the docs site and the published package can drift; keep them in sync deliberately.
Gotchas & current caveats
- Per-subdomain wiring is manual. A new component must be
@used into every relevantmp-*.scssand, if interactive, imported + called in every relevantmp-*.js. There is no auto-include;refresh/6-components/index.scssis empty. Missing one subdomain fails silently. - Docs deploy ≠ npm publish, and version bumps are manual (see Releasing).
- Some in-app docs are outdated:
src/pages/components/creating-a-component.astrodescribes an obsolete Nunjucks/.njkworkflow — ignore it; follow this README's component contract instead.src/pages/components/installation.astrodocuments consuming the system via an SCSS-source@import '~mp'; this does not work becausesrc/is not published (onlydist/build). Use the compiled assets (see Consuming the design system).
- Registry links are mostly empty/stale. In
registry.tsthefigmafields are unused and the two populatedbitbucketlinks point at a defunctsrc/_includes/components/…path. - No tests, no ESLint, no pinned Node version — quality relies on manual review and Prettier.
Ownership & support
The system is maintained by the Design System Team at Malvern Panalytical. For questions, feedback, or change requests, raise a ticket with the Web Development team via the Service Desk portal (the same link is in the site footer).
- Published package: https://www.npmjs.com/package/mp-design-system
- Source repository: https://bitbucket.org/mp-website/mp-design-system
