First CH Designs
BLK-43Blocks

Job Requirements Sheet

A definition-list job posting sheet with a vertical role selector. Nine rows (salary, location, hours, holidays, benefits, requirements, hiring process) are set with hairline rules only; each term pairs a Japanese label with a lowercase English sub-label. Values are limited to three shapes: a sentence, a bulleted list, and a numbered process. No rounded corners, no arrow links, and a single application CTA right after the table. Zero images, zero dependencies.

Added:
2026-08-27
Dependencies:
None
tags
#recruit #jobs #definition-list #vertical-tabs #table #no-image #no-dependency #a11y

Preview

募集要項

職種を選ぶと、下の求人票がその職種の条件に入れ替わります。記載している金額・条件はすべて架空のサンプルです。

最終更新2026.08.27

募集中の職種

Webディレクター

正社員
想定年収420万〜600万円
勤務地フルリモート可
募集人数1〜2名
仕事内容role

要件の整理から公開後の運用改善まで、1案件を通して担当します。進行管理だけでなく、何を作るかを決める段階から入るポジションです

雇用形態employment

正社員(試用期間3か月・期間中の条件変更なし)

給与salary

月給 280,000円〜420,000円(経験・スキルを考慮のうえ決定)/賞与 年2回

勤務地location

フルリモート可(国内在住であれば居住地は不問)/月1回の集まりは任意参加

勤務時間hours

フレックスタイム制(コアタイム 11:00〜15:00)/標準労働時間 1日7時間45分

休日・休暇holidays
  • 完全週休2日(土・日・祝)
  • 年末年始・夏季休暇
  • 有給休暇(入社月から付与・初年度10日)
  • 慶弔休暇/看護・介護休暇
待遇・福利厚生benefits
  • 各種社会保険(健康・厚生年金・雇用・労災)
  • 在宅勤務手当 月10,000円
  • 書籍・受講料の全額補助(事前申請不要)
  • 作業環境の購入補助(3年ごと・上限20万円)
  • 健康診断 年1回
応募資格requirements
  • Web制作の進行管理または要件定義の実務経験2年以上
  • お客さまと直接やり取りした経験
  • 決めたことを文章で残すのが苦にならない方
選考フローprocess
  1. 書類選考
  2. カジュアル面談(オンライン30分)
  3. 実務課題(有償・3時間程度)
  4. 最終面談
この職種に応募する

応募の前に条件だけ確かめたい、という連絡も受け付けています(返信は3営業日以内)

"use client";

import { useId, useRef, useState } from "react";
import { Montserrat } from "next/font/google";

const montserrat = Montserrat({
  weight: ["500", "600", "700"],
  subsets: ["latin"],
  display: "swap",
});

