Skip to content
UI Kit
Accent color

Logo generator

Placeholder brands from a seed for logo clouds, mockups and case studies. Twelve families of marks, seeded wordmarks in the one typeface, lockups, stacked logos and app icons. The same name always gives the same brand, none of them looks like a real company.

One of 3 components in Placeholders and generators

5 variants

Variants

Marks

Twelve symbols from twelve families, each with seeded details. Switch between mono, accent and color, hover turns a mark.

Open in Studio

Lockups

Symbol and name side by side, stacked or the word alone. The marks spring in and the words sharpen as they scroll into view.

Open in Studio

App icons

The symbol on a rounded tile with a soft light, on a home screen with a dock. Icons rise in one after another and press down on a tap.

Open in Studio

Logo cloud

A quiet trusted-by grid for a landing page before the real logos arrive. Muted until hover, drawn in as it scrolls into view.

Open in Studio

Brand sheet

Type a name and get a small brand sheet with the lockup, the stacked logo on dark and the icon in three sizes. Another take draws a new version.

Open in Studio

Details

What Logo generator brings along, read from the registry and the source.

Variants
5
Version
1.0.0 · since 1.1.0
File
components/ui/logo-generator.tsx
Brings along
utilskit
npm packages
None
Languages
Texts come through props

Props

The GeneratedLogoProps interface from components/ui/logo-generator.tsx, with the comments from the code.

  • namerequired
    string

    The brand name. Also the seed unless seed is given.

  • seedoptional
    string
  • layoutoptional
    LogoLayout

    markwordmarklockupstackedapp-icon

  • toneoptional
    LogoTone

    monoaccentcolor

  • sizeoptional
    number

    Height of the mark in pixels, the text scales with it. Default 32.

  • animateoptional
    boolean

    Draw the mark in when it scrolls into view

  • markoptional
    number

    Pick the family of the mark yourself, 0 to 11. Default: from the seed.

Accessibility

Read from the source of the component.

  • Sets aria-label, so screen readers announce purpose and state.
  • Uses the role img.
  • Needs no client state. The server writes it in full, so it reads without JavaScript too.

Motion

  • With reduced motion (prefers-reduced-motion) it stays still. 2 places in the code switch motion off then.
  • No animations, it stands still from the start.

Source

The file aiw add puts into your project. From then on it is yours.

Show the file, 256 lines
components/ui/logo-generator.tsx
'use client';

import { cn, round, useInView } from '@/lib/kit';

/*
 * Placeholder brands from a seed: a mark, a wordmark, or both. For logo
 * clouds, app mockups, case studies and anywhere a real logo is not there
 * yet. The same seed always gives the same brand. Nothing here resembles a
 * real company, the marks are built from simple geometry and the words are
 * set in the kit's one typeface.
 *
 *   mark       the symbol alone
 *   wordmark   the name in a seeded treatment (weight, case, tracking, a dot or a bar)
 *   lockup     symbol and name side by side
 *   stacked    symbol over name, centered
 *   app-icon   the symbol on a rounded tile with a soft light, like a home screen icon
 *
 * tone: mono follows the text color (for logo rows), accent uses the theme
 * accent, color picks one of the theme colors from the seed.
 */

export type LogoLayout = 'mark' | 'wordmark' | 'lockup' | 'stacked' | 'app-icon';
export type LogoTone = 'mono' | 'accent' | 'color';

export interface GeneratedLogoProps {
  /** The brand name. Also the seed unless seed is given. */
  name: string;
  seed?: string;
  layout?: LogoLayout;
  tone?: LogoTone;
  /** Height of the mark in pixels, the text scales with it. Default 32. */
  size?: number;
  /** Draw the mark in when it scrolls into view */
  animate?: boolean;
  /** Pick the family of the mark yourself, 0 to 11. Default: from the seed. */
  mark?: number;
  className?: string;
}

function hash(s: string) {
  let h = 2166136261;
  for (let i = 0; i < s.length; i++) {
    h ^= s.charCodeAt(i);
    h = Math.imul(h, 16777619);
  }
  return h >>> 0;
}
function random(seed: number) {
  let a = seed;
  return () => {
    a = (a + 0x6d2b79f5) | 0;
    let t = Math.imul(a ^ (a >>> 15), 1 | a);
    t = (t + Math.imul(t ^ (t >>> 7), 61 | t)) ^ t;
    return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
  };
}

const COLORS = ['var(--primary)', 'var(--success)', 'var(--warning)', 'var(--info)', 'var(--primary-2)', 'var(--destructive)'];

