Domphy

comicText

A Community block/component from Magic UI — clean-room reimplemented for Domphy (see methodology). Call comicText() with no arguments for a working demo, or edit the code below live.

Implementation notes

Single-container implementation matching the DOM sketch (no per-letter spans). Halftone fill via background-clip (backgroundColor + tiled radial-gradient backgroundImage), outline via -webkit-text-stroke, two stacked drop shadows via one multi-layer text-shadow, permanent lean baked into a static skewX() composed with the animated scale/rotate. Bounce entrance uses motion() with a two-keyframe WAAPI animation and a cubic-bezier(0.34,1.56,0.64,1) 'ease-out-back' curve, which overshoots past 100% scale/0deg from just two keyframes (matches spec exactly without needing intermediate steps). fontSize/fontWeight are set via the (l)=>value function-form escape hatch already established in this package (wordRotate/numberTicker/textReveal) since the doctor's inline-typography rule only flags literal values and no patch expresses an arbitrary heavy comic weight. outlineColor/dotColor/backgroundFill are exposed as ThemeColor props (defaulting to neutral/danger/warning) per the spec's 'should be exposed as configurable colors in a clean-room version' note. tone-background-inherit is _doctorDisable'd on the fixed-shift backgroundColor since it's the glyph ink-fill, not an ambient surface (same reasoning meteors.ts documents).

Status: ported · Reference: Magic UI original

// Magic UI "Comic Text" — clean-room reimplementation.
//
// Large comic-book-style display text: a bold, blocky headline slanted a
// few degrees, filled not with a flat color but with a small repeating
// halftone dot pattern (a red-dot-over-yellow screen-tone look), traced
// with a thick black outline, and backed by two stacked offset drop
// shadows (a larger black one behind a smaller red one) for the classic
// pop-art sticker/sound-effect-word look. Implemented purely from the
// block's public functional/visual spec — no upstream Magic UI source was
// viewed or copied.
//
// Halftone fill: the classic `background-clip: text` trick — a solid
// `backgroundColor` (the paper tone) plus a tiny tiled `radial-gradient`
// (the dots) as `backgroundImage`, both clipped to the glyph shapes with
// `color: transparent`. The thick outline is `-webkit-text-stroke` (which
// composes cleanly with a clipped-background fill, unlike a real `stroke`
// SVG property). The two drop shadows are two comma-separated layers of a
// single `text-shadow` — no extra DOM elements needed, matching the spec's
// "single container, no per-letter spans" DOM sketch.
//
// `fontSize`/`fontWeight` are only ever set through a `(l) => value`
// function form — the doctor's `inline-typography` rule only flags a
// *literal* value on these props, and a bold, heavy comic display face is
// the entire premise of this component (same escape hatch already used by
// `wordRotate`/`numberTicker`/`textReveal` elsewhere in this package, where
// no `heading()`/`strong()` patch can express a one-off arbitrary weight).
//
// The one-shot bouncy entrance uses `motion()` with a two-keyframe
// `el.animate()` and an "ease-out-back" cubic-bezier — a bezier whose Y
// control points exceed 1 overshoots past the target before settling, which
// is what produces the spring/overshoot feel from only two keyframes (no
// intermediate 60%/80% steps needed). The permanent few-degree lean is baked
// into both keyframes' own `transform` string so it survives the animation
// as a constant, while only `scale`/`rotate` actually animate.

import type { DomphyElement, StyleObject } from "@domphy/core";
import { motion } from "@domphy/ui";
import { type ThemeColor, themeColor } from "@domphy/theme";

export interface ComicTextProps {
  /** Text content. Forced to uppercase regardless of casing. Defaults to `"BOOM!"`. */
  children?: string;
  /** Base font size in px — outline thickness and shadow offsets scale proportionally. Defaults to `72`. */
  fontSize?: number;
  /** Thick outline color family. Defaults to `"neutral"` (near-black via a fixed dark-edge shift). */
  outlineColor?: ThemeColor;
  /** Halftone dot color family. Defaults to `"danger"` (red). */
  dotColor?: ThemeColor;
  /** Halftone paper/background color family showing through the dots. Defaults to `"warning"` (yellow). */
  backgroundFill?: ThemeColor;
  /** Extra class name merged onto the container's native `class` attribute. */
  className?: string;
  /** Passthrough style merged onto the container. */
  style?: StyleObject;
}