/**
 * 募集要項(定義リスト型求人票・BLK-43)
 *
 * 差別化: 既存の採用ブロック(BLK-14)は人の気配とキャッチコピーで惹きつけるティーザーで、
 * 募集要項は4行の要約に留まる。本ブロックは職種を切り替えて9行の求人票そのものを読ませる
 * 高密度な「書類」で、既存のタブ型(BLK-35 / BLK-37)が横並びの下線タブなのに対し、
 * こちらは職種一覧そのものが縦のセレクタになる。掲載内容はすべて架空のサンプル。
 *
 * 意図的に外した型(ギャラリー全体で飽和していたもの):
 * - 英字キッカー(大文字+広い字間)を置かない。英字は dt の従属ラベルとして小文字・
 *   字間そのままで敷き、和文ラベルの下に沈める(見出しの装飾ではなく列の目印にする)
 * - 角丸をゼロにする(バッジもCTAも直角)。印刷された求人票の面を作る
 * - 行末の矢印リンクを使わない。選考フローは矢印ではなく 01〜04 の番号付き ol で組む
 * - スクロール連動の出現アニメーションを持たない。書類は最初から読める状態で置く
 *
 * 汎用技法メモ(web-design-playbook 還流用):
 *
 * 1. 縦タブ(aria-orientation="vertical")は「一覧そのもの」を操作子にできる
 *    横タブはラベルを短く切り詰める必要があるが、縦なら1項目に2行使える。
 *    職種名+雇用形態/想定年収を1行ずつ置けるので、タブ列が求人一覧として単体で読める。
 *    キー操作は ArrowUp/Down(+Home/End)に入れ替える。横タブのまま ArrowLeft/Right を
 *    残すと、視覚の並びと操作方向がズレて支援技術の利用者が迷う。
 *
 * 2. 定義リストの dt は「和文(主)+英字(従)」の2段にすると列幅が安定する
 *    和文ラベルは2〜6文字で幅が倍以上ぶれるが、下に固定幅の英字を敷くと列の見た目の幅が
 *    そろう。英字を大文字+広字間にすると装飾に見えるので、小文字・既定の字間に留める。
 *
 * 3. スキーマを固定したまま行の意味だけ差し替える
 *    正社員・業務委託・パートタイムで「給与/報酬/時給」「休日/稼働日」と項目名が変わる。
 *    行ラベルを共通定数に固定せず職種ごとに持たせると、同じ組み方のまま雇用形態の違いを
 *    正しく書ける(表の骨格が同じなので、切り替えても読み手の視線移動は変わらない)。
 *
 * 4. 値の型を text / list / steps の3つに絞る
 *    求人票の中身は「1文」「箇条書き」「順序のある工程」しか出てこない。
 *    3種に閉じてレンダラを分けると、行を足しても組版が崩れない。
 *
 * 5. 切替のアニメーションは key 付け替え+ CSS animation、停止は同じ <style> 内で
 *    transition は切替直後の1フレームを取りこぼす。パネルに key を与えて再マウントし
 *    animation を1本当てる。DOM末尾の <style> は head のユーティリティより後勝ちするので、
 *    reduced-motion の打ち消しも同じ <style> 内に !important で置く。
 */

type Row = {
  label: string;
  /** dt の従属ラベル(小文字・列の目印) */
  en: string;
  kind: "text" | "list" | "steps";
  value: string | string[];
  /** 給与など主要な行だけ濃く出す */
  strong?: boolean;
};

type Job = {
  id: string;
  title: string;
  employment: string;
  incomeLabel: string;
  income: string;
  headcount: string;
  place: string;
  rows: Row[];
};

const EMPLOYEE_HOLIDAYS: Row = {
  label: "休日・休暇",
  en: "holidays",
  kind: "list",
  value: [
    "完全週休2日(土・日・祝)",
    "年末年始・夏季休暇",
    "有給休暇(入社月から付与・初年度10日)",
    "慶弔休暇/看護・介護休暇",
  ],
};

const EMPLOYEE_BENEFITS: Row = {
  label: "待遇・福利厚生",
  en: "benefits",
  kind: "list",
  value: [
    "各種社会保険(健康・厚生年金・雇用・労災)",
    "在宅勤務手当 月10,000円",
    "書籍・受講料の全額補助(事前申請不要)",
    "作業環境の購入補助(3年ごと・上限20万円)",
    "健康診断 年1回",
  ],
};

