BLK-14Blocks
採用セクション(手書き風アクセント)
やわらかいアンバーの色面ブロックと手書き風SVG(うねり下線・粗いリング・矢印)で「人の気配」を出す採用セクション。顔写真を使わずイニシャル円で人物を表し、募集要項の破線フレームと「募集要項を見る/社員インタビューを読む」の2導線を添える。画像素材ゼロ・外部依存ゼロで、モーションは transform/opacity のみ(参考: 保育・教育系の中途採用サイトに見られる色面+手書き装飾+顔のない人物図の構図をブランド再構築・2026-08-07)。
- 追加日:
- 2026-08-07
- 依存:
- なし
- tags
- #recruit #block #hand-drawn #svg #no-image #no-dependency #motion #soft-color-field #reference:sakura-39-recruit.jp/career
プレビュー
BLK-14 — Recruit / 採用情報
いっしょに、
つくる人を
探しています。
人数が少ないぶん、ひとつの仕事に関わる範囲が広くなります。 決まった役割をこなす人ではなく、決め方から一緒に考えられる人と働きたいと思っています。
- K.M
Web ディレクター
在籍 3 年
「言われたものを作る」で終わらせないでいい。最初にそう言われました。
- S.T
フロントエンド
在籍 2 年
実装の理由を毎回ことばにする。そのぶん、迷わなくなりました。
- A.N
デザイナー
在籍 4 年
案件の数より、直せる回数。手を動かす時間がちゃんとあります。
- R.Y
運用サポート
在籍 1 年
納品したあとの話を、いちばん長くしている会社だと思います。
Open positions / 募集要項
- 募集職種
- Webディレクター / フロントエンドエンジニア / デザイナー
- 雇用形態
- 正社員・業務委託(いずれも試用期間3か月)
- 勤務地
- フルリモート可(月1回の集まりは任意参加)
- 選考フロー
- 書類 → カジュアル面談 → 実務課題 → 最終面談
"use client";
import { useEffect, useRef, useState } from "react";
import { Montserrat } from "next/font/google";
const montserrat = Montserrat({
weight: ["600", "700"],
subsets: ["latin"],
display: "swap",
});
/**
* 採用セクション(やわらかい色面 × 手書き風アクセント)
* 参考: 保育・教育系の中途採用サイト群に見られる「やわらかい色面ブロック+手書き風の線+顔のない人物図」の
* 構図を、First CH のブランド(白基調+アンバー)へ再構築(コピーではない)。
*
* 汎用技法メモ(web-design-playbook 還流用):
* - 手書き風の"描かれる"演出を stroke-dashoffset なしで作る: SVGを内包した span を origin-left で
* scaleX(0)→scaleX(1) させると、うねり線・下線・矢印は「左から引かれた」ように読める。
* dashoffset アニメーションと違い transform だけで済む(合成レイヤーのみで動くのでjank無し)。
* 線が長いほど水平方向の歪みが出るので、うねりの振幅は小さめ・線幅は非スケールで持たせるのがコツ。
* - 顔写真を使わずに「人の気配」を出す: イニシャル円(やわらかい色面)+手書き風の粗いリング。
* リングは真円ではなく制御点をわずかにずらしたベジェにし、hover で微回転させると"人が描いた線"に見える。
* 写真の権利処理・差し替えコストがゼロで、採用ページの初期提案やプレースホルダにそのまま使える。
* - 色面ブロックは blur-3xl のブロブを背景に置き、translateY の緩慢な往復(8〜11s)で"呼吸"させる。
* 前景の罫線・文字は動かさないので可読性を落とさない。motion-reduce:animate-none で静止。
* - 脱カード: 人物もポイントもカードで囲まず、hairline の縦罫(md 以上)と横罫でグルーピング。
* 囲いを使うのは募集要項の破線フレーム1枚だけに絞り、「掲示物」の比喩として意味を持たせる。
*/
type Member = {
initials: string;
role: string;
tenure: string;
voice: string;
/** イニシャル円のやわらかい色面 */
field: string;
};
const members: Member[] = [
{
initials: "K.M",
role: "Web ディレクター",
tenure: "在籍 3 年",
voice: "「言われたものを作る」で終わらせないでいい。最初にそう言われました。",
field: "bg-amber-100",
},
{
initials: "S.T",
role: "フロントエンド",
tenure: "在籍 2 年",
voice: "実装の理由を毎回ことばにする。そのぶん、迷わなくなりました。",
field: "bg-orange-100",
},
{
initials: "A.N",
role: "デザイナー",
tenure: "在籍 4 年",
voice: "案件の数より、直せる回数。手を動かす時間がちゃんとあります。",
field: "bg-stone-200",
},
{
initials: "R.Y",
role: "運用サポート",
tenure: "在籍 1 年",
voice: "納品したあとの話を、いちばん長くしている会社だと思います。",
field: "bg-amber-50",
},
];
const outline: { term: string; detail: string }[] = [
{ term: "募集職種", detail: "Webディレクター / フロントエンドエンジニア / デザイナー" },
{ term: "雇用形態", detail: "正社員・業務委託(いずれも試用期間3か月)" },
{ term: "勤務地", detail: "フルリモート可(月1回の集まりは任意参加)" },
{ term: "選考フロー", detail: "書類 → カジュアル面談 → 実務課題 → 最終面談" },
];
/** 手書き風のうねり下線(origin-left の scaleX で"引かれる") */
function HandUnderline({ drawn }: { drawn: boolean }) {
return (
<span
aria-hidden="true"
className={`absolute -bottom-2 left-0 block w-full origin-left transition-transform duration-[900ms] ease-out motion-reduce:transition-none motion-reduce:scale-x-100 ${
drawn ? "scale-x-100" : "scale-x-0"
}`}
>
<svg
viewBox="0 0 200 12"
fill="none"
preserveAspectRatio="none"
className="block h-[9px] w-full text-amber-500"
>
<path
d="M2 8C28 3 44 10.5 70 6.5 96 2.5 118 10 144 6 168 2.4 186 8.6 198 5.4"
stroke="currentColor"
strokeWidth="3"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
</span>
);
}
/** 手書き風の粗いリング(真円にしない・hover で微回転) */
function HandRing() {
return (
<svg
aria-hidden="true"
viewBox="0 0 100 100"
fill="none"
className="pointer-events-none absolute -inset-[7px] h-[calc(100%+14px)] w-[calc(100%+14px)] text-amber-500/70 transition-transform duration-500 ease-out group-hover:rotate-6 motion-reduce:transition-none"
>
<path
d="M51 4C73 6 95 23 95.5 48.5 96 74 75.5 96 49 95.5 24 95 4.5 74.5 4.5 49.5 4.5 25.5 24 5.5 47 4.2"
stroke="currentColor"
strokeWidth="1.6"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
);
}
/** 手書き風のきらめき(色面の余白に散らす小さな装飾) */
function Sparkle({ className }: { className: string }) {
return (
<svg
aria-hidden="true"
viewBox="0 0 24 24"
fill="none"
className={className}
>
<path
d="M12 2.5c.6 5 3.9 8.3 9 9-5.1.7-8.4 4-9 9-.6-5-3.9-8.3-9-9 5.1-.7 8.4-4 9-9Z"
fill="currentColor"
/>
</svg>
);
}
export default function RecruitTeaser() {
const rootRef = useRef<HTMLElement>(null);
const [active, setActive] = useState(false);
// 画面内に入ったら reveal を開始(IO 非対応環境は即開始)
// reduced-motion 対応は各要素の motion-reduce: variant で完結(JS判定は不要)
useEffect(() => {
const el = rootRef.current;
if (!el) return;
if (typeof IntersectionObserver === "undefined") {
const id = requestAnimationFrame(() => setActive(true));
return () => cancelAnimationFrame(id);
}
const io = new IntersectionObserver(
(entries) => {
if (entries.some((e) => e.isIntersecting)) {
setActive(true);
io.disconnect();
}
},
{ threshold: 0.15 },
);
io.observe(el);
return () => io.disconnect();
}, []);
const reveal = `transition-[opacity,transform] duration-700 ease-out motion-reduce:transition-none ${
active
? "translate-y-0 opacity-100"
: "translate-y-6 opacity-0 motion-reduce:translate-y-0 motion-reduce:opacity-100"
}`;
return (
<section
ref={rootRef}
aria-labelledby="recruit-teaser-heading"
className="relative w-full max-w-4xl overflow-hidden bg-white px-1 py-2"
>
{/* やわらかい色面ブロック(緩慢に呼吸させる・前景は動かさない) */}
<div aria-hidden="true" className="pointer-events-none absolute inset-0">
<span className="absolute -top-16 -left-14 block h-72 w-72 rounded-full bg-amber-100 blur-3xl [animation:recruit-float_9s_ease-in-out_infinite_alternate] motion-reduce:animate-none" />
<span className="absolute top-1/4 -right-16 block h-80 w-80 rounded-full bg-orange-100/80 blur-3xl [animation:recruit-float_11s_ease-in-out_1.2s_infinite_alternate] motion-reduce:animate-none" />
<span className="absolute bottom-4 left-1/4 block h-64 w-64 rounded-full bg-stone-200/70 blur-3xl [animation:recruit-float_8s_ease-in-out_.6s_infinite_alternate] motion-reduce:animate-none" />
<Sparkle className="absolute top-6 right-10 h-5 w-5 text-amber-400/80" />
<Sparkle className="absolute top-24 right-[4.5rem] h-3 w-3 text-amber-300" />
</div>
<div className="relative">
{/* ヘッダー */}
<header className="max-w-2xl">
<p
style={{ transitionDelay: "0ms" }}
className={`${montserrat.className} text-xs font-bold tracking-[0.25em] text-amber-600 uppercase ${reveal}`}
>
BLK-14 — Recruit / 採用情報
</p>
<h2
id="recruit-teaser-heading"
style={{ transitionDelay: "80ms" }}
className={`mt-4 text-[clamp(1.7rem,4.2vw,2.7rem)] leading-[1.35] font-bold tracking-tight text-gray-900 ${reveal}`}
>
{/* 和文の改行は成り行きに任せず意図した位置で折る(行末に「す。」だけが残る事故を防ぐ) */}
いっしょに、
<br />
<span className="relative inline-block whitespace-nowrap">
つくる人
<HandUnderline drawn={active} />
</span>
を
<br className="sm:hidden" />
探しています。
</h2>
<p
style={{ transitionDelay: "160ms" }}
className={`mt-6 text-[15px] leading-[1.9] font-medium text-gray-600 ${reveal}`}
>
人数が少ないぶん、ひとつの仕事に関わる範囲が広くなります。
決まった役割をこなす人ではなく、決め方から一緒に考えられる人と働きたいと思っています。
</p>
</header>
{/* はたらく人(顔写真を使わずイニシャル円+手書き風リングで気配を出す・脱カード) */}
<ul
role="list"
className="mt-12 grid grid-cols-1 gap-y-8 border-t border-gray-200 pt-10 sm:grid-cols-2 sm:gap-x-8 md:grid-cols-4 md:gap-x-0 md:divide-x md:divide-gray-200"
>
{members.map((m, i) => (
<li
key={m.initials}
style={{ transitionDelay: `${260 + i * 90}ms` }}
className={`group md:px-5 md:first:pl-0 md:last:pr-0 ${reveal}`}
>
<span className="relative inline-block">
<span
className={`flex h-[68px] w-[68px] items-center justify-center rounded-full ${m.field} transition-transform duration-500 ease-out group-hover:scale-105 motion-reduce:transition-none`}
>
<span
className={`${montserrat.className} text-[15px] font-bold tracking-[0.08em] text-gray-700`}
>
{m.initials}
</span>
</span>
<HandRing />
</span>
<p className="mt-5 text-[15px] font-semibold text-gray-900">
{m.role}
</p>
{/* 和文に英字向けの広いトラッキングを掛けると字間が割れるので、ここは既定の和文フォント・弱いトラッキングに留める */}
<p className="mt-1 text-[11px] font-semibold tracking-[0.04em] text-amber-600 tabular-nums">
{m.tenure}
</p>
<p className="mt-3 text-[13px] leading-[1.85] text-gray-600">
{m.voice}
</p>
</li>
))}
</ul>
{/* 募集要項(唯一の"囲い"=掲示物の比喩。破線フレーム+やわらかい色面) */}
<div
style={{ transitionDelay: "640ms" }}
className={`relative mt-14 rounded-[1.75rem] border-2 border-dashed border-amber-300/80 bg-amber-50/50 px-6 py-8 sm:px-9 sm:py-10 ${reveal}`}
>
<h3
className={`${montserrat.className} text-[11px] font-bold tracking-[0.22em] text-amber-700 uppercase`}
>
Open positions / 募集要項
</h3>
<dl className="mt-6 divide-y divide-amber-200/70 border-t border-amber-200/70">
{outline.map((row) => (
<div
key={row.term}
className="grid grid-cols-1 gap-y-1 py-4 sm:grid-cols-[8.5rem_1fr] sm:gap-x-6"
>
<dt className="text-[13px] font-semibold text-gray-500">
{row.term}
</dt>
<dd className="text-[14px] leading-[1.8] font-medium text-gray-800">
{row.detail}
</dd>
</div>
))}
</dl>
</div>
{/* 導線(募集要項=主 / インタビュー=従)+手書き風の矢印と添え書き */}
<div
style={{ transitionDelay: "760ms" }}
className={`mt-10 flex flex-col items-start gap-5 sm:flex-row sm:items-center ${reveal}`}
>
<a
href="#"
className="group inline-flex items-center gap-3 rounded-full bg-amber-600 px-7 py-3.5 text-[15px] font-bold text-white transition-transform duration-300 ease-out hover:-translate-y-0.5 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600 motion-reduce:transition-none"
>
募集要項を見る
<span
aria-hidden="true"
className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
>
→
</span>
</a>
<a
href="#"
className="group inline-flex items-center gap-2 text-[15px] font-semibold text-gray-900 focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-amber-600"
>
<span className="relative">
社員インタビューを読む
<span
aria-hidden="true"
className="absolute -bottom-1 left-0 h-px w-full origin-left scale-x-100 bg-gray-900 transition-transform duration-300 ease-out group-hover:scale-x-0 motion-reduce:transition-none"
/>
<span
aria-hidden="true"
className="absolute -bottom-1 left-0 h-px w-full origin-right scale-x-0 bg-amber-600 transition-transform delay-150 duration-300 ease-out group-hover:origin-left group-hover:scale-x-100 motion-reduce:transition-none"
/>
</span>
<span
aria-hidden="true"
className="inline-block transition-transform duration-300 ease-out group-hover:translate-x-1 motion-reduce:transition-none"
>
→
</span>
</a>
{/* 手書き風の矢印+添え書き(scaleX で引かれる) */}
<p className="flex items-center gap-2 text-[13px] font-medium text-gray-500 sm:ml-auto">
<span
aria-hidden="true"
className={`block w-14 origin-left transition-transform duration-[900ms] delay-300 ease-out motion-reduce:transition-none motion-reduce:scale-x-100 ${
active ? "scale-x-100" : "scale-x-0"
}`}
>
<svg
viewBox="0 0 60 26"
fill="none"
preserveAspectRatio="none"
className="block h-5 w-full text-amber-500 [transform:scaleX(-1)]"
>
<path
d="M3 20C13 21.5 30 18 40 8.5"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
vectorEffect="non-scaling-stroke"
/>
<path
d="M30 6.5 41 7.5 39.5 18"
stroke="currentColor"
strokeWidth="2"
strokeLinecap="round"
strokeLinejoin="round"
vectorEffect="non-scaling-stroke"
/>
</svg>
</span>
まずは話を聞くだけでも
</p>
</div>
</div>
{/* 色面ブロックの呼吸(transform のみ・reduced-motion で静止) */}
<style>{`
@keyframes recruit-float {
from { transform: translate3d(0, 0, 0) scale(1); }
to { transform: translate3d(0, -18px, 0) scale(1.06); }
}
`}</style>
</section>
);
}
shadcn CLI でプロジェクトに追加
npx shadcn@latest add https://designs.first-ch.com/r/recruit-teaser.json