Taxonomy/ArrowTag
Default
Large
Large Group
Large Linked
Size Comparison
Small
Small Group
arrow-tag.tsx
/**
* Size presets for arrow polygon clip-paths and spacing.
* sm: compact tags for list items
* lg: larger tags for article detail page headers
*/
import type { FunctionComponent, ComponentChildren } from 'preact';
const SIZE_STYLES = {
sm: {
borderClip: '[clip-path:polygon(0_0,calc(100%-6px)_0,100%_50%,calc(100%-6px)_100%,0_100%)]',
contentClip:
'[clip-path:polygon(1px_1px,calc(100%-7px)_1px,calc(100%-1px)_50%,calc(100%-7px)_calc(100%-1px),1px_calc(100%-1px))]',
contentSize: 'text-[0.65rem] lg:text-xs pl-[6px] pr-[12px] pt-[.2em] pb-[.2em]',
},
lg: {
borderClip: '[clip-path:polygon(0_0,calc(100%-16px)_0,100%_50%,calc(100%-16px)_100%,0_100%)]',
contentClip:
'[clip-path:polygon(1px_1px,calc(100%-17px)_1px,calc(100%-1px)_50%,calc(100%-17px)_calc(100%-1px),1px_calc(100%-1px))]',
contentSize: 'text-sm lg:text-base pl-[8px] pr-[22px] pt-[.25em] pb-[.25em]',
},
};
/**
* Renders label text with parenthesized portions in smaller font.
* Matches both full-width (...) and half-width (...) parentheses.
*/
function FormattedLabel({ label }: { label: string }) {
const parts = label.split(/(([^)]*)|\([^)]*\))/);
if (parts.length === 1) return <>{label}</>;
return (
<>
{parts.map((part, i) =>
/^[((]/.test(part) ? (
<span key={i} className="text-[.7em]">
{part}
</span>
) : (
<span key={i}>{part}</span>
),
)}
</>
);
}
interface ArrowTagProps {
label: string;
size?: 'sm' | 'lg';
suffix?: ComponentChildren;
active?: boolean;
}
/**
* Arrow polygon tag shape with bordered clip-path effect.
* Renders the visual tag element — callers handle wrapper (li, a, etc.)
*
* Hover/active inversion is self-contained via group-hover/group-active.
* Parent element must have `group` class for hover to work.
*/
export const ArrowTag: FunctionComponent<ArrowTagProps> = ({
label,
size = 'sm',
suffix,
active,
}) => {
const styles = SIZE_STYLES[size];
return (
<span className="relative inline-flex">
{/* Border layer */}
<span
className={`absolute inset-0 zd-tag-border ${active ? 'bg-zd-literal-black' : 'bg-zd-literal-white'} group-hover:bg-zd-literal-black group-active:!bg-zd-active ${styles.borderClip}`}
/>
{/* Content layer with inset clip for border effect */}
<span
className={`relative inline-flex items-center font-thin no-underline text-shadow-none zd-tag-content ${active ? 'text-zd-literal-black bg-zd-literal-white' : 'text-zd-literal-white bg-zd-literal-black'} group-hover:text-zd-literal-black group-hover:bg-zd-literal-white group-active:!bg-zd-active ${styles.contentSize} ${styles.contentClip}`}
>
<span className="zd-hash">#</span>
<span>
<FormattedLabel label={label} />
{suffix}
</span>
</span>
</span>
);
};
arrow-tag.stories.tsx
import { ArrowTag } from './arrow-tag';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';
/**
* ArrowTag Component
*
* Arrow polygon tag shape with bordered clip-path effect.
* Two size variants: sm (list items) and lg (detail page headers).
* Callers handle wrapper elements (li, a, etc.)
*/
export const meta = {
title: 'Taxonomy/ArrowTag',
};
export const controls: ControlsMap = {
label: { type: 'text', default: 'Modular Synthesizer' },
size: { type: 'select', default: 'sm', options: ['sm', 'lg'] },
};
/**
* Default - controllable via props panel
*/
export const Default = {
args: { label: 'Modular Synthesizer', size: 'sm' },
render: ({ label, size }: { label: string; size: 'sm' | 'lg' }) => (
<ArrowTag label={label} size={size} />
),
};
/**
* Small size — used in article list items
*/
export const Small = () => <ArrowTag label="Modular Synthesizer" size="sm" />;
/**
* Large size — used in article detail page headers
*/
export const Large = () => <ArrowTag label="Modular Synthesizer" size="lg" />;
/**
* Small tags in a row — simulates list item tag display
*/
export const SmallGroup = () => (
<ul className="inline">
{['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
<li key={label} className="inline-flex mr-[6px] mb-[4px] whitespace-nowrap">
<ArrowTag label={label} size="sm" />
</li>
))}
</ul>
);
/**
* Large tags in a row — simulates detail page header tag display
*/
export const LargeGroup = () => (
<ul className="inline">
{['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
<li key={label} className="inline-flex mr-[8px] mb-[8px] whitespace-nowrap">
<ArrowTag label={label} size="lg" />
</li>
))}
</ul>
);
/**
* Large tags wrapped in links — simulates detail page with hover/active states
*/
export const LargeLinked = () => (
<ul className="inline">
{['Modular Synthesizer', '電源', 'DIY', 'Case(ケース)'].map((label) => (
<li key={label} className="inline-flex mr-[8px] mb-[8px] whitespace-nowrap">
<a className="group" href="#">
<ArrowTag label={label} size="lg" />
</a>
</li>
))}
</ul>
);
/**
* Size comparison — sm vs lg side by side
*/
export const SizeComparison = () => (
<div className="flex flex-col gap-vgap-md">
<div>
<div className="text-xs text-zd-gray pb-vgap-xs">size="sm"</div>
<ArrowTag label="Modular Synthesizer" size="sm" />
</div>
<div>
<div className="text-xs text-zd-gray pb-vgap-xs">size="lg"</div>
<ArrowTag label="Modular Synthesizer" size="lg" />
</div>
</div>
);