Skip to content
Blocks

Feature Mosaic

An asymmetric feature grid with a leading product story.

Open ↗

Make it yours.

Install the editable source and its dependencies into your React + Tailwind project.

Scroll horizontally for long lines
pnpm dlx shadcn@4.0.8 add http://localhost:3000/r/feature-mosaic.json

Use it

Scroll horizontally for long lines
"use client";
import { FeatureMosaic } from "@/components/jez-ui/blocks/feature-mosaic";

export default function Example() {
  return (
    <>
      <FeatureMosaic />
    </>
  );
}

Compose your own

These styled parts are included in the same install. Use children to build your layout; add className only when you want an override.

Scroll horizontally for long lines
"use client";
import {
  FeatureMosaic,
  FeatureMosaicGrid,
  FeatureMosaicHeading,
} from "@/components/jez-ui/blocks/feature-mosaic";
export default function FeatureMosaicExample() {
  return (
    <FeatureMosaic>
      <FeatureMosaicHeading />
      <FeatureMosaicGrid />
    </FeatureMosaic>
  );
}
Scroll horizontally for long lines
import { FeatureMosaic, FeatureMosaicTitle, FeatureMosaicDescription, FeatureMosaicHeading, FeatureMosaicGrid, FeatureMosaicLead, FeatureMosaicCard } from "@/components/jez-ui/blocks/feature-mosaic";
Read the composition guide →

Props & types

Scroll horizontally for long lines
export type FeatureMosaicItem = {
  title: React.ReactNode;
  description: React.ReactNode;
  icon?: React.ComponentType<{ className?: string; size?: number }>;
};

export type FeatureMosaicProps = Omit<
  React.ComponentPropsWithoutRef<"section">,
  "title"
> & {
  title?: React.ReactNode;
  description?: React.ReactNode;
  items?: FeatureMosaicItem[];
};

Source

View 2 source files

registry/blocks/feature-mosaic.tsx

Scroll horizontally for long lines
"use client";

import * as React from "react";
import { ArrowUpRight, Braces, CheckCircle2, Sparkles } from "lucide-react";
import { cn } from "../ui/utils";

export type FeatureMosaicItem = {
  title: React.ReactNode;
  description: React.ReactNode;
  icon?: React.ComponentType<{ className?: string; size?: number }>;
};

const defaultItems: FeatureMosaicItem[] = [
  {
    title: "A considered first draft",
    description:
      "Start with a clear brief, then let the work take shape together.",
    icon: Sparkles,
  },
  {
    title: "Every decision stays visible",
    description:
      "Keep approvals, source material, and useful context in reach.",
    icon: CheckCircle2,
  },
  {
    title: "Built to meet your stack",
    description: "Bring the tools your team already trusts into the same flow.",
    icon: Braces,
  },
];

export type FeatureMosaicProps = Omit<
  React.ComponentPropsWithoutRef<"section">,
  "title"
> & {
  title?: React.ReactNode;
  description?: React.ReactNode;
  items?: FeatureMosaicItem[];
};

type FeatureMosaicModel = Required<
  Pick<FeatureMosaicProps, "title" | "description" | "items">
>;
const FeatureMosaicContext = React.createContext<FeatureMosaicModel | null>(
  null,
);
function useFeatureMosaic() {
  const context = React.useContext(FeatureMosaicContext);
  if (!context)
    throw new Error("FeatureMosaic parts must be inside FeatureMosaic.");
  return context;
}

export const FeatureMosaic = React.forwardRef<HTMLElement, FeatureMosaicProps>(
  function FeatureMosaic(
    {
      title = "The pieces you need, arranged around the work.",
      description = "A flexible workspace for turning early direction into a decision everyone can follow.",
      items = defaultItems,
      className,
      children,
      ...props
    },
    ref,
  ) {
    return (
      <FeatureMosaicContext.Provider value={{ title, description, items }}>
        <section
          ref={ref}
          data-slot="feature-mosaic"
          className={cn("py-12 md:py-20", className)}
          {...props}
        >
          {children !== undefined ? (
            children
          ) : (
            <>
              <FeatureMosaicHeading />
              <FeatureMosaicGrid />
            </>
          )}
        </section>
      </FeatureMosaicContext.Provider>
    );
  },
);

export const FeatureMosaicTitle = React.forwardRef<
  HTMLHeadingElement,
  React.ComponentPropsWithoutRef<"h2">
>(function FeatureMosaicTitle({ className, ...props }, ref) {
  return (
    <h2
      ref={ref}
      data-slot="feature-mosaic-title"
      className={cn(
        "max-w-2xl text-balance text-4xl leading-[1.04] tracking-[-0.035em] md:text-6xl",
        className,
      )}
      {...props}
    />
  );
});
export const FeatureMosaicDescription = React.forwardRef<
  HTMLParagraphElement,
  React.ComponentPropsWithoutRef<"p">
>(function FeatureMosaicDescription({ className, ...props }, ref) {
  return (
    <p
      ref={ref}
      data-slot="feature-mosaic-description"
      className={cn(
        "mt-5 max-w-xl text-sm leading-relaxed text-muted-foreground md:text-base",
        className,
      )}
      {...props}
    />
  );
});
export const FeatureMosaicHeading = React.forwardRef<
  HTMLDivElement,
  React.ComponentPropsWithoutRef<"div">