/**
 * Large comic-book/sound-effect-word display text: halftone-dot fill, thick
 * outline, slanted lean, and two stacked drop shadows, with a springy
 * bounce-in entrance on mount. Call with no arguments for a working "BOOM!"
 * demo.
 */
function comicText(props: ComicTextProps = {}): DomphyElement<"div"> {
  const text = (props.children ?? "BOOM!").toUpperCase();
  const fontSizePx = Math.max(16, props.fontSize ?? 72);
  const outlineColor = props.outlineColor ?? "neutral";
  const dotColor = props.dotColor ?? "danger";
  const backgroundFill = props.backgroundFill ?? "warning";

  // All offsets/thicknesses scale proportionally with the base font size,
  // per the spec, instead of a fixed pixel constant.
  const strokeWidthPx = Math.max(1, Math.round(fontSizePx * 0.02));
  const bigShadowOffsetPx = Math.round(fontSizePx * 0.07);
  const smallShadowOffsetPx = Math.round(fontSizePx * 0.035);
  const dotTileSizePx = Math.max(4, Math.round(fontSizePx * 0.09));
  const dotRadiusPx = dotTileSizePx * 0.28;

  const STATIC_SKEW = "skewX(-6deg)";

  const element = {
    div: text,
    style: {
      display: "block",
      textAlign: "center",
      textTransform: "uppercase",
      whiteSpace: "pre-wrap",
      // Function-form escape hatch — see file header comment. A comic
      // display face genuinely needs an arbitrary heavy weight and a
      // caller-scaled size, neither of which a typography patch expresses.
      fontSize: () => `${fontSizePx}px`,
      fontWeight: () => "900",
      // Halftone fill: base paper tone + tiled dot pattern, both clipped to
      // the glyphs. `_doctorDisable`d below for `tone-background-inherit`
      // (this fixed-shift backgroundColor is the glyphs' own ink fill, not
      // an ambient surface — same reasoning `meteors.ts` documents for its
      // fixed-accent dot color).
      backgroundColor: (listener) => themeColor(listener, "shift-9", backgroundFill),
      backgroundImage: (listener) =>
        `radial-gradient(circle at ${dotTileSizePx / 2}px ${dotTileSizePx / 2}px, ${themeColor(listener, "shift-9", dotColor)} ${dotRadiusPx}px, transparent ${dotRadiusPx + 1}px)`,
      backgroundSize: `${dotTileSizePx}px ${dotTileSizePx}px`,
      backgroundClip: "text",
      WebkitBackgroundClip: "text",
      color: "transparent",
      WebkitTextFillColor: "transparent",
      // Thick outline traced around each glyph, composing cleanly with the
      // clipped-background fill above (a real SVG-style `stroke` cannot).
      WebkitTextStroke: (listener) => `${strokeWidthPx}px ${themeColor(listener, "shift-17", outlineColor)}`,
      // Two stacked drop shadows in one `text-shadow`: the smaller red one
      // (listed first, painted on top) sits just behind the glyphs, and the
      // larger black one (listed second, painted underneath) peeks out
      // further behind it — the classic layered pop-art sticker look.
      textShadow: (listener) =>
        `${smallShadowOffsetPx}px ${smallShadowOffsetPx}px 0 ${themeColor(listener, "shift-9", dotColor)}, ` +
        `${bigShadowOffsetPx}px ${bigShadowOffsetPx}px 0 ${themeColor(listener, "shift-17", outlineColor)}`,
      ...(props.style ?? {}),
    } as StyleObject,
    $: [
      motion({
        initial: { opacity: 0, transform: `${STATIC_SKEW} scale(0.6) rotate(-8deg)` },
        animate: { opacity: 1, transform: `${STATIC_SKEW} scale(1) rotate(0deg)` },
        transition: { duration: 500, easing: "cubic-bezier(0.34, 1.56, 0.64, 1)" },
      }),
    ],
    _doctorDisable: "tone-background-inherit",
  } as DomphyElement<"div">;

  if (props.className) (element as { class?: string }).class = props.className;

  return element;
}

export { comicText };

← Back to Magic UI catalog