const JOBS: Job[] = [
  {
    id: "director",
    title: "Webディレクター",
    employment: "正社員",
    incomeLabel: "想定年収",
    income: "420万〜600万円",
    headcount: "1〜2名",
    place: "フルリモート可",
    rows: [
      {
        label: "仕事内容",
        en: "role",
        kind: "text",
        value:
          "要件の整理から公開後の運用改善まで、1案件を通して担当します。進行管理だけでなく、何を作るかを決める段階から入るポジションです",
      },
      {
        label: "雇用形態",
        en: "employment",
        kind: "text",
        value: "正社員(試用期間3か月・期間中の条件変更なし)",
      },
      {
        label: "給与",
        en: "salary",
        kind: "text",
        strong: true,
        value: "月給 280,000円〜420,000円(経験・スキルを考慮のうえ決定)/賞与 年2回",
      },
      {
        label: "勤務地",
        en: "location",
        kind: "text",
        value: "フルリモート可(国内在住であれば居住地は不問)/月1回の集まりは任意参加",
      },
      {
        label: "勤務時間",
        en: "hours",
        kind: "text",
        value: "フレックスタイム制(コアタイム 11:00〜15:00)/標準労働時間 1日7時間45分",
      },
      EMPLOYEE_HOLIDAYS,
      EMPLOYEE_BENEFITS,
      {
        label: "応募資格",
        en: "requirements",
        kind: "list",
        value: [
          "Web制作の進行管理または要件定義の実務経験2年以上",
          "お客さまと直接やり取りした経験",
          "決めたことを文章で残すのが苦にならない方",
        ],
      },
      {
        label: "選考フロー",
        en: "process",
        kind: "steps",
        value: ["書類選考", "カジュアル面談(オンライン30分)", "実務課題(有償・3時間程度)", "最終面談"],
      },
    ],
  },
  {
    id: "frontend",
    title: "フロントエンドエンジニア",
    employment: "正社員",
    incomeLabel: "想定年収",
    income: "450万〜680万円",
    headcount: "1名",
    place: "フルリモート可",
    rows: [
      {
        label: "仕事内容",
        en: "role",
        kind: "text",
        value:
          "コーポレートサイト・LPの実装と、公開後の改善を担当します。実装の判断理由をその都度ことばにして残す進め方をとっています",
      },
      {
        label: "雇用形態",
        en: "employment",
        kind: "text",
        value: "正社員(試用期間3か月・期間中の条件変更なし)",
      },
      {
        label: "給与",
        en: "salary",
        kind: "text",
        strong: true,
        value: "月給 300,000円〜470,000円(経験・スキルを考慮のうえ決定)/賞与 年2回",
      },
      {
        label: "勤務地",
        en: "location",
        kind: "text",
        value: "フルリモート可(国内在住であれば居住地は不問)/月1回の集まりは任意参加",
      },
      {
        label: "勤務時間",
        en: "hours",
        kind: "text",
        value: "フレックスタイム制(コアタイム 11:00〜15:00)/標準労働時間 1日7時間45分",
      },
      EMPLOYEE_HOLIDAYS,
      EMPLOYEE_BENEFITS,
      {
        label: "応募資格",
        en: "requirements",
        kind: "list",
        value: [
          "HTML・CSS・JavaScript での実装経験3年以上",
          "React など、コンポーネント指向のフレームワークでの実務経験",
          "キーボード操作・代替テキストなど、アクセシビリティへの配慮ができる方",
        ],
      },
      {
        label: "選考フロー",
        en: "process",
        kind: "steps",
        value: ["書類選考", "カジュアル面談(オンライン30分)", "技術面談(コードを一緒に読む形式)", "最終面談"],
      },
    ],
  },
  {
    id: "designer",
    title: "Webデザイナー",
    employment: "業務委託",
    incomeLabel: "想定報酬",
    income: "1案件 250,000円〜",
    headcount: "2名",
    place: "稼働場所は不問",
    rows: [
      {
        label: "仕事内容",
        en: "role",
        kind: "text",
        value:
          "コーポレートサイト・LPのデザインを、参考サイトの読み解きから担当します。1案件あたりの本数を絞り、直す時間をあらかじめ確保しています",
      },
      {
        label: "契約形態",
        en: "contract",
        kind: "text",
        value: "業務委託(3か月ごとの更新・案件単位のご依頼も可)",
      },
      {
        label: "報酬",
        en: "fee",
        kind: "text",
        strong: true,
        value: "1案件 250,000円〜(規模・ページ数により決定)/お支払いは検収の翌月末",
      },
      {
        label: "稼働場所",
        en: "location",
        kind: "text",
        value: "不問(打ち合わせはオンライン・録画を残すので同席は必須ではありません)",
      },
      {
        label: "稼働日・時間",
        en: "working days",
        kind: "text",
        value: "相談のうえ決定(土日・祝日の稼働は求めません)/連絡は平日 10:00〜18:00",
      },
      {
        label: "支給・補助",
        en: "support",
        kind: "list",
        value: [
          "デザインツールのライセンス費用を負担",
          "素材・フォントの購入費を実費精算",
          "作業用のアカウントは当社側で用意",
        ],
      },
      {
        label: "応募資格",
        en: "requirements",
        kind: "list",
        value: [
          "Webサイトのデザイン実務経験2年以上",
          "実装を前提とした設計ができる方(余白・状態・崩れ方まで決められること)",
          "制作物3点以上のポートフォリオ",
        ],
      },
      {
        label: "選考フロー",
        en: "process",
        kind: "steps",
        value: ["ポートフォリオ選考", "オンライン面談(30分)", "トライアル案件1本(有償)"],
      },
    ],
  },
  {
    id: "support",
    title: "運用サポート",
    employment: "パートタイム",
    incomeLabel: "想定時給",
    income: "1,400円〜1,800円",
    headcount: "1名",
    place: "フルリモート可",
    rows: [
      {
        label: "仕事内容",
        en: "role",
        kind: "text",
        value:
          "公開後のサイトの更新代行と、お問い合わせの一次対応を担当します。手順が定まっていない作業をマニュアルに直していく仕事でもあります",
      },
      {
        label: "雇用形態",
        en: "employment",
        kind: "text",
        value: "パートタイム(週3日〜・試用期間1か月)",
      },
      {
        label: "時給",
        en: "hourly wage",
        kind: "text",
        strong: true,
        value: "1,400円〜1,800円(経験を考慮のうえ決定)/交通費は実費支給",
      },
      {
        label: "勤務地",
        en: "location",
        kind: "text",
        value: "フルリモート可(初月のみ、週1回のオンライン同席をお願いします)",
      },
      {
        label: "勤務時間",
        en: "hours",
        kind: "text",
        value: "週3日〜(1日4時間以上)/10:00〜18:00 の間で相談",
      },
      {
        label: "休日・休暇",
        en: "holidays",
        kind: "list",
        value: ["シフト制(土・日・祝は休み)", "年末年始・夏季休暇", "有給休暇(法定どおり付与)"],
      },
      {
        label: "待遇・福利厚生",
        en: "benefits",
        kind: "list",
        value: [
          "雇用保険・労災保険(勤務時間により社会保険)",
          "在宅勤務手当 月5,000円",
          "書籍・受講料の全額補助",
        ],
      },
      {
        label: "応募資格",
        en: "requirements",
        kind: "list",
        value: [
          "パソコンでの事務作業に慣れている方",
          "決まった手順どおりに進められる方(未経験可)",
          "平日日中に連絡が取れる方",
        ],
      },
      {
        label: "選考フロー",
        en: "process",
        kind: "steps",
        value: ["書類選考", "オンライン面談(30分)"],
      },
    ],
  },
];