>(function FeatureMosaicHeading({ className, children, ...props }, ref) {
  const { title, description } = useFeatureMosaic();
  return (
    <div
      ref={ref}
      data-slot="feature-mosaic-heading"
      className={cn("mb-8 md:mb-12", className)}
      {...props}
    >
      {children !== undefined ? (
        children
      ) : (
        <>
          <FeatureMosaicTitle>{title}</FeatureMosaicTitle>
          <FeatureMosaicDescription>{description}</FeatureMosaicDescription>
        </>
      )}
    </div>
  );
});
export const FeatureMosaicGrid = React.forwardRef<
  HTMLDivElement,
  React.ComponentPropsWithoutRef<"div">
>(function FeatureMosaicGrid({ className, children, ...props }, ref) {
  const { items } = useFeatureMosaic();
  return (
    <div
      ref={ref}
      data-slot="feature-mosaic-grid"
      className={cn(
        "grid overflow-hidden rounded-[14px] border border-border bg-muted md:grid-cols-12",
        className,
      )}
      {...props}
    >
      {children !== undefined
        ? children
        : items.map((item, index) =>
            index === 0 ? (
              <FeatureMosaicLead key={index} item={item} />
            ) : (
              <FeatureMosaicCard key={index} item={item} />
            ),
          )}
    </div>
  );
});
export const FeatureMosaicLead = React.forwardRef<
  HTMLDivElement,
  React.ComponentPropsWithoutRef<"div"> & { item?: FeatureMosaicItem }
>(function FeatureMosaicLead({ item, className, children, ...props }, ref) {
  const Icon = item?.icon ?? Sparkles;
  return (
    <div
      ref={ref}
      data-slot="feature-mosaic-lead"
      className={cn(
        "relative min-h-80 overflow-hidden bg-primary p-6 text-primary-foreground md:col-span-7 md:p-9",
        className,
      )}
      {...props}
    >
      {children !== undefined ? (
        children
      ) : (
        <>
          <div className="relative z-10 flex size-10 items-center justify-center rounded-full bg-primary-foreground/15">
            <Icon size={19} />
          </div>
          <div className="relative z-10 mt-16 max-w-sm">
            <p className="text-2xl leading-tight tracking-[-0.03em] md:text-4xl">
              {item?.title}
            </p>
            <p className="mt-4 max-w-xs text-sm leading-relaxed text-primary-foreground/75">
              {item?.description}
            </p>
          </div>
          <div
            aria-hidden
            className="absolute -right-10 -top-10 size-64 rounded-full border-[28px] border-primary-foreground/10"
          />
          <div
            aria-hidden
            className="absolute bottom-8 right-8 grid size-28 place-items-center rounded-full border border-primary-foreground/20 text-center text-xs font-medium uppercase tracking-[0.18em]"
          >
            Make
            <br />
            room
          </div>
        </>
      )}
    </div>
  );
});
export const FeatureMosaicCard = React.forwardRef<
  HTMLDivElement,
  React.ComponentPropsWithoutRef<"div"> & { item?: FeatureMosaicItem }
>(function FeatureMosaicCard({ item, className, children, ...props }, ref) {
  const Icon = item?.icon ?? CheckCircle2;
  return (
    <div
      ref={ref}
      data-slot="feature-mosaic-card"
      className={cn(
        "group relative min-h-64 border-t border-border bg-background p-6 md:col-span-5 md:border-l md:border-t-0 md:p-8 [&+&]:md:col-span-5 [&+&]:md:border-l-0 [&+&]:md:border-t",
        className,
      )}
      {...props}
    >
      {children !== undefined ? (
        children
      ) : (
        <>
          <div className="flex items-start justify-between">
            <span className="grid size-9 place-items-center rounded-full bg-muted">
              <Icon size={17} />
            </span>
            <ArrowUpRight
              size={17}
              className="text-muted-foreground transition-transform duration-300 group-hover:-translate-y-0.5 group-hover:translate-x-0.5"
            />
          </div>
          <div className="mt-7 rounded-lg border border-border bg-muted/55 p-3">
            <div className="flex items-center justify-between text-xs">
              <span className="font-medium">Release brief</span>
              <span className="rounded-full bg-background px-2 py-1 text-muted-foreground">
                Updated
              </span>
            </div>
            <div className="mt-3 h-1.5 w-4/5 rounded-full bg-border" />
            <div className="mt-2 h-1.5 w-3/5 rounded-full bg-border" />
          </div>
          <div className="mt-5">
            <h3 className="text-xl tracking-[-0.03em]">{item?.title}</h3>
            <p className="mt-2 max-w-sm text-sm leading-relaxed text-muted-foreground">
              {item?.description}
            </p>
          </div>
        </>
      )}
    </div>
  );
});

FeatureMosaic.displayName = "FeatureMosaic";

registry/ui/utils.ts

Scroll horizontally for long lines
import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...values: ClassValue[]) {
  return twMerge(clsx(values));
}

Dependencies

lucide-react@0.577.0 · clsx@2.1.1 · tailwind-merge@3.5.0

Accessibility & behaviour

Provide meaningful labels and preserve keyboard focus styles. Check contrast when customising theme colours.

Read the accessibility and performance guide →