WP Manifestindependent plugin directory
manifest / content / carousel-and-marquee-kit

Content Motion Kit self-updates

Lightweight, dependency-free carousels, auto-scrolling marquees and scroll-synchronised lists via shortcode. Query any post type, build cards from a block repeater, and style everything with CSS custom properties. Framework-free, with no bundled slider library.

by Lee Hernandez · github.com/noleemits/carousel-and-marquee-kit

0stars
0forks

Install

No release zip yet. The repository archive installs, but the folder name will carry the branch suffix and updates will not flow:

wp plugin install https://github.com/noleemits/carousel-and-marquee-kit/archive/refs/heads/main.zip

Ships its own WordPress updater (built-in updater), so new versions show up under Dashboard → Updates.

(Formerly Carousel & Marquee Kit — the display name changed in 1.4.0; every `cmk_` identifier, shortcode tag and CSS prefix is unchanged.)*

Portable WordPress shortcodes for dependency-free marquees, native scroll-snap carousels and scroll-synchronised lists. The front end and admin editor use vanilla JavaScript. Requires WordPress 6.0+ and PHP 7.4+.

Quick start

Create reusable instances under Carousels, then copy their shortcodes:

[cmk_marquee id="123"]
[cmk_marquee tracks="2" axis="vertical" speed="60s"]
[cmk_carousel id="124"]
[cmk_carousel post_type="post" per_view="4"]
  <a class="your-theme-button" href="/articles/">Read all</a>
[/cmk_carousel]
[cmk_scrollsync id="125"]

Carousels can autoplay. It is off by default; switch it on with an interval and it advances one card — or one full viewport with autoplay_step="page" — then rewinds to the first card at the end:

[cmk_carousel id="124" autoplay="1" autoplay_interval_ms="5000"]

It pauses on hover and focus, and stops for the rest of the visit once a visitor touches the controls; both are switchable. It never runs under prefers-reduced-motion: reduce, in a background tab, while scrolled out of view, or on a single-card carousel.

The native two-level editor supports items and nested content blocks with add, duplicate, delete, drag-sort, device visibility, typography, images, icons, and links. Each instance is stored atomically as normalized JSON in _cmk_config. Import and export use that same portable document.

Styling

Configurable public CSS is in the cmk cascade layer. A small unlayered, single-class structural reset prevents generic theme element rules from adding root borders, blockquote side margins, link decoration, button chrome, or broken card-image sizing. Keep that reset outside @layer cmk; later unlayered site CSS of equal specificity still wins. Stable roots are .cmk-marquee and .cmk-carousel; descendants use the documented __part naming.

In instance-scoped Custom CSS, & means the instance root. Use & { --cmk-radius: 24px; } or & .cmk-carousel__card { border-radius: 16px; }. A bare .cmk-carousel or .cmk-marquee is treated as a descendant and cannot match the root, so the editor warns about that pattern.

Layered backgrounds are ordered topmost first and can combine color, linear, radial, conic, and media-library image layers. A mesh is an uploaded image layer, never a raw URL. The same composite setting is available for the marquee panel/cards and carousel section/cards/overlay caption. Existing bg_color and card_bg_color remain bottom-color fallbacks.

Carousel title, subtitle, card title, card excerpt, and card more row each expose font family, size, weight, line height, letter spacing, color, opacity, transform, alignment, and maximum width. Empty values inherit. Tag controls change document semantics only; use typography controls for appearance. Overlay captions default to white body text on a dark translucent background, with individual part colors available as overrides.

Sizing card type against the card, not the window

Every card is a container query container (container: cmk-card / inline-size), so any size setting may be written in cqw — 1% of the card's own inline size. Reach for it whenever the same card appears at different widths in different layouts: a four-up card is 328px wide in a 1360px row but 286px in a 1263px grid, and a vw-only size measures the window instead, overshoots the narrower card, and clips titles that fit everywhere else. Because cqw tracks the card, characters-per-line stays constant across contexts.

clamp(), min(), max() and calc() are all accepted in a size setting, so the useful shape is a clamp whose middle term is bounded both ways:

clamp(24.833px, min(calc(22.31px + 0.673vw), 9.756cqw), 32px)

The vw term carries the deliberate type scale (a phone gets smaller type than a desktop even when the card is the same width — that is a design decision, not a width effect, and no card-relative term can express it); the cqw term caps the result so it always fits its card. Prefer this over a @media breakpoint that restates a font size: a step at 601px is visible, and it is usually redundant because a clamp already passes through the intended value at the narrow end.

