Layr Components

Components / Preloader

Slate Intro

On first load a clapperboard fills the screen, its striped arm snaps shut, and the page hard-cuts in. Every field is your text.

Props
1.3 s
#FFFFFF
#0E0E0D

Press Replay to clap it again.

In Framer, open Assets, add a new code file, paste this over everything in it and save. Slate Intro shows up in the Insert menu, and its controls appear in the right panel.
// Slate Intro · Layr Components
//
// When the page first loads a clapperboard fills the screen, its striped arm
// snaps shut, and the page hard-cuts in. Every field on the slate is your text.
// Pure CSS: it starts before any JavaScript runs, never blocks clicks, and is
// skipped for visitors who prefer reduced motion.
// Paste into a new code file in Framer. Place one on the page (or in a shared
// layout); it covers the window by itself. On the canvas it shows as a small
// label so it never covers your design. Preview the page to see it.

import { addPropertyControls, ControlType, RenderTarget } from "framer"
import { useEffect, useState, type CSSProperties } from "react"

interface SlateIntroProps {
    production: string
    scene: string
    take: string
    roll: string
    director: string
    duration: number
    background: string
    color: string
    fontFamily: string
    once: boolean
    style?: CSSProperties
}

/** The slate plays once per page load; moving between pages inside the site won't replay it. */
let played = false

const SHEET = `
@keyframes lyr-si-cut{to{visibility:hidden}}
@keyframes lyr-si-clap{0%,50%{transform:rotate(-22deg)}64%,100%{transform:rotate(0deg)}}
@media (prefers-reduced-motion:reduce){.lyr-si{display:none!important}}
`

/**
 * @framerIntrinsicWidth 200
 * @framerIntrinsicHeight 28
 *
 * @framerSupportedLayoutWidth auto
 * @framerSupportedLayoutHeight auto
 */
export default function SlateIntro(props: SlateIntroProps) {
    const {
        production = "Showreel",
        scene = "01",
        take = "01",
        roll = "A",
        director = "Mara Ellis",
        duration = 1.3,
        background = "#0A0A0A",
        color = "#F2EFE9",
        fontFamily = "Inter",
        once = true,
    } = props
    const target = RenderTarget.current()
    const onCanvas = target === RenderTarget.canvas || target === RenderTarget.thumbnail || target === RenderTarget.export
    const [show] = useState(() => !once || !played)
    useEffect(() => {
        played = true
    }, [])

    const font = `"${fontFamily}", "Inter", system-ui, sans-serif`
    if (onCanvas) {
        return (
            <div
                style={{
                    ...props.style,
                    padding: "7px 10px",
                    borderRadius: 4,
                    background,
                    color,
                    border: `1px dashed ${color}`,
                    fontFamily: font,
                    fontSize: 12,
                    lineHeight: 1,
                    whiteSpace: "nowrap",
                    opacity: 0.8,
                }}
            >
                Slate intro · plays on load
            </div>
        )
    }
    if (!show) return null

    const seconds = Math.min(3, Math.max(0.6, Number(duration) || 1.3))
    const stripes = `repeating-linear-gradient(-45deg, ${color} 0 18px, ${background} 18px 36px)`
    const field = (label: string, value: string, wide = false) => (
        <div style={{ display: "flex", flexDirection: "column", gap: 8, padding: "14px 16px", gridColumn: wide ? "1 / -1" : undefined, borderTop: `1px solid ${color}` }}>
            <span style={{ fontSize: 12, lineHeight: 1, letterSpacing: "-0.005em", opacity: 0.6 }}>{label}</span>
            <span style={{ fontSize: "clamp(22px, 3vw, 32px)", lineHeight: 1, letterSpacing: "-0.02em" }}>{value}</span>
        </div>
    )

    return (
        <div
            className="lyr-si"
            aria-hidden="true"
            style={{
                position: "fixed",
                inset: 0,
                zIndex: 2147482000,
                display: "flex",
                alignItems: "center",
                justifyContent: "center",
                padding: 24,
                background,
                color,
                fontFamily: font,
                pointerEvents: "none",
                animation: `lyr-si-cut 0s linear ${seconds}s forwards`,
            }}
        >
            <style>{SHEET}</style>
            <div style={{ width: "min(460px, 86vw)", border: `1.5px solid ${color}`, borderRadius: 6, overflow: "visible" }}>
                <div
                    style={{
                        height: 40,
                        background: stripes,
                        borderBottom: `1.5px solid ${color}`,
                        transformOrigin: "0% 100%",
                        animation: `lyr-si-clap ${seconds * 0.8}s cubic-bezier(0.7, 0, 0.3, 1) forwards`,
                        borderRadius: "5px 5px 0 0",
                    }}
                />
                <div style={{ height: 40, background: stripes, opacity: 0.9 }} />
                <div style={{ display: "grid", gridTemplateColumns: "1fr 1fr 1fr" }}>
                    {field("Production", production, true)}
                    {field("Scene", scene)}
                    {field("Take", take)}
                    {field("Roll", roll)}
                    {field("Director", director, true)}
                </div>
            </div>
        </div>
    )
}

addPropertyControls(SlateIntro, {
    production: { type: ControlType.String, title: "Production", defaultValue: "Showreel" },
    scene: { type: ControlType.String, title: "Scene", defaultValue: "01" },
    take: { type: ControlType.String, title: "Take", defaultValue: "01" },
    roll: { type: ControlType.String, title: "Roll", defaultValue: "A" },
    director: { type: ControlType.String, title: "Director", defaultValue: "Mara Ellis" },
    duration: {
        type: ControlType.Number,
        title: "Duration",
        defaultValue: 1.3,
        min: 0.6,
        max: 3,
        step: 0.1,
        unit: "s",
        description: "How long the slate shows before it cuts to the page.",
    },
    background: { type: ControlType.Color, title: "Background", defaultValue: "#0A0A0A" },
    color: { type: ControlType.Color, title: "Color", defaultValue: "#F2EFE9" },
    fontFamily: { type: ControlType.String, title: "Font family", defaultValue: "Inter", description: "Use the family name of your text styles." },
    once: {
        type: ControlType.Boolean,
        title: "Once per load",
        defaultValue: true,
        description: "On: plays when the site is opened or reloaded, not again on page changes inside the site. Off: plays every time it appears.",
    },
})

In this preview

The photos are demo pictures shown on this site only; they are not in the files you copy. Set your own through the image props.