Skip to content
Identity primitive

Featured Icon

The glyph-in-a-tinted-box that every feature grid and empty state is built from, in five tones and four treatments — decorative by default, because it sits next to a heading that says the same thing.

124 linesNo dependenciesAdded 13 Sept 2026
  • icon
  • feature
  • empty state
  • container
  • tint

What's included

  • components/ui/featured-icon.tsx
  • No runtime dependencies

Works with

  • React
  • Next.js
  • Tailwind CSS
  • TypeScript

npx hoverlab add featured-icon

Or over MCP, from your editor's agent — no account needed.

License

Free to read, copy and install, for personal and non-commercial projects. Shipping it in client work or a paid product needs Pro ($79 once). The source lands in your repo and stops being ours — no attribution, nothing to upgrade.

Was this useful?

Live

Interactive, in your current theme. Every state below is the real component — type in it, tab through it, switch the theme.

Source

One file. Create components/ui/featured-icon.tsx and paste.

components/ui/featured-icon.tsx
/**
 * <FeaturedIcon> — an icon in a container, which is a different thing.
 *
 * This is the single most repeated element in a marketing or empty-state
 * design and nobody ships it: a glyph, centred in a shaped and tinted box,
 * at one of a few sizes. Every feature grid, every empty state, every
 * "what's included" row is a stack of these, and each project rebuilds the
 * ring-and-tint arithmetic by hand.
 *
 * The variants are the ones that actually recur:
 *
 *   soft     tinted background, coloured glyph. The default.
 *   solid    filled with the tone, glyph knocked out.
 *   outline  border only, for dense layouts where a filled square is loud.
 *   ring     soft, with two concentric translucent rings — the "halo"
 *            treatment used in empty states, where the icon carries a
 *            whole screen and needs the extra presence.
 *
 * It is decoration, and it is marked as such. A featured icon sits next to
 * a heading that says the same thing, so announcing it is a duplicate. Pass
 * `label` on the rare occasion it is the only content — then it becomes an
 * `img` role with a name instead of being hidden.
 */

import * as React from 'react'

export type IconTone = 'brand' | 'neutral' | 'success' | 'warning' | 'danger'
export type IconVariant = 'soft' | 'solid' | 'outline' | 'ring'

const TONES: Record<IconTone, { soft: string; solid: string; outline: string; halo: string }> = {
  brand: {
    soft: 'bg-primary/10 text-primary',
    solid: 'bg-primary text-primary-foreground',
    outline: 'border border-primary/30 text-primary',
    halo: 'ring-primary/10',
  },
  neutral: {
    soft: 'bg-muted text-muted-foreground',
    solid: 'bg-foreground text-background',
    outline: 'border border-border text-muted-foreground',
    halo: 'ring-foreground/5',
  },
  success: {
    soft: 'bg-emerald-500/10 text-emerald-600 dark:text-emerald-400',
    solid: 'bg-emerald-600 text-white',
    outline: 'border border-emerald-500/30 text-emerald-600 dark:text-emerald-400',
    halo: 'ring-emerald-500/10',
  },
  warning: {
    soft: 'bg-amber-500/10 text-amber-600 dark:text-amber-400',
    solid: 'bg-amber-500 text-amber-950',
    outline: 'border border-amber-500/30 text-amber-600 dark:text-amber-400',
    halo: 'ring-amber-500/10',
  },
  danger: {
    soft: 'bg-destructive/10 text-destructive',
    solid: 'bg-destructive text-destructive-foreground',
    outline: 'border border-destructive/30 text-destructive',
    halo: 'ring-destructive/10',
  },
}

const SIZES = {
  sm: 'h-8 w-8 rounded-lg [&>svg]:h-4 [&>svg]:w-4',
  md: 'h-10 w-10 rounded-xl [&>svg]:h-5 [&>svg]:w-5',
  lg: 'h-12 w-12 rounded-xl [&>svg]:h-6 [&>svg]:w-6',
  xl: 'h-14 w-14 rounded-2xl [&>svg]:h-7 [&>svg]:w-7',
} as const

export interface FeaturedIconProps {
  /** The glyph. Sized by the container, so pass it without size classes. */
  children: React.ReactNode
  tone?: IconTone
  variant?: IconVariant
  size?: keyof typeof SIZES
  /** Circle instead of the rounded square. */
  circle?: boolean
  /** Only when the icon is the sole content. Otherwise it stays decorative. */
  label?: string
  className?: string
}

export function FeaturedIcon({
  children,
  tone = 'brand',
  variant = 'soft',
  size = 'md',
  circle = false,
  label,
  className = '',
}: FeaturedIconProps) {
  const palette = TONES[tone]

  const surface =
    variant === 'solid'
      ? palette.solid
      : variant === 'outline'
        ? palette.outline
        : palette.soft

  return (
    <span
      // Hidden by default: it sits beside a heading that already says this.
      role={label ? 'img' : undefined}
      aria-label={label}
      aria-hidden={label ? undefined : true}
      className={[
        'inline-flex shrink-0 items-center justify-center',
        SIZES[size],
        circle ? 'rounded-full' : '',
        surface,
        // The halo is two rings rather than one wide one, so the falloff
        // reads as a glow instead of a second border.
        variant === 'ring' ? `ring-[6px] ${palette.halo}` : '',
        className,
      ]
        .filter(Boolean)
        .join(' ')}
    >
      {children}
    </span>
  )
}

Props

Read out of the component’s own type and signature, so this cannot drift from the source below. Every prop has a default — the component renders standalone before you pass it anything.

PropTypeDefault
childrenrequiredThe glyph. Sized by the container, so pass it without size classes.React.ReactNode—
toneIconTone'brand'
variantIconVariant'soft'
sizekeyof typeof SIZES'md'
circleCircle instead of the rounded square.booleanfalse
labelOnly when the icon is the sole content. Otherwise it stays decorative.string—
classNamestring''

For AI

The component, its props, the design tokens it expects and the command that installs it — as one prompt. Paste it into Claude, Cursor, v0 or ChatGPT and what they build around it will match the rest of the catalog instead of inventing its own system.

See the prompt

More identity

View all
Open the full page for this primitive

Avatar Group

Overlapping faces with a descending stack order, deterministic colours for initials, and an overflow chip announced as "and 3 more" rather than an ambiguous +3.

Identity148 linesNo deps