Now with AI-powered page building via MCP Server
Documentation

Advanced Block Features

Layout blocks, Tailwind styling, server vs client components, and fetching data.

Last updated: June 7, 2026

Layout blocks

Header, footer and other shared regions are layout blocks — the same as page blocks, but tagged with layoutPositions and rendered by CmssyServerLayout per position.

// blocks/header/block.ts
import type { ComponentType } from "react";
import { defineBlock, fields } from "@cmssy/react";
import Component from "./src";

export const headerBlock = defineBlock({
  type: "header",
  label: "Header",
  layoutPositions: ["header"],
  component: Component as unknown as ComponentType<{ content: Record<string, unknown> }>,
  props: {
    logo: fields.media({ label: "Logo" }),
    links: fields.repeater({
      label: "Navigation",
      fields: {
        label: fields.singleLine({ label: "Label" }),
        url: fields.link({ label: "URL" }),
      },
    }),
  },
});

Render the position in app/layout.tsx with CmssyServerLayout (see the cmssy-web reference repo).


Styling with Tailwind v4

Blocks are styled with Tailwind. Import your global stylesheet once, then use utility classes in your components.

/* styles/main.css */
@import "tailwindcss";

@theme {
  --color-background: oklch(1 0 0);
  --color-foreground: oklch(0.145 0 0);
  --color-primary: oklch(0.205 0 0);
}

Server & client components

Blocks are standard Next.js components. By default they render as server components — zero client JS. Add "use client" at the top of a component only when it needs hooks, event handlers, browser APIs or client animations.

"use client";
import { useState } from "react";

export default function StatsCounter({ content }: { content: Record<string, unknown> }) {
  const [n, setN] = useState(0);
  // ...
}

Scroll / entrance animations need a client component. If a block uses reveal-on-scroll (framer-motion whileInView, initial={{ opacity: 0 }}, IntersectionObserver), it must be a client component ("use client") so the animation runs — otherwise the server HTML stays at opacity:0 and the content never fades in on the published site (it looks fine in the editor, which renders client-side).


Fetching data in blocks

For custom models, records, forms or site config, use the data client from @cmssy/react instead of the block context:

import { createCmssyClient } from "@cmssy/react";

const client = createCmssyClient({
  apiUrl: process.env.CMSSY_API_URL!,
  workspaceSlug: process.env.CMSSY_WORKSPACE_SLUG!,
});
// client.records(...), client.form(...), client.siteConfig() ...

You can also POST GraphQL directly with graphqlRequest. Auth and form definitions are not in the block context in headless — fetch them through the client.


Next Steps