Show a Secondary Variant on Every Nth CMS Item in Framer
A free code override that automatically highlights every 5th (or Nth) card in a Framer CMS collection — with full breakpoint control, zero extra markup, and a live resize listener.
Every 5th card receives the Secondary variant → highlighted in purple
Overview
Framer’s CMS collection lists are great for rendering dynamic content — but there’s no built-in way to make every Nth item look different from the rest. Whether you want a “featured” project card, a highlighted testimonial, or a visually distinct entry at a regular interval, you currently need a code override to pull it off.
This override solves exactly that. Drop it onto your card component, tell it which breakpoints should trigger which interval, and it handles everything automatically — including live resize updates.
The code
Copy and paste this into a new file in your Framer project’s overrides folder (e.g. NthItemVariant.tsx):
import { ComponentType, forwardRef, useEffect, useRef, useState } from "react"
// ─── CONFIG ──────────────────────────────────────────────────────────────────
const DEFAULT_VARIANT = "Default"
const SECONDARY_VARIANT = "Default - Position Diff" // rename this to match your Framer variant
// Breakpoint rules — each entry: { "viewportWidth": nthPosition }
// The LARGEST matching breakpoint (≤ current window width) wins.
// Remove any entry you don't need — the remaining ones still work.
const BREAKPOINT_NTH: Record<number, number>[] = [
{ 1440: 5 }, // viewport >= 1440px → highlight every 5th item
{ 1200: 4 }, // viewport >= 1200px → highlight every 4th item
{ 810: 3 }, // viewport >= 810px → highlight every 3th item
]
// ─────────────────────────────────────────────────────────────────────────────
/** Resolve which NTH value to use for the current viewport width. */
function resolveNth(width: number): number | null {
// Flatten and sort breakpoints descending
const sorted = BREAKPOINT_NTH.flatMap((entry) =>
Object.entries(entry).map(([bp, nth]) => ({
bp: Number(bp),
nth: Number(nth),
}))
).sort((a, b) => b.bp - a.bp)
// Pick the largest breakpoint that is <= current width
const match = sorted.find((entry) => width >= entry.bp)
return match ? match.nth : null
}
export function withFifthItemVariant(Component): ComponentType {
return forwardRef((props: any, ref) => {
const [variant, setVariant] = useState(DEFAULT_VARIANT)
const domRef = useRef<HTMLElement>(null)
useEffect(() => {
function calculate() {
const el = domRef.current
if (!el) return
const nth = resolveNth(window.innerWidth)
// No matching breakpoint → always use default
if (nth === null) {
setVariant(DEFAULT_VARIANT)
return
}
// Walk up DOM to find the collection list container
let node: HTMLElement | null = el
while (node && node.parentElement) {
const siblings = Array.from(node.parentElement.children)
if (siblings.length > 1) {
const index = siblings.indexOf(node) // 0-based
const isNth = (index + 1) % nth === 0
setVariant(isNth ? SECONDARY_VARIANT : DEFAULT_VARIANT)
return
}
node = node.parentElement
}
}
calculate()
window.addEventListener("resize", calculate)
return () => window.removeEventListener("resize", calculate)
}, [])
// Merge forwarded ref with local domRef
const mergedRef = (el: HTMLElement | null) => {
;(domRef as any).current = el
if (typeof ref === "function") ref(el)
else if (ref) (ref as any).current = el
}
return <Component {...props} ref={mergedRef} variant={variant} />
})
}How it works
The override is applied directly to your card component — not the collection list wrapper. When each card mounts, it reads its own position in the DOM by walking up the tree until it finds a parent element that contains all its siblings. From there it checks whether (index + 1) % nth === 0 and sets the variant accordingly.
A resize listener recalculates on every window resize, so the correct breakpoint rule is always active — including in Framer’s preview panel.
Why DOM walking instead of itemIndex?
Framer doesn’t pass an itemIndex prop to CMS collection items. Reading position from the DOM after mount is the only reliable method that works in both preview and published sites.
Breakpoint configuration
The entire breakpoint system lives in one array at the top of the file. Each entry is an object with a single key (viewport width in px) and a value (every Nth item to highlight). The largest matching breakpoint wins.
| Entry | Condition | Result |
|---|---|---|
| { 1440: 5 } | viewport ≥ 1440px | Every 5th card → Secondary |
| { 1200: 4 } | 1200px – 1439px | Every 4th card → Secondary |
| { 810: 3 } | 810px – 1199px | Every 3rd card → Secondary |
| (entry removed) | No match | Override inactive → always Default |
Handling small screens
There’s no special “mobile” key. The largest breakpoint that is ≤ the current width always wins, so add a low breakpoint like { 0: 3 } as a universal catch-all. Leave the smallest entry out entirely and the override simply does nothing below your smallest defined breakpoint — the card stays on the Default variant.
How to set it up
- 1
Create a new file in your Framer project’s overrides folder and paste the code. Name it NthItemVariant.tsx.
- 2
Update DEFAULT_VARIANT and SECONDARY_VARIANT to match the exact variant names on your card component in Framer.
- 3
Edit the BREAKPOINT_NTH array to match your design’s column counts and breakpoints. Add or remove entries freely.
- 4
Select your card on the canvas. In the right panel under Code Overrides, choose withFifthItemVariant.
- 5
Hit preview — every Nth card should render your Secondary variant automatically.
Editing the variants
Your component variants will have different names. After creating them in Framer’s component editor, update the two constants at the top of the file:
const DEFAULT_VARIANT = "Default" // e.g. "Standard", "Base"
const SECONDARY_VARIANT = "Default - Position Diff" // rename to match your variant