/** The geometry of a mark on a 32 × 32 grid. Twelve families, each with seeded details. */
function markShapes(kind: number, r: () => number, letter: string): React.ReactNode {
  const v = Math.floor(r() * 3);
  switch (kind) {
    case 0: // sliced circle
      return (
        <>
          <path d="M16 3a13 13 0 1 1 0 26Z" fill="currentColor" />
          <path d="M16 3a13 13 0 0 0 0 26" fill="currentColor" opacity="0.4" />
          {v > 0 && <circle cx="16" cy="16" r={v === 1 ? 4 : 6} fill="var(--kl-bg, var(--background))" />}
        </>
      );
    case 1: // stacked bars
      return (
        <>
          {[0, 1, 2].map((i) => (
            <rect key={i} x={4 + (v === 1 ? i * 3 : 0)} y={5 + i * 8.5} width={v === 2 ? 24 - i * 6 : 24 - (v === 1 ? i * 3 : 0)} height="5.5" rx="2.75" fill="currentColor" opacity={1 - i * 0.25} />
          ))}
        </>
      );
    case 2: // triangle pair
      return (
        <>
          <path d="M3 27 14 6l11 21Z" fill="currentColor" />
          <path d="M14 27 22 12l8 15Z" fill="currentColor" opacity="0.45" />
        </>
      );
    case 3: // orbit
      return (
        <>
          <circle cx="16" cy="16" r="11.5" fill="none" stroke="currentColor" strokeWidth="3" />
          <circle cx={round(16 + Math.cos(-0.7 - v) * 11.5, 1)} cy={round(16 + Math.sin(-0.7 - v) * 11.5, 1)} r="4.5" fill="currentColor" />
        </>
      );
    case 4: // four petals
      return (
        <g fill="currentColor">
          {[0, 90, 180, 270].map((a, i) => (
            <path key={a} d="M16 16C16 9 20 4 26 4c0 6-5 12-10 12Z" transform={`rotate(${a + v * 15} 16 16)`} opacity={i % 2 ? 0.55 : 1} />
          ))}
        </g>
      );
    case 5: // dot grid
      return (
        <g fill="currentColor">
          {Array.from({ length: 9 }, (_, i) => {
            const x = 6 + (i % 3) * 10;
            const y = 6 + Math.floor(i / 3) * 10;
            const on = i === 4 || (i + v) % 2 === 0;
            return <circle key={i} cx={x} cy={y} r={on ? 3.6 : 2} opacity={on ? 1 : 0.4} />;
          })}
        </g>
      );
    case 6: // wave
      return (
        <>
          <path d="M3 13c4-5 7-5 10 0s6 5 10 0 6-5 6-5" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" />
          <path d="M3 22c4-5 7-5 10 0s6 5 10 0 6-5 6-5" fill="none" stroke="currentColor" strokeWidth="3.4" strokeLinecap="round" opacity="0.45" />
        </>
      );
    case 7: // monogram in a square
      return (
        <>
          <rect x="3" y="3" width="26" height="26" rx={v === 0 ? 7 : v === 1 ? 13 : 2.5} fill="currentColor" />
          <text x="16" y="16" dy="0.35em" textAnchor="middle" fontSize="16" fontWeight="700" fill="var(--kl-bg, var(--background))">
            {letter}
          </text>
        </>
      );
    case 8: // arch
      return (
        <>
          <path d="M5 28V16a11 11 0 0 1 22 0v12h-6V16a5 5 0 0 0-10 0v12Z" fill="currentColor" />
          {v === 1 && <circle cx="16" cy="6" r="2.5" fill="currentColor" opacity="0.5" />}
        </>
      );
    case 9: // diamond fold
      return (
        <>
          <path d="M16 2 30 16 16 30 2 16Z" fill="currentColor" opacity="0.4" />
          <path d="M16 2 30 16 16 30Z" fill="currentColor" />
        </>
      );
    case 10: // sun rays
      return (
        <g fill="currentColor">
          <circle cx="16" cy="16" r="6" />
          {Array.from({ length: 8 }, (_, i) => (
            <rect key={i} x="15" y="2" width="2" height="6" rx="1" transform={`rotate(${i * 45} 16 16)`} opacity={i % 2 ? 0.5 : 1} />
          ))}
        </g>
      );
    default: // interlocking rings
      return (
        <g fill="none" stroke="currentColor" strokeWidth="3.2">
          <circle cx="12" cy="16" r="8" />
          <circle cx="20" cy="16" r="8" opacity="0.5" />
        </g>
      );
  }
}

/** Seeded word treatments, all in the one typeface */
function wordStyle(r: () => number) {
  const variant = Math.floor(r() * 5);
  return {
    weight: [600, 700, 500, 800, 600][variant],
    tracking: ['-0.03em', '-0.045em', '0.08em', '-0.02em', '0.01em'][variant],
    upper: variant === 2,
    lower: variant === 1 || variant === 4,
    dot: variant === 1 || variant === 3,
    bar: variant === 4,
  };
}

/**
 * A placeholder brand from a seed.
 *   <GeneratedLogo name="Brightwell" layout="lockup" tone="accent" size={32} />
 */
