Forest & Fern Documentation

Documentation / v0.3.0

A small foundation.
Room for your ideas.

One stylesheet. Real HTML. A few deliberate choices.

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