⚠️ calc() multiplies a length by a bare number only. calc(18px * var(--len)) is length × length, which drops the whole declaration silently and reads exactly like a rule that never loaded.

For moving marquee cards, prefer no backdrop blur or a small value. Values above about 20px are costly and can make text look soft while it moves; the editor warns when that threshold is exceeded.

Overriding safely

Unlayered custom CSS wins over plugin rules, including state rules. Avoid fixing opacity, transform, display, or filter on carousel state elements unless you also repeat the hover, focus, touch, and active-state selectors. Prefer the documented custom properties for color, size, backgrounds, blur, radius, spacing, transitions, and zoom. The visible progress dot is the inner span painted with currentColor; set color on .cmk-carousel__dot, not its background.

Automation API

Enable the API under Carousels → Settings. Authenticate with a WordPress Application Password or a named CMK bearer token. Bearer tokens are stored as HMAC hashes, shown once, optionally expire, can be IP-restricted, and are limited to 60 writes per minute. The authenticated user needs cmk_manage_instances or edit_pages.

Base URL: https://example.test/wp-json/carousel-marquee-kit/v1

# Discovery
curl -u 'user:application-password' "$BASE/schema"
curl -u 'user:application-password' "$BASE/capabilities"

# Validate and list
curl -u 'user:application-password' -H 'Content-Type: application/json' -d @instance.json "$BASE/validate"
curl -u 'user:application-password' "$BASE/instances?component=carousel&page=1&per_page=20"

# Create, read, update, delete
curl -u 'user:application-password' -H 'Content-Type: application/json' -H 'Idempotency-Key: build-001' -d @instance.json "$BASE/instances"
curl -u 'user:application-password' "$BASE/instances/123"
curl -u 'user:application-password' -X PATCH -H 'Content-Type: application/json' -d '{"settings":{"speed":"45s"}}' "$BASE/instances/123"
curl -u 'user:application-password' -X PUT -H 'Content-Type: application/json' -d @instance.json "$BASE/instances/123"
curl -u 'user:application-password' -X DELETE "$BASE/instances/123"

# Instance operations
curl -u 'user:application-password' -X POST "$BASE/instances/123/duplicate"
curl -u 'user:application-password' "$BASE/instances/123/render"
curl -u 'user:application-password' "$BASE/instances/123/revisions"
curl -u 'user:application-password' -X POST "$BASE/instances/123/revisions/456/restore"

# Presets
curl -u 'user:application-password' "$BASE/presets"
curl -u 'user:application-password' -X POST "$BASE/presets/example-glass-testimonials/apply"

For bearer authentication, replace -u ... with -H 'Authorization: Bearer YOUR_TOKEN'.

The portable request body is:

{
  "title": "Two-track testimonials",
  "component": "marquee",
  "settings": {
    "axis": "vertical",
    "tracks": 2,
    "items_per_track": "all",
    "alternate_direction": true
  },
  "items": [
    {
      "id": "testimonial-a",
      "blocks": [
        {
          "id": "quote-a",
          "type": "quote",
          "tag": "blockquote",
          "preset": "body",
          "text": "First quote"
        }
      ]
    }
  ],
  "custom_css": ""
}

Strict validation reports all unknown fields, suggests close setting names, and lists valid enum values. GET /schema returns {schema_version, settings, items, background}. Settings describe their type, default, component, group, shortcode name, CSS variable, responsive behavior, description, and enum/range; items describes every item and block field, tag settings include their accepted enum, and background documents the canonical ordered composite-background object. Instance list records include top-level component as well as config.component.

Import, export, and presets

  • Paste portable JSON under Carousels → Settings → Import to create a validated draft.
  • Use Export JSON from an instance row or edit screen.
  • Bundled presets live in presets/ and use the same schema as exported instances.
  • Updates create WordPress revisions; revisions are listed and restored through the API.

Hooks

  • cmk_should_enqueue_marquee filters marquee pre-detection.
  • cmk_should_enqueue_carousel filters carousel pre-detection.
  • cmk_cache_ttl filters the carousel-query cache lifetime.

Architecture

  • includes/ — schema, sanitization, renderers, REST, assets, presets, and cache.
  • admin/ — native editor, settings, docs, diagnostics, import, and export.
  • public/ — budgeted component CSS and vanilla JavaScript.
  • presets/ — portable bundled preset JSON.
  • languages/ — translation template.