/** 求人票の値。text / list / steps の3種だけに閉じる */
function RowValue({ row }: { row: Row }) {
  if (row.kind === "text") {
    return (
      <p
        className={`text-[13.5px] leading-[1.95] ${
          row.strong ? "font-semibold text-gray-900" : "text-gray-700"
        }`}
      >
        {row.value as string}
      </p>
    );
  }

  if (row.kind === "list") {
    return (
      <ul role="list" className="flex flex-col gap-1.5">
        {(row.value as string[]).map((item) => (
          <li key={item} className="flex gap-2 text-[13.5px] leading-[1.85] text-gray-700">
            <span aria-hidden="true" className="mt-[0.7em] block h-px w-2.5 shrink-0 bg-amber-600" />
            <span>{item}</span>
          </li>
        ))}
      </ul>
    );
  }

  return (
    <ol className="flex flex-col gap-2">
      {(row.value as string[]).map((item, index) => (
        <li key={item} className="flex items-baseline gap-2.5 text-[13.5px] leading-[1.85] text-gray-700">
          <span
            aria-hidden="true"
            className={`${montserrat.className} shrink-0 text-[11px] font-bold text-amber-600 tabular-nums`}
          >
            {String(index + 1).padStart(2, "0")}
          </span>
          <span>{item}</span>
        </li>
      ))}
    </ol>
  );
}

