Portfolio Logo
WritingFramer
Framer

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.

Category Framer·Updated May 2025·5 min read
01
02
03
04
05FEAT
06
07
08
09
10FEAT

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):

TypeScript
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.

EntryConditionResult
{ 1440: 5 }viewport ≥ 1440pxEvery 5th card → Secondary
{ 1200: 4 }1200px – 1439pxEvery 4th card → Secondary
{ 810: 3 }810px – 1199pxEvery 3rd card → Secondary
(entry removed)No matchOverride 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. 1

    Create a new file in your Framer project’s overrides folder and paste the code. Name it NthItemVariant.tsx.

  2. 2

    Update DEFAULT_VARIANT and SECONDARY_VARIANT to match the exact variant names on your card component in Framer.

  3. 3

    Edit the BREAKPOINT_NTH array to match your design’s column counts and breakpoints. Add or remove entries freely.

  4. 4

    Select your card on the canvas. In the right panel under Code Overrides, choose withFifthItemVariant.

  5. 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:

TypeScript
const DEFAULT_VARIANT   = "Default"                  // e.g. "Standard", "Base"
const SECONDARY_VARIANT = "Default - Position Diff"  // rename to match your variant

Limitations

Slight paint delay
The variant is set after mount via a DOM read. Cards above the fold may briefly flash the Default variant before switching. This is a constraint of all DOM-based position detection in Framer.
🌐
DOM structure dependency
The override walks up the DOM to find its position. If Framer changes how CMS collection lists render internally, the walk logic may need adjusting.
🃏
Card component only
Apply this override to the card — not the collection list wrapper. Applying it to the list won’t work because Framer doesn’t expose CMS children as walkable React children.