# Agency: Careers

> The open roles as one ruled grid with what everyone gets and how the studio hires, and a job post per role with its facts and an apply link.

A shadcn/ui block from the [@eased](https://eased.dev/?utm_source=shadcn.exchange) registry, listed on [shadcn.exchange](https://shadcn.exchange/r/eased/agency-careers).

- Page, with a live preview: https://shadcn.exchange/r/eased/agency-careers
- Screenshot: https://shadcn.exchange/shot/eased/agency-careers/light.png
- Registry item: https://eased.dev/r/agency-careers.json
- Listed: 2026-10-23, source last changed 2026-10-23

## Install

```bash
npx shadcn@latest add https://shadcn.exchange/r/eased/agency-careers.json
```

Run it in a project that has shadcn/ui set up. It adds the files below and installs what they depend on.

## Dependencies

- npm packages: `motion`
- Registry items: `https://eased.dev/r/eased-tokens.json`, `https://eased.dev/r/eased-motion.json`, `https://eased.dev/r/agency-core.json`, `https://eased.dev/r/button.json`

## Files

### `src/registry/eased/templates/agency/careers.tsx`

```tsx
"use client";

import type { ReactNode } from "react";
import { Button } from "../../button/button";
import { latest, type RoleSlug } from "./content";
import { careers, jobPosts } from "./content-careers";
import { Arrow, COLUMN, LABEL, PageHead, Reveal, Section, useHref } from "./kit";

/**
 * The careers index, /careers: the open roles as one ruled grid (title, team, place, arrow) so
 * every column lines up (rule 103), then what everyone gets and how the studio hires.
 */
export function CareersIndex() {
  const to = useHref();
  return (
    <>
      <div className={`${COLUMN} pt-section-sm`}>
        <PageHead title={careers.title} lead={careers.lead} />
      </div>

      <Section id="roles" label={careers.roles.label}>
        <ul className="grid grid-cols-[minmax(0,1fr)_auto] border-t border-line-subtle md:grid-cols-[minmax(0,5fr)_minmax(0,4fr)_minmax(0,3fr)_auto]">
          {latest.roles.items.map((role, i) => (
            <li key={role.slug} className="col-span-full grid grid-cols-subgrid">
              <Reveal
                index={i}
                className="group relative col-span-full grid grid-cols-subgrid items-center gap-x-pad-md border-b border-line-subtle py-pad-md"
              >
                <span className="flex min-w-0 flex-col gap-1 md:contents">
                  <a
                    href={to(`/careers/${role.slug}`)}
                    className="focus-ring self-start rounded-control-xs text-display-xl font-medium text-text transition-colors duration-(--eased-duration-fast) ease-standard group-hover-fine:text-text-secondary after:absolute after:inset-0 md:self-center"
                  >
                    {role.title}
                  </a>
                  <span className="text-ui text-text-secondary">{jobPosts[role.slug].team}</span>
                  <span className="text-ui text-text-muted">{role.place}</span>
                </span>
                <Arrow className="text-text-muted" />
              </Reveal>
            </li>
          ))}
        </ul>
      </Section>

      <Section id="benefits" label={careers.benefits.label}>
        <ul className="grid gap-x-tiles gap-y-pad-lg sm:grid-cols-2 lg:grid-cols-4">
          {careers.benefits.items.map((b, i) => (
            <li key={b.title}>
              <Reveal index={i} className="flex flex-col gap-1.5 border-t border-line-subtle pt-in">
                <h3 className="text-display-xl font-medium text-text">{b.title}</h3>
                <p className="text-ui-md text-pretty text-text-secondary">{b.body}</p>
              </Reveal>
            </li>
          ))}
        </ul>
      </Section>

      <Section id="hiring" label={careers.process.label}>
        <ol className="grid gap-x-tiles gap-y-pad-lg sm:grid-cols-2 lg:grid-cols-4">
          {careers.process.items.map((step, i) => (
            <li key={step.title}>
              <Reveal index={i} className="flex flex-col gap-1.5 border-t border-line-subtle pt-in">
                <span className="text-ui-sm text-text-muted tabular-nums">{String(i + 1).padStart(2, "0")}</span>
                <h3 className="text-display-xl font-medium text-text">{step.title}</h3>
                <p className="text-ui-md text-pretty text-text-secondary">{step.body}</p>
              </Reveal>
            </li>
          ))}
        </ol>
      </Section>
    </>
  );
}

/**
 * A job post, /careers/<slug>: the role's title, its facts in a ruled list beside the post (the
 * role, what you will do, what you bring), and how to apply, by email with the role as subject.
 */
export function JobPost({ slug }: { slug: RoleSlug }) {
  const role = latest.roles.items.find((r) => r.slug === slug) ?? latest.roles.items[0];
  const post = jobPosts[slug];
  const { facts, labels, apply } = careers.post;
  const rows = [
    { term: facts.team, value: post.team },
    { term: facts.place, value: role.place },
    { term: facts.type, value: post.type },
    { term: facts.salary, value: post.salary },
  ];
  const mail = `mailto:${apply.email}?subject=${encodeURIComponent(role.title)}`;

  return (
    <article aria-labelledby="page-title" className={`${COLUMN} pt-section-sm pb-group`}>
      <PageHead back={careers.back} title={role.title} size="md" />
      <div className="mt-group grid gap-group lg:grid-cols-[minmax(0,5fr)_minmax(0,7fr)] lg:gap-group-lg">
        <Reveal className="lg:sticky lg:top-pad-lg lg:self-start">
          <dl className="grid grid-cols-[auto_minmax(0,1fr)] gap-x-pad-md border-t border-line-subtle text-ui">
            {rows.map((f) => (
              <div key={f.term} className="col-span-full grid grid-cols-subgrid border-b border-line-subtle py-in">
                <dt className="text-text-muted">{f.term}</dt>
                <dd className="text-text tabular-nums">{f.value}</dd>
              </div>
            ))}
          </dl>
          <Button variant="primary" className="mt-pad-md" nativeButton={false} render={<a href={mail} />}>
            {careers.roles.apply}
          </Button>
        </Reveal>

        <Reveal index={1} className="flex max-w-2xl flex-col gap-group">
          <PostPart label={labels.role}>
            {post.intro.map((p) => (
              <p key={p.slice(0, 24)} className="text-ui-md text-pretty text-text-secondary sm:text-ui-lg">
                {p}
              </p>
            ))}
          </PostPart>
          <PostPart label={labels.doing}>
            <PostList items={post.doing} />
          </PostPart>
          <PostPart label={labels.bring}>
            <PostList items={post.bring} />
          </PostPart>
          <PostPart label={labels.bonus}>
            <PostList items={post.bonus} />
          </PostPart>
          <div className="flex flex-col items-start gap-pad-base rounded-panel bg-surface-sunken p-pad-md">
            <h2 className="text-display-xl font-medium text-text">{apply.title}</h2>
            <p className="max-w-lg text-ui-md text-pretty text-text-secondary">{apply.body}</p>
            <a
              href={mail}
              className="focus-ring rounded-control-xs font-display text-display-2xl font-medium tracking-display break-all text-text underline decoration-line-strong decoration-2 underline-offset-[0.18em] transition-colors duration-(--eased-duration-fast) ease-standard hover-fine:decoration-text"
            >
              {apply.email}
            </a>
          </div>
        </Reveal>
      </div>
    </article>
  );
}

function PostPart({ label, children }: { label: string; children: ReactNode }) {
  return (
    <section className="flex flex-col gap-pad-base">
      <h2 className={`${LABEL} border-t border-line-subtle pt-in`}>{label}</h2>
      {children}
    </section>
  );
}

/** A post's list: a short rule for a bullet, so each line starts on the text's own edge. */
function PostList({ items }: { items: readonly string[] }) {
  return (
    <ul className="flex flex-col gap-2.5">
      {items.map((item) => (
        <li key={item} className="grid grid-cols-[1rem_minmax(0,1fr)] items-baseline gap-2 text-ui-md text-pretty text-text-secondary">
          <span aria-hidden className="h-px w-2.5 translate-y-[-0.3em] bg-text-muted" />
          {item}
        </li>
      ))}
    </ul>
  );
}
```