export default function RecruitRequirementsTable() {
  const uid = useId();
  const tabRefs = useRef<(HTMLButtonElement | null)[]>([]);
  const [activeId, setActiveId] = useState(JOBS[0].id);

  const activeIndex = JOBS.findIndex((job) => job.id === activeId);
  const job = JOBS[activeIndex];

  // 縦タブなので上下キーで移動する(横タブの左右キーを残さない)
  const onKeyDown = (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
    const last = JOBS.length - 1;
    let next: number | null = null;
    if (event.key === "ArrowDown") next = index === last ? 0 : index + 1;
    else if (event.key === "ArrowUp") next = index === 0 ? last : index - 1;
    else if (event.key === "Home") next = 0;
    else if (event.key === "End") next = last;
    if (next === null) return;
    event.preventDefault();
    setActiveId(JOBS[next].id);
    tabRefs.current[next]?.focus();
  };

  return (
    <section
      aria-labelledby={`${uid}-title`}
      className="@container w-full max-w-4xl bg-white px-1 text-gray-900"
    >
      <header className="@2xl:flex @2xl:items-end @2xl:justify-between @2xl:gap-10">
        <div>
          <h2
            id={`${uid}-title`}
            className="text-[clamp(1.5rem,3.4cqw,2.05rem)] leading-[1.35] font-bold tracking-tight"
          >
            募集要項
          </h2>
          <p className="mt-4 max-w-[34rem] text-[13.5px] leading-[1.95] font-medium text-gray-600">
            職種を選ぶと、下の求人票がその職種の条件に入れ替わります。記載している金額・条件はすべて架空のサンプルです。
          </p>
        </div>
        <p className="mt-5 flex shrink-0 items-baseline gap-2 text-[11px] font-semibold text-gray-400 @2xl:mt-0">
          <span className="text-gray-500">最終更新</span>
          <span className={`${montserrat.className} tabular-nums`}>2026.08.27</span>
        </p>
      </header>

      <div className="mt-9 grid gap-y-8 @2xl:grid-cols-[14rem_1fr] @2xl:gap-x-9">
        {/* 職種セレクタ(一覧そのものを操作子にする・縦) */}
        <div>
          <p className="text-[11.5px] font-bold text-gray-500">募集中の職種</p>
          <div
            role="tablist"
            aria-orientation="vertical"
            aria-label="募集中の職種"
            className="mt-3 border-t border-l border-gray-200"
          >
            {JOBS.map((item, index) => {
              const selected = item.id === activeId;
              return (
                <button
                  key={item.id}
                  ref={(node) => {
                    tabRefs.current[index] = node;
                  }}
                  type="button"
                  role="tab"
                  id={`${uid}-tab-${item.id}`}
                  aria-selected={selected}
                  aria-controls={`${uid}-panel`}
                  tabIndex={selected ? 0 : -1}
                  onClick={() => setActiveId(item.id)}
                  onKeyDown={(event) => onKeyDown(event, index)}
                  className="relative block w-full border-b border-gray-200 py-3.5 pr-3 pl-4 text-left transition-colors duration-200 hover:bg-amber-50/60 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-amber-600 motion-reduce:transition-none"
                >
                  <span
                    aria-hidden="true"
                    className={`absolute inset-y-0 -left-px block w-[3px] origin-top bg-amber-600 transition-transform duration-300 ease-out motion-reduce:transition-none ${
                      selected ? "scale-y-100" : "scale-y-0"
                    }`}
                  />
                  <span
                    className={`block text-[13.5px] leading-[1.5] font-bold transition-colors duration-200 motion-reduce:transition-none ${
                      selected ? "text-gray-900" : "text-gray-500"
                    }`}
                  >
                    {item.title}
                  </span>
                  <span className="mt-1 block text-[11px] leading-[1.5] text-gray-400">
                    {item.employment}/{item.headcount}
                  </span>
                </button>
              );
            })}
          </div>
        </div>

        {/* 求人票(key の付け替えで頭から再生される) */}
        <div
          key={activeId}
          role="tabpanel"
          id={`${uid}-panel`}
          aria-labelledby={`${uid}-tab-${activeId}`}
          tabIndex={-1}
          className="jobsheet-anim border-t-2 border-gray-900 pt-5 [animation:jobsheet-in_420ms_ease-out_both] focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
        >
          <div className="flex flex-wrap items-baseline justify-between gap-x-4 gap-y-2">
            <h3 className="text-[clamp(1.15rem,2.6cqw,1.4rem)] leading-[1.4] font-bold">
              {job.title}
            </h3>
            <span className="border border-gray-300 px-2.5 py-1 text-[11px] leading-none font-semibold text-gray-600">
              {job.employment}
            </span>
          </div>

          {/* 要点3つ(表に入る前に読ませる) */}
          <div className="mt-5 grid border-t border-gray-200 @md:grid-cols-3">
            {[
              { label: job.incomeLabel, value: job.income, accent: true },
              { label: "勤務地", value: job.place, accent: false },
              { label: "募集人数", value: job.headcount, accent: false },
            ].map((cell, index) => (
              <div
                key={cell.label}
                className={`py-3.5 @md:px-4 @md:first:pl-0 @md:last:pr-0 ${
                  index === 0 ? "" : "border-t border-gray-200 @md:border-t-0 @md:border-l"
                }`}
              >
                <span className="block text-[11px] font-semibold text-gray-500">{cell.label}</span>
                <span
                  className={`mt-1 block text-[15px] leading-[1.5] font-bold tabular-nums ${
                    cell.accent ? "text-amber-700" : "text-gray-900"
                  }`}
                >
                  {cell.value}
                </span>
              </div>
            ))}
          </div>

          {/* 定義リスト本体(dt は 和文+小文字英字の2段) */}
          <dl className="border-b border-gray-200">
            {job.rows.map((row, index) => (
              <div
                key={row.label}
                style={{ animationDelay: `${120 + index * 40}ms` }}
                className="jobsheet-anim grid gap-y-2 border-t border-gray-200 py-4 [animation:jobsheet-in_380ms_ease-out_both] @md:grid-cols-[7.5rem_1fr] @md:gap-x-6 @md:py-5"
              >
                <dt>
                  <span className="block text-[12.5px] leading-[1.5] font-bold text-gray-900">
                    {row.label}
                  </span>
                  <span
                    className={`${montserrat.className} mt-0.5 block text-[10px] leading-none font-medium text-gray-400`}
                  >
                    {row.en}
                  </span>
                </dt>
                <dd>
                  <RowValue row={row} />
                </dd>
              </div>
            ))}
          </dl>

          {/* 応募CTAは表の直後に1本だけ(直角・矢印なし) */}
          <div className="mt-8 flex flex-col gap-y-3.5 @md:flex-row @md:items-center @md:gap-x-6">
            <a
              href="#"
              className="inline-flex shrink-0 items-center justify-center bg-amber-600 px-8 py-4 text-[14px] leading-none font-bold text-white transition-colors duration-200 hover:bg-amber-700 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
            >
              この職種に応募する
            </a>
            <p className="text-[12.5px] leading-[1.85] text-gray-500">
              応募の前に条件だけ確かめたい、という連絡も受け付けています(返信は3営業日以内)
            </p>
          </div>
        </div>
      </div>

      {/* 切替の再生(opacity と transform のみ)。reduced-motion は同じ <style> 内で打ち消す */}
      <style>{`
        @keyframes jobsheet-in {
          from { opacity: 0; transform: translate3d(0, 8px, 0); }
          to   { opacity: 1; transform: translate3d(0, 0, 0); }
        }
        @media (prefers-reduced-motion: reduce) {
          .jobsheet-anim { animation: none !important; }
        }
      `}</style>
    </section>
  );
}

Add to your project via shadcn CLI

npx shadcn@latest add https://designs.first-ch.com/r/recruit-requirements-table.json