# Blog Featured

> A featured story with a large photo beside three recent posts.

A shadcn/ui block from the [@devsloka](https://ui.devsloka.in/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/devsloka/blog-featured).

- Page, with a live preview: https://shadcn.exchange/r/devsloka/blog-featured
- Screenshot: https://shadcn.exchange/shot/devsloka/blog-featured/light.png
- Registry item: https://ui.devsloka.in/r/blog-featured.json
- Listed: 2026-10-28, source last changed 2026-10-28

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/devsloka/blog-featured.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `lucide-react`

## Files

### `src/registry/blocks/blog/blog-featured.tsx`

```tsx
import { useId, type CSSProperties, type ComponentProps } from "react";
import { ArrowRight } from "lucide-react";
import { cn } from "@/lib/utils";

export type BlogPost = {
  title: string;
  dek?: string;
  href: string;
  category: string;
  author?: string;
  /** Display date, e.g. "2 Oct 2026". */
  date: string;
  /** Machine-readable date for <time>, e.g. "2026-10-02". */
  dateTime: string;
  /** Reading time in minutes. */
  readingTime: number;
  image: { src: string; alt: string };
};

export type BlogFeaturedProps = {
  eyebrow?: string;
  title?: string;
  archive?: { label: string; href: string };
  featured?: BlogPost;
  posts?: BlogPost[];
  /** Ink used for the category tags. Text on the tag is white, so keep it dark. */
  accent?: string;
} & ComponentProps<"section">;

const unsplash = (id: string, w: number) =>
  `https://images.unsplash.com/${id}?w=${w}&q=80&auto=format&fit=crop`;

const DEFAULT_FEATURED: BlogPost = {
  title: "We moved booking reminders to the night before. No-shows fell 31%.",
  dek: "A guesthouse above Lake Oeschinen asked why half its guests missed the 8 a.m. boat. Eight weeks and 412 properties later we have an answer, and a few caveats we did not expect.",
  href: "#",
  category: "Field notes",
  author: "Ana Ferreira",
  date: "2 Oct 2026",
  dateTime: "2026-10-02",
  readingTime: 9,
  image: {
    src: unsplash("photo-1501785888041-af3ef285b470", 1400),
    alt: "An alpine lake with a small boat, mountains rising behind it",
  },
};

const DEFAULT_POSTS: BlogPost[] = [
  {
    title: "Why the shops we love still hand-letter their opening hours",
    href: "#",
    category: "Owners",
    author: "Hanne Vos",
    date: "24 Sep 2026",
    dateTime: "2026-09-24",
    readingTime: 6,
    image: {
      src: unsplash("photo-1472851294608-062f824d29cc", 400),
      alt: "An “Open” sign hanging on a shop door",
    },
  },
  {
    title: "Seasonal pricing is mostly a guess. Here is how to guess better.",
    href: "#",
    category: "Operations",
    author: "Tomás Reyes",
    date: "11 Sep 2026",
    dateTime: "2026-09-11",
    readingTime: 7,
    image: {
      src: unsplash("photo-1507525428034-b723cf961d3e", 400),
      alt: "A beach at sunset",
    },
  },
  {
    title: "Rebuilding the calendar view without breaking anyone’s bookmarks",
    href: "#",
    category: "Engineering",
    author: "Jun Watanabe",
    date: "19 Aug 2026",
    dateTime: "2026-08-19",
    readingTime: 12,
    image: {
      src: unsplash("photo-1551288049-bebda4e38f71", 400),
      alt: "An analytics dashboard open on a laptop",
    },
  },
];

const DEFAULTS = {
  eyebrow: "Saltbox Journal · Notes from the front desk",
  title: "What we learned running 4,000 small front desks",
  archive: { label: "All 146 articles", href: "#" },
  accent: "#0A6EFF",
};

const TAG_TEXT = "#FFFFFF";

const focusRing =
  "outline-none focus-visible:ring-2 focus-visible:ring-ring focus-visible:ring-offset-4 focus-visible:ring-offset-background";

function Tag({ children }: { children: string }) {
  return (
    <span
      className="inline-block bg-[var(--block-accent)] px-2 py-1 font-mono text-[11px] uppercase leading-none tracking-wider"
      style={{ color: TAG_TEXT }}
    >
      {children}
    </span>
  );
}

function Meta({ post, className }: { post: BlogPost; className?: string }) {
  return (
    <p
      className={cn(
        "flex flex-wrap gap-x-3 gap-y-1 font-mono text-xs text-muted-foreground",
        className
      )}
    >
      {post.author && <span>{post.author}</span>}
      <time dateTime={post.dateTime}>{post.date}</time>
      <span>{post.readingTime} min read</span>
    </p>
  );
}

export function BlogFeatured({
  eyebrow = DEFAULTS.eyebrow,
  title = DEFAULTS.title,
  archive = DEFAULTS.archive,
  featured = DEFAULT_FEATURED,
  posts = DEFAULT_POSTS,
  accent = DEFAULTS.accent,
  className,
  style,
  ...props
}: BlogFeaturedProps) {
  const headingId = useId();

  return (
    <section
      data-slot="blog-featured"
      aria-labelledby={headingId}
      className={cn("w-full bg-background text-foreground", className)}
      style={{ "--block-accent": accent, ...style } as CSSProperties}
      {...props}
    >
      <div className="mx-auto max-w-6xl px-6 py-20 md:py-28">
        <div className="grid gap-6 border-b pb-8 md:grid-cols-12 md:items-end">
          <div className="md:col-span-8">
            <p className="font-mono text-xs uppercase tracking-wider text-muted-foreground">
              {eyebrow}
            </p>
            <h2
              id={headingId}
              className="mt-5 max-w-2xl text-4xl font-semibold leading-[1.02] tracking-[-0.03em] text-balance md:text-5xl"
            >
              {title}
            </h2>
          </div>
          <a
            href={archive.href}
            className={cn(
              "inline-flex items-center gap-2 justify-self-start text-sm font-medium underline decoration-1 underline-offset-[6px] hover:text-muted-foreground md:col-span-4 md:justify-self-end",
              focusRing
            )}
          >
            {archive.label}
            <ArrowRight aria-hidden className="size-4" />
          </a>
        </div>

        <div className="mt-10 grid gap-12 md:grid-cols-12 md:gap-10">
          <article className="md:col-span-7">
            <a href={featured.href} className={cn("group block", focusRing)}>
              {/* Plain <img> keeps the block portable outside Next.js. */}
              {/* eslint-disable-next-line @next/next/no-img-element */}
              <img
                src={featured.image.src}
                alt={featured.image.alt}
                width={1400}
                height={1050}
                className="aspect-[4/3] w-full bg-muted object-cover"
              />
              <div className="mt-6">
                <Tag>{featured.category}</Tag>
                <h3 className="mt-4 text-3xl font-semibold leading-[1.08] tracking-[-0.025em] text-balance decoration-1 underline-offset-[6px] group-hover:underline md:text-4xl">
                  {featured.title}
                </h3>
                {featured.dek && (
                  <p className="mt-4 max-w-xl text-base leading-relaxed text-muted-foreground">
                    {featured.dek}
                  </p>
                )}
                <Meta post={featured} className="mt-5" />
              </div>
            </a>
          </article>

          <div className="md:col-span-5">
            <p className="flex items-baseline justify-between border-b pb-3 font-mono text-xs uppercase tracking-wider text-muted-foreground">
              <span>Also this month</span>
              <span className="tabular-nums">{String(posts.length).padStart(2, "0")}</span>
            </p>
            <ul className="divide-y border-b">
              {posts.map((post) => (
                <li key={post.href + post.title}>
                  <article>
                    <a
                      href={post.href}
                      className={cn("group grid grid-cols-[1fr_6rem] gap-5 py-6 sm:grid-cols-[1fr_7.5rem]", focusRing)}
                    >
                      <div>
                        <Tag>{post.category}</Tag>
                        <h3 className="mt-3 text-lg font-semibold leading-snug tracking-[-0.015em] text-pretty decoration-1 underline-offset-4 group-hover:underline">
                          {post.title}
                        </h3>
                        <Meta post={post} className="mt-3" />
                      </div>
                      {/* eslint-disable-next-line @next/next/no-img-element */}
                      <img
                        src={post.image.src}
                        alt={post.image.alt}
                        width={400}
                        height={300}
                        loading="lazy"
                        className="aspect-[4/3] w-full bg-muted object-cover"
                      />
                    </a>
                  </article>
                </li>
              ))}
            </ul>
            <p className="mt-4 font-mono text-[11px] leading-relaxed text-muted-foreground">
              New pieces most Thursdays. Written by the people who answer your support emails.
            </p>
          </div>
        </div>
      </div>
    </section>
  );
}
```
