Installation
Build with npm ci && npm run build. Copy
dist/assets/forest-and-fern.css into your app, or download the built bundle
below. No runtime JavaScript is required by the CSS package.
Download the CSS bundle ↓ (v0.3.0)
<link rel="stylesheet" href="/assets/forest-and-fern.css">
<section class="ff-theme" data-ff-theme="light">
<article class="ff-card" data-ff-shape="organic">
<h2>Your workspace</h2>
<button class="ff-button">Explore</button>
</article>
</section>
For React, Vue, Astro, or another framework, import the same CSS and add classes to native elements. React is a consumer, never a dependency:
import './assets/forest-and-fern.css';
export function Example() {
return <button className="ff-button">Explore</button>;
}
Pin v0.3.0 when vendoring this repository’s source or release artifact. The
Pages download follows the default branch and can change. Install registry releases with
npm install @cmwen/min-design-system; GitHub releases publish through the
verified release workflow.
Modular CSS
The build also ships css/ with the original modules. Import
src/index.css through your bundler, or copy the whole directory and link
css/index.css. Keep the modules together so relative imports resolve.
@import './tokens.css';
@import './themes.css';
@import './typography.css';
@import './shapes.css';
@import './waves.css'; /* Optional; omit for the smallest footprint. */
@import './native-controls.css';
@import './motion.css';
Theme boundaries & scoping
Tokens have a sensible light default on :root. Put
data-ff-theme="dark" on the root for a whole app, or on any section for a
nested boundary. Both themes reset every palette token and set
color-scheme for native platform widgets.
.ff-theme applies typography, text color, and background to its container.
All other styling is opt-in through .ff-* classes. There is no global reset
in the library; the playground’s reset is a separate site stylesheet. Use
.ff-native only if you deliberately want element styling inside a scoped
container.
<section class="ff-theme" data-ff-theme="dark">
A dark workspace.
<aside class="ff-theme" data-ff-theme="light">
A light inset, with its own native color-scheme.
</aside>
</section>
Token contract
Override public custom properties on a theme boundary or individual element. Override palette tokens after a theme’s own declaration, or in an inline style. Custom palettes need their own contrast checks.
| Group | Public tokens |
|---|---|
| Surfaces |
--ff-color-bg, --ff-color-surface,
--ff-color-surface-raised, --ff-card-bg
|
| Content |
--ff-color-text, --ff-color-muted,
--ff-color-primary, --ff-color-primary-hover,
--ff-color-on-primary
|
| State |
--ff-color-success, --ff-color-warning,
--ff-color-error, --ff-color-selected,
--ff-color-on-selected, --ff-color-disabled,
--ff-color-on-disabled
|
| Outline |
--ff-color-border, --ff-color-control-border,
--ff-focus-ring, --ff-border-width
|
| Geometry |
--ff-shape-radius, --ff-control-radius,
--ff-wave-mask
|
| Motion |
--ff-duration, --ff-duration-subtle,
--ff-duration-organic, --ff-duration-expressive,
--ff-ease, --ff-hover-lift
|
| Type & spacing |
--ff-font-family, --ff-font-size-sm,
--ff-font-size-body, --ff-font-size-title,
--ff-line-height, --ff-line-height-heading,
--ff-letter-spacing-heading, --ff-font-weight-body,
--ff-font-weight-label, --ff-font-weight-heading,
--ff-space-1/2/3/4/6/8/12
|
Shapes & wave edges
Choose data-ff-shape="none|soft|organic|expressive" or equivalent
.ff-shape-* classes. Presets use different horizontal and vertical corner
radii. Shape and wave are separate choices; no positional selectors define the API.
Waves are off by default. Set data-ff-wave="soft" (strength 3),
data-ff-wave="expressive" (8), or
data-ff-wave-strength="1" through "12". Use one wave attribute
per boundary. Strength 0, data-ff-wave="off", or
.ff-no-wave disables the wave mask. Settings inherit, so opt in on a single
surface or a whole container.
Precomputed SVG silhouettes mask only the two decorative pseudo-elements. The silhouette scales with the panel: values represent relative strength, not an exact displacement in pixels. Very long, narrow panels may exaggerate the wave; prefer pure radii there. Changing radius still changes the clipping geometry, while content and focus rings remain unclipped.
Unsupported masks degrade to asymmetric CSS corners. No SVG filters, turbulence, or continuous animation are used. Masked waves do not retain the normal box shadow. Keep waves limited to deliberate showcase surfaces, and use the 50-surface performance page before enabling them broadly.
Motion & static fallback
Use data-ff-motion="subtle|organic|expressive|none" on a container.
Durations are 160, 280, and 440 ms. Only .ff-interactive surfaces and
buttons lift on hover-capable fine pointers; pressed buttons scale slightly. Native
focus is shown with an independent 3 px ring.
data-ff-reduced-motion="true" provides a local reduced-motion preview. The
system’s prefers-reduced-motion: reduce always removes movement even if the
preview is off. Switches use a brief thumb position transition only on the tiny control.
Static waves never animate.
Native controls
| Class | Use |
|---|---|
.ff-button |
Native button, or anchor for navigation. Add .ff-button-secondary or
.ff-button-quiet.
|
.ff-input |
Text, email, search, textarea. Use native validation;
aria-invalid="true" can expose application validation.
|
.ff-select |
Native select; platform dropdown and arrow retained. |
.ff-radio, .ff-checkbox |
Native accent-color rendering. Radio name groups and checkbox indeterminate semantics remain intact. |
.ff-switch |
A native checkbox with a visual track. Add role="switch" for a
setting that immediately turns on or off.
|
.ff-range |
Native slider with WebKit and Firefox tracks and thumbs. Supply a label and an output when helpful. |
.ff-card |
Decorative surface. Add .ff-interactive only to a semantic
interactive element.
|
.ff-link, .ff-heading |
Opt-in link color/focus and heading typography. |
.ff-native |
Deliberately opt in a container’s buttons, inputs, selects, textareas, and links. |
Always provide labels, fieldset/legend for radio groups, real
disabled attributes, and textual error messages connected with
aria-describedby. Use 44 px label rows for small checkboxes/radios. Avoid
click handlers on non-semantic cards.
Common component recipes
Explore app integration recipes ↗: compact density, semantic statuses, icon and destructive actions, selectable
contexts, field feedback, and responsive navigation. Set
data-ff-density="compact" on a container,
data-ff-size="sm|lg" on controls, and
data-ff-tone="neutral|info|success|warning|danger" on tags and alerts.
Explore the full component collection ↗. Import components.css or the full bundle. Application data and event
handlers belong to the consumer; no Web Components are needed for these native patterns.
| Pattern | API and semantics |
|---|---|
| Tables |
.ff-table-region, .ff-table,
.ff-table-sort. Use caption, scoped headers, a labeled focusable
scroll region, data-ff-numeric on numeric cells, and optional
data-ff-density="compact". Sorting updates aria-sort on
the active column, with real buttons as triggers.
|
| Charts |
.ff-chart, .ff-chart-grid/line/area/bar/point. Style
your SVG marks; include a title/description and equivalent data table. Layout
scales with a viewBox. For tiny screens or magnification, the data table remains
the readable alternative.
|
| Modals |
.ff-dialog on a native dialog, with
.ff-overlay-header and .ff-overlay-actions. Call
showModal() and close with close() or a
method="dialog" form. Give it a heading, description and intentional
initial focus. Do not set display:flex on the dialog itself or override its closed
state.
|
| Popups |
.ff-popover, .ff-popover-actions, native
popover="auto" and popovertarget. The demo positions
using --ff-popover-left and --ff-popover-top; content
stays inside the viewport. Ordinary action buttons use normal Tab navigation, not
menu roles.
|
Accordions use details.ff-accordion, a native summary, and
.ff-accordion-content. Tabs use .ff-tab-list,
.ff-tab, and .ff-tab-panel; the optional
initTabs() helper adds keyboard navigation, selection and cleanup. Without
enhancement, links point to visible panels.
Cards use .ff-card. Descriptive labels use .ff-tag or
.ff-pill; toggle pills are real buttons with aria-pressed.
Compose responsive layouts with .ff-stack, .ff-cluster,
.ff-grid, .ff-sidebar-layout, and .ff-container.
Tune --ff-gap, --ff-grid-min, and
--ff-container-max locally.
Ambient branding is opt-in with .ff-brand-mark[data-ff-ambient="true"] and
your own inline SVG. Provide a visible pause control using
data-ff-paused="true", pause while the document is hidden, and retain
reduced-motion support. The temporary cmwen.dev mark is a demo asset, excluded from the
package.
<dialog class="ff-dialog" id="confirm" aria-labelledby="confirm-title">
<form method="dialog">
<h2 id="confirm-title">Ready to grow?</h2>
<button class="ff-button" value="confirm">Confirm</button>
</form>
</dialog>
<!-- Your app calls document.querySelector('#confirm').showModal(). -->
Overlay content uses normal theme inheritance even in the browser’s top layer. Dialogs
and popovers keep native semantics. Add .ff-wave-surface for a decorative
wave edge; only pseudo-elements are masked, so content and focus rings remain clear.
Short opening animations use the motion tokens and respect reduced motion. Tables scroll
locally at mobile widths; overlays have bounded width, height, and internal scrolling.
Unsupported popover rendering is an inline panel. If dialog support is absent, present
an inline form rather than building an incomplete modal substitute.
Browser behavior references: native dialog and Popover API.
Nunito & font hosting
The CSS declares Nunito with a system-ui fallback and makes no network requests. To self-host, obtain Nunito WOFF2 files from its official open-source distribution, include the font’s SIL Open Font License 1.1, then add:
@font-face {
font-family: Nunito;
src: url('./nunito-variable.woff2') format('woff2');
font-weight: 400 900;
font-display: swap;
}
Optional Google Fonts stylesheet: Nunito 400–900. Third-party font hosting is a deliberate consumer choice. The library works offline.
Accessibility & browser notes
Core token pairs have automated WCAG AA contrast checks. Decorative card borders are intentionally subtle; native control borders and focus rings use separate stronger tokens. Disabled controls remain readable, although WCAG exempts inactive controls from the contrast requirement.
Playwright covers Chromium, Firefox, WebKit, and emulated mobile Chromium/WebKit. WebKit emulation is useful coverage, but does not replace testing real Safari, iOS, and Android devices. Forced colors restores native switch rendering and system borders. No cutting-edge shape() or customizable-select API is required.
Use npm run check for lint, token tests, and build;
npm run test:browser for keyboard, accessibility, layout, theme, and visual
checks. Chromium screenshot baselines use the bundled font and fixed viewports. Update
them intentionally with npm run test:visual:update. Performance smoke
results are relative measurements, not Core Web Vitals promises.
GitHub Pages
One Vite project builds the playground, docs, examples, stable CSS bundle, and modular
source. The default base is /forest-and-fern/. Set
FF_BASE=/your-repository/ for another repo, or / for a custom
domain. The Actions workflow derives the base from the actual repository name.
Enable Pages with GitHub Actions as its source. The workflow checks, builds, tests, then deploys the default branch with repository-provided permissions. No external hosting secrets are required.
Install & explore
Run npm pack here, copy the versioned tarball into your app, then run
npm install ./vendor/cmwen-min-design-system-0.3.0.tgz. Import
forest-and-fern for the CSS; import initTabs from
@cmwen/min-design-system/tabs only when you want the optional tab behavior.
Use the release guide to publish this
repository on GitHub Pages and the CSS package on npm. After publication, install your
configured package name with npm install.
Complete installation guide · Open Storybook · Demo asset provenance
Start the workshop locally with npm run storybook on port 6006. Build it
with npm run build-storybook after the site build. Twenty isolated stories
include controls, documentation, accessibility tooling, and global theme/wave/motion
choices. npm run test:package verifies a separate consumer app;
npm run test:storybook verifies the static workshop. See the release guide
for registry publishing.
License & contributions
Project code is MIT licensed. Nunito has its own SIL OFL 1.1 license. See the repository’s LICENSE and CONTRIBUTING.md. Behavioral Web Components remain a future phase, reserved for a clear need.
To contribute, read the PRD and reference, run npm run check and
npm run test:browser, and review visual baselines before proposing a
change. Regenerate static waves with node scripts/generate-waves.mjs and
benchmark geometry or motion changes.
Contributor guide · Architecture decisions · Validation record