There is no front-end build step or runtime package dependency. The in-plugin Docs page generates its attribute tables directly from the canonical schema.

Updating to 1.2.0

Nothing is required. Schema-1 and schema-2 instances migrate in memory when read, new typography settings are optional, and the confirmed v1.1.0 backgrounds, overlays, fallback images, tags, page dots, and validation behavior remain supported.

Scroll-synchronised lists

A tall column of item titles beside a pinned media pane. The item crossing the trigger line becomes active: it lights up and the pane crossfades to that item's image.

[cmk_scrollsync id="125"]
[cmk_scrollsync id="125" ss_source="manual" ss_manual_ids="12,7,31" ss_title_trim_words="2"]
[cmk_scrollsync id="125" ss_image_source="meta" ss_image_meta_key="product_shot" ss_sticky_offset="120px"]

position: sticky plus one IntersectionObserver. No scroll hijacking, no pinning library, no smooth-scroll library. Under prefers-reduced-motion: reduce the active item still changes — which item is active is information, not decoration — but nothing animates and pointer parallax is disabled.

Three things here have no client-side equivalent, which is why the module renders server-side:

  • A word-level title trim. wp_trim_words counts words. CSS clamps lines or clips pixels, so it can only chop mid-token: MIC (Methionine/Inositol/C… instead of MIC (Methionine/Inositol/Choline).
  • Arbitrary meta sources. The image can come from any meta key, including an ACF image field storing an attachment ID, a URL, or an array.
  • A correct no-JS base state. The first item and first image are rendered active by the server, so the section looks finished before any script runs.

Set a fallback attachment: an item that resolves no image uses it, and if there is no fallback either the pane keeps the previous image rather than blanking. The manual source renders your ID list in exactly the order given; an empty list renders nothing rather than the entire post type.

Pacing: why a list can appear to skip an item

An item goes active when it crosses the trigger line, so the scroll distance spent on each item is its pitch — its height plus the row gap. One notch of a mouse wheel is ~100px. A 40px title with a 6px gap is a 58px pitch, so a single notch crosses nearly two items and the one in the middle never gets a turn. Measured on the reference implementation this module was modelled on: 96px type at a 107px pitch, roughly one notch per item, which is why it never appears to skip.

ss_item_min_height sets a floor on the item box, with the title centred inside it, so the pitch is tunable without touching the type. It makes the list taller — which is a layout decision, and the reason it is a setting rather than a default.

Motion

Every motion choice is a setting, because this primitive serves several sections and a value hardcoded for the first one has to be undone for the rest.

Setting Does
ss_media_enter fade, fade-scale, fade-rotate, fade-scale-rotate, reference
ss_enter_scale / ss_enter_rotate the amounts the scale and rotate presets use (default 0.9, 6°)
ss_enter_shift_x / ss_enter_shift_y slide distance; read under every preset, so fade + a shift is a plain slide-in
ss_enter_direction right, left, alternate — applied as a sign, so alternate alternates sides
ss_media_exit same, mirror (leaves towards the other side, as the reference does), fade
ss_pane_intro / ss_intro_ms the pane animates in once, when the section arrives
ss_pointer_parallax the pane leans towards the cursor
ss_parallax_source viewport — tracks the cursor anywhere on the page — or section
ss_parallax_axis x (as the reference), y, both. The rotation follows the horizontal reading.
ss_parallax_shift / ss_parallax_rotate / ss_parallax_ms / ss_parallax_easing amplitude and feel of the follow

reference pins the source site's literal scale(0) rotate(45deg) translateY(50%) and ignores the numeric knobs; every other preset reads them.

The pane arrival start state is armed by the script, never rendered — and only while the section is still below the fold. A start state in the stylesheet would leave the pane collapsed forever on a page whose script never ran, and arming a section already on screen would paint the image and then yank it back out.

Two things to know when styling it:

  • scrollsync.css is unlayered, unlike the other two stylesheets, because the item titles are headings and an unlayered h3 {} in a theme beats every cascade layer. Its rules sit at 0-3-0 and its state rules at 0-4-0, so instance Custom CSS still wins.
  • overflow: hidden on any ancestor of a sticky element silently stops it sticking. This module keeps its own overflow inside the sticky pane. If the pane will not pin, look for an overflow or a clip-path on a wrapper above it.

A section using this module is a classic shortcode, so it cannot become a native Elementor component. Keep the surrounding section in the builder and let the module render only the list and pane.