Takazudo Modular Styleguide

Shared/MobileMenuToggle

Closed
Comparison
Default
Interactive
Open
With Aria Label

Component Source

mobile-menu-toggle.tsx

interface MobileMenuToggleProps {
  isOpen: boolean;
  onToggle: () => void;
  className?: string;
  'aria-label'?: string;
}

const MobileMenuToggle = ({
  isOpen,
  onToggle,
  className = '',
  'aria-label': ariaLabel,
}: MobileMenuToggleProps) => {
  return (
    <button
      type="button"
      onClick={onToggle}
      className={`zd-invert-color-link cursor-pointer relative z-50 flex w-[40px] h-[40px] flex-col items-center justify-center focus:outline-none text-zd-white ${className}`}
      aria-expanded={isOpen}
      aria-label={ariaLabel || 'Toggle menu'}
    >
      <span className="sr-only">{isOpen ? 'Close menu' : 'Open menu'}</span>
      <div className="relative flex h-[20px] w-[28px] flex-col items-center justify-center">
        <span
          className={`absolute h-[2px] w-[28px] transform bg-current transition-all duration-300 ease-in-out ${isOpen ? 'translate-y-0 rotate-45' : '-translate-y-[8px]'}`}
        />
        <span
          className={`absolute h-[2px] w-[28px] transform bg-current transition-all duration-300 ease-in-out ${isOpen ? 'scale-0 opacity-0' : ''}`}
        />
        <span
          className={`absolute h-[2px] w-[28px] transform bg-current transition-all duration-300 ease-in-out ${isOpen ? 'translate-y-0 -rotate-45' : 'translate-y-[8px]'}`}
        />
      </div>
    </button>
  );
};

export default MobileMenuToggle;

Story Source

mobile-menu-toggle.stories.tsx

import { useState } from 'preact/hooks';
import MobileMenuToggle from './mobile-menu-toggle';
import type { ControlsMap } from '../../sub-packages/styleguide-v2/src/data/control-types';

/**
 * MobileMenuToggle Component
 *
 * An animated hamburger menu toggle button that transforms
 * into an X when open.
 *
 * Features:
 * - Smooth CSS animation between open/closed states
 * - Accessible with aria-expanded and aria-label
 * - Screen reader text for state changes
 */
export const meta = {
  title: 'Shared/MobileMenuToggle',
};

export const controls: ControlsMap = {
  isOpen: { type: 'boolean', default: false },
};

/**
 * Interactive wrapper to demonstrate toggle behavior
 */
const InteractiveToggle = () => {
  const [isOpen, setIsOpen] = useState(false);

  return (
    <div className="flex flex-col items-center gap-vgap-md">
      <MobileMenuToggle isOpen={isOpen} onToggle={() => setIsOpen(!isOpen)} />
      <p className="text-zd-white text-sm">
        State: <span className="font-bold">{isOpen ? 'Open (X)' : 'Closed (≡)'}</span>
      </p>
      <p className="text-muted text-xs">Click to toggle</p>
    </div>
  );
};

/**
 * Default - Static state controlled by props panel
 */
export const Default = {
  args: { isOpen: false },
  render: ({ isOpen }: { isOpen: boolean }) => (
    <MobileMenuToggle isOpen={isOpen} onToggle={() => {}} />
  ),
};

/**
 * Interactive toggle
 */
export const Interactive = () => <InteractiveToggle />;

/**
 * Closed state (hamburger icon)
 */
export const Closed = () => <MobileMenuToggle isOpen={false} onToggle={() => {}} />;

/**
 * Open state (X icon)
 */
export const Open = () => <MobileMenuToggle isOpen={true} onToggle={() => {}} />;

/**
 * Side by side comparison
 */
export const Comparison = () => (
  <div className="flex items-center gap-hgap-lg">
    <div className="flex flex-col items-center gap-vgap-xs">
      <MobileMenuToggle isOpen={false} onToggle={() => {}} />
      <span className="text-muted text-xs">Closed</span>
    </div>
    <div className="flex flex-col items-center gap-vgap-xs">
      <MobileMenuToggle isOpen={true} onToggle={() => {}} />
      <span className="text-muted text-xs">Open</span>
    </div>
  </div>
);

/**
 * With custom aria-label
 */
export const WithAriaLabel = () => (
  <MobileMenuToggle isOpen={false} onToggle={() => {}} aria-label="ナビゲーションメニューを開く" />
);