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
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.zipShips 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_marqueefilters marquee pre-detection.cmk_should_enqueue_carouselfilters carousel pre-detection.cmk_cache_ttlfilters 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_wordscounts words. CSS clamps lines or clips pixels, so it can only chop mid-token:MIC (Methionine/Inositol/C…instead ofMIC (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.cssis unlayered, unlike the other two stylesheets, because the item titles are headings and an unlayeredh3 {}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: hiddenon any ancestor of a sticky element silently stops it sticking. This module keeps its ownoverflowinside the sticky pane. If the pane will not pin, look for anoverflowor aclip-pathon 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.