Skip to content
Component

Footer meta

The feed links and credits rows at the very bottom of a footer.

  • #arc
  • #component
  • #footer
  • #feed
  • #rss
  • #credits
  • #meta

Install

One command installs the files and packages this item needs.

Terminalbash
npx shadcn@latest add https://dsui.vercel.app/r/footer-meta.json

footer-meta.module.csscss
.meta { display: flex; flex-direction: column; }
.feeds { display: flex; flex-wrap: wrap; align-items: center; gap: var(--space-2) var(--space-3); padding-block: var(--space-4); border-block-end: 1px solid var(--border-subtle); }
.feedItem { display: inline-flex; align-items: center; gap: var(--space-2) var(--space-3); }
.divider { width: 1px; height: var(--space-4); background: var(--border); }
.feed { display: inline-flex; align-items: center; gap: var(--space-1); border-radius: var(--radius-pill); padding: var(--space-1) var(--space-2); color: var(--text-secondary); font-size: var(--text-sm); text-decoration: none; transition: color var(--duration-fast) var(--ease-standard), background var(--duration-fast) var(--ease-standard); }
.feed svg { flex: none; }
@media (hover: hover) and (pointer: fine) { .feed:hover { background: var(--surface-muted); color: var(--foreground); } }
.feed:active { background: var(--surface-sunken); color: var(--foreground); }

.credits { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: var(--space-2) var(--space-4); padding-block: var(--space-4); }
/* Smaller than the feed row, since nobody comes to a page looking for this. The colour still has to hold its contrast. */
.copyright { color: var(--text-secondary); font-size: var(--text-xs); }
.creditList { display: flex; flex-wrap: wrap; gap: var(--space-2) var(--space-4); }
.credit { color: var(--text-secondary); font-size: var(--text-xs); }
.creditName { color: var(--foreground); }
.creditLink { color: var(--foreground); text-decoration: none; }
@media (hover: hover) and (pointer: fine) { .creditLink:hover { text-decoration: underline; text-underline-offset: 3px; } }
@media (prefers-reduced-motion: reduce) { .feed { transition: none; } }
footer-meta.tsxtsx
import type { HTMLAttributes, ReactNode } from "react";
import { ArcLink } from "../lib/arc-provider";
import styles from "./footer-meta.module.css";

export interface FooterFeedLink {
  href: string;
  label: string;
  /** A small decorative mark before the label. */
  icon?: ReactNode;
  /** Leaves the link as a plain anchor: a file, a feed, another origin. */
  plain?: boolean;
}

export interface FooterCredit {
  /** What is being credited, e.g. "Powered by". */
  label: string;
  /** Who, e.g. "Arc". */
  name: ReactNode;
  href?: string;
}

export interface FooterMetaProps extends HTMLAttributes<HTMLDivElement> {
  /** Machine-readable things: a feed, llms.txt, a sitemap. */
  feeds?: FooterFeedLink[];
  /** The copyright line, at the start of the credits row. */
  copyright?: ReactNode;
  /** What the site owes to others. */
  credits?: FooterCredit[];
  /** The accessible name of the feed row. */
  feedLabel?: string;
}

/**
 * The two quiet rows at the very bottom of a footer: what machines can read, and what the site owes to other people.
 * They are separated because they are read by different audiences — one by a feed reader or a crawler, the other by a
 * person — and keeping them in one row makes both harder to find.
 *
 * Everything here is 12–13px. Bigger and it competes with the content; the contrast still has to hold, so the size
 * drops but the colour does not.
 */
export function FooterMeta({ className, feeds, copyright, credits, feedLabel = "피드와 파일", ...props }: FooterMetaProps) {
  if (!feeds?.length && !copyright && !credits?.length) return null;
  return (
    <div {...props} className={[styles.meta, className].filter(Boolean).join(" ")}>
      {feeds?.length ? (
        <nav aria-label={feedLabel} className={styles.feeds}>
          {feeds.map((feed, index) => (
            <span key={feed.href} className={styles.feedItem}>
              {index > 0 ? <span aria-hidden="true" className={styles.divider} /> : null}
              {feed.plain ? (
                <a href={feed.href} className={styles.feed}>{feed.icon}{feed.label}</a>
              ) : (
                <ArcLink href={feed.href} className={styles.feed}>{feed.icon}{feed.label}</ArcLink>
              )}
            </span>
          ))}
        </nav>
      ) : null}

      {copyright || credits?.length ? (
        <div className={styles.credits}>
          {copyright ? <span className={styles.copyright}>{copyright}</span> : null}
          {credits?.length ? (
            <span className={styles.creditList}>
              {credits.map(credit => (
                <span key={credit.label} className={styles.credit}>
                  {credit.label}{" "}
                  {credit.href
                    ? <a href={credit.href} className={styles.creditLink} rel="noreferrer">{credit.name}</a>
                    : <span className={styles.creditName}>{credit.name}</span>}
                </span>
              ))}
            </span>
          ) : null}
        </div>
      ) : null}
    </div>
  );
}

FooterMeta.displayName = "FooterMeta";

export default FooterMeta;