export function GeneratedLogo({ name, seed, layout = 'lockup', tone = 'mono', size = 32, animate = false, mark: family, className }: GeneratedLogoProps) {
  const h = hash(seed ?? name);
  const r = random(h);
  r();
  // The family comes from the high bits of the hash, which spread better over twelve than the first random draw
  const kind = family !== undefined ? ((Math.round(family) % 12) + 12) % 12 : (h >>> 20) % 12;
  const color = tone === 'mono' ? 'currentColor' : tone === 'accent' ? 'var(--primary)' : COLORS[Math.floor(r() * COLORS.length)];
  const letter = (name.trim()[0] ?? 'A').toUpperCase();
  const word = wordStyle(r);
  const [ref, inView] = useInView<HTMLSpanElement>();
  const hidden = animate && !inView;

  const mark = (box: number, onTile = false) => (
    <svg
      viewBox="0 0 32 32"
      aria-hidden
      className={cn('shrink-0 transition-[transform,opacity] duration-700 ease-[cubic-bezier(0.34,1.4,0.64,1)] motion-reduce:transition-none', hidden && 'rotate-[-30deg] scale-50 opacity-0')}
      style={{ width: box, height: box, color: onTile ? 'white' : color, ['--kl-bg' as string]: onTile ? color : undefined }}
    >
      {markShapes(kind, random(hash(`${seed ?? name}m`)), letter)}
    </svg>
  );
  // Case is a style, the text itself stays the name, so it reads and copies as written
  const caseClass = word.upper ? 'uppercase' : word.lower ? 'lowercase' : '';
  const wordmark = (fs: number) => (
    <span
      className={cn('relative inline-flex items-baseline whitespace-nowrap leading-none transition-[opacity,transform,filter]', caseClass, ' duration-700 ease-[var(--ease-kit)] motion-reduce:transition-none', hidden && 'translate-y-1 opacity-0 blur-[2px]')}
      style={{ fontSize: fs, fontWeight: word.weight, letterSpacing: word.tracking, color: tone === 'mono' ? 'currentColor' : 'var(--foreground)', transitionDelay: animate ? '150ms' : undefined }}
    >
      {name}
      {word.dot && <span style={{ color, marginLeft: '0.04em' }}>.</span>}
      {word.bar && <span aria-hidden className="absolute -bottom-[0.18em] left-0 h-[0.09em] w-[38%] rounded-full" style={{ background: color }} />}
    </span>
  );

  if (layout === 'app-icon') {
    return (
      <span ref={ref} role="img" aria-label={name} className={cn('relative inline-grid shrink-0 place-items-center overflow-hidden', className)} style={{ width: size, height: size, borderRadius: round(size * 0.24), background: tone === 'mono' ? 'var(--foreground)' : color }}>
        <span aria-hidden className="absolute inset-0 bg-[linear-gradient(160deg,rgb(255_255_255/0.28),transparent_55%)]" />
        <span aria-hidden className="absolute inset-0 rounded-[inherit] shadow-[inset_0_1px_0_rgb(255_255_255/0.35),inset_0_-1px_0_rgb(0_0_0/0.12)]" />
        <span className="relative" style={{ ['--kl-bg' as string]: tone === 'mono' ? 'var(--foreground)' : color }}>
          {mark(round(size * 0.56), true)}
        </span>
      </span>
    );
  }
  if (layout === 'mark') {
    return (
      <span ref={ref} role="img" aria-label={name} className={cn('inline-flex', className)}>
        {mark(size)}
      </span>
    );
  }
  if (layout === 'wordmark') {
    return (
      <span ref={ref} className={cn('inline-flex', className)}>
        {wordmark(round(size * 0.82))}
      </span>
    );
  }
  if (layout === 'stacked') {
    return (
      <span ref={ref} className={cn('inline-flex flex-col items-center', className)} style={{ gap: round(size * 0.28) }}>
        {mark(size)}
        {wordmark(round(size * 0.5))}
      </span>
    );
  }
  return (
    <span ref={ref} className={cn('inline-flex items-center', className)} style={{ gap: round(size * 0.3) }}>
      {mark(size)}
      {wordmark(round(size * 0.62))}
    </span>
  );
}

/** Names that sound like small, believable companies. Combine with a seed for endless placeholders. */
export const PLACEHOLDER_BRANDS = ['Brightwell', 'Fernhall', 'Kestrel', 'Lumora', 'Northbay', 'Oakline', 'Quillo', 'Rivetta', 'Solace', 'Tidewell', 'Veridian', 'Wrenfield', 'Halden', 'Marisol', 'Corvana', 'Ostara'];

Frequent questions

How do I install Logo generator?

With aiw add @ai-website/logo-generator. The command puts components/ui/logo-generator.tsx into your project, the code is yours from then on. It needs no further npm packages.

Which variants does Logo generator have?

5 variants, Marks, Lockups, App icons, Logo cloud and Brand sheet.

Can an AI assistant build with Logo generator?

Yes. Through the MCP server of the aiw CLI, Claude, Cursor, GitHub Copilot, Windsurf, Codex and Gemini find Logo generator with aiw_components and install it with aiw_add @ai-website/logo-generator.

Can I adjust Logo generator without code?

Yes, in the Studio. All 5 variants are editable there, texts, lists and switches. The Studio writes the matching GeneratedLogo code to copy.

Often searched

Jump to

Copied to the clipboard