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.
Props
| Prop | Type | Description |
|---|---|---|
children | string | Text content. Forced to uppercase regardless of casing. Defaults to "BOOM!". |
fontSize | number | Base font size in rem (e.g. 5 -> 5rem). Only the outline thickness scales with it (fontSize * 0.35px); the halftone dot pattern and drop shadows are fixed. Defaults to 5. |
outlineColor | ThemeColor | Thick outline color family. Defaults to "neutral" (near-black via a fixed dark-edge shift). |
dotColor | ThemeColor | Halftone dot color family. Defaults to "danger" (red). |
backgroundFill | ThemeColor | Halftone paper/background color family showing through the dots. Defaults to "warning" (yellow). |
className | string | Extra class name merged onto the container's native class attribute. |
style | StyleObject | Passthrough style merged onto the container. |
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). Direct-source-diff fix (2026-07-05): Missing the comic-style display font — upstream sets a Bangers/Comic-Sans/Impact stack; this rendered in the theme's default sans font. Added the font stack.
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 `drop-shadow()` layers of a
// single `filter` — 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 { type ThemeColor, themeColor } from "@domphy/theme";
import { motion } from "@domphy/ui";
import { fixed } from "../../shared/typography.js";
export interface ComicTextProps {
/** Text content. Forced to uppercase regardless of casing. Defaults to `"BOOM!"`. */
children?: string;
/** Base font size in `rem` (e.g. `5` -> `5rem`). Only the outline thickness scales with it (`fontSize * 0.35px`); the halftone dot pattern and drop shadows are fixed. Defaults to `5`. */
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"` (bright red, upstream's `#EF4444`). */
dotColor?: ThemeColor;
/** Halftone paper/background color family showing through the dots. Defaults to `"highlight"` (bright yellow, upstream's `#FACC15`). */
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();
// Unitless rem value, matching upstream (`${fontSize}rem`). Only the outline
// stroke width is derived from it; dots and shadows are fixed-pixel constants.
const fontSize = props.fontSize ?? 5;
const outlineColor = props.outlineColor ?? "neutral";
const dotColor = props.dotColor ?? "danger";
const backgroundFill = props.backgroundFill ?? "highlight";
const STATIC_SKEW = "skewX(-10deg)";
const element = {
div: text,
style: {
display: "block",
textAlign: "center",
textTransform: "uppercase",
whiteSpace: "pre-wrap",
userSelect: "none",
// 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: () => `${fontSize}rem`,
fontWeight: () => "900",
// A blocky comic display face is the entire premise of this component;
// browsers fall through 'Bangers' (if loaded) to the system 'Impact'/
// 'Comic Sans MS' display fonts before the generic sans-serif.
fontFamily: fixed("'Bangers', 'Comic Sans MS', 'Impact', sans-serif"),
// 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) =>
// Bright yellow paper (upstream `#FACC15`): the family's vivid
// high-chroma step, not the dark mid-ramp step (which read brown).
themeColor(listener, "shift-5", backgroundFill),
backgroundImage: (listener) =>
`radial-gradient(circle at 1px 1px, ${themeColor(listener, "shift-6", dotColor)} 1px, transparent 0)`,
backgroundSize: "8px 8px",
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) =>
`${fontSize * 0.35}px ${themeColor(listener, "shift-17", outlineColor)}`,
// Two stacked drop shadows via `filter` (matching upstream): the larger
// black one is applied first, then the smaller red one on top of it.
// Offsets are fixed 5px/3px and do NOT scale with fontSize. Using
// `filter` drop-shadow (not `text-shadow`) means the shadow silhouette
// includes the thick outline stroke — the layered pop-art sticker look.
filter: (listener) =>
`drop-shadow(5px 5px 0 ${themeColor(listener, "shift-17", outlineColor)}) ` +
`drop-shadow(3px 3px 0 ${themeColor(listener, "shift-6", dotColor)})`,
...(props.style ?? {}),
} as StyleObject,
$: [
motion({
initial: {
opacity: 0,
transform: `${STATIC_SKEW} scale(0.8) rotate(-2deg)`,
},
animate: {
opacity: 1,
transform: `${STATIC_SKEW} scale(1) rotate(0deg)`,
},
transition: {
duration: 600,
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 };