03ショートカットタイルカード
正方形のタイル4枚が aspect-ratio で形を保ち、矢印が角へ滑って行き先を示します。
使いどころホーム画面のクイックメニュー、サポート入口
aspect-ratiotranslategrid

<main class="stage">
<ul class="st is-demo" id="st">
<li class="st__tile">
<span class="st__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 56v152l-32-16l-32 16l-32-16l-32 16l-32-16l-32 16V56a8 8 0 0 1 8-8h176a8 8 0 0 1 8 8" opacity=".2"/><path d="M72 104a8 8 0 0 1 8-8h96a8 8 0 0 1 0 16H80a8 8 0 0 1-8-8m8 40h96a8 8 0 0 0 0-16H80a8 8 0 0 0 0 16m152-88v152a8 8 0 0 1-11.58 7.15L192 200.94l-28.42 14.21a8 8 0 0 1-7.16 0L128 200.94l-28.42 14.21a8 8 0 0 1-7.16 0L64 200.94l-28.42 14.21A8 8 0 0 1 24 208V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16m-16 0H40v139.06l20.42-10.22a8 8 0 0 1 7.16 0L96 199.06l28.42-14.22a8 8 0 0 1 7.16 0L160 199.06l28.42-14.22a8 8 0 0 1 7.16 0L216 195.06Z"/></g></svg></span>
<span class="st__go" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg></span>
<a class="st__link" href="#orders">注文履歴</a>
</li>
<li class="st__tile">
<span class="st__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M248 120v64a8 8 0 0 1-8 8h-24a24 24 0 0 0-48 0h-64a24 24 0 0 0-48 0H32a8 8 0 0 1-8-8v-40h160v-24Z" opacity=".2"/><path d="m255.42 117l-14-35a15.93 15.93 0 0 0-14.84-10H192v-8a8 8 0 0 0-8-8H32a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h17a32 32 0 0 0 62 0h50a32 32 0 0 0 62 0h17a16 16 0 0 0 16-16v-64a7.9 7.9 0 0 0-.58-3M192 88h34.58l9.6 24H192ZM32 72h144v64H32Zm48 136a16 16 0 1 1 16-16a16 16 0 0 1-16 16m81-24h-50a32 32 0 0 0-62 0H32v-32h144v12.31A32.1 32.1 0 0 0 161 184m31 24a16 16 0 1 1 16-16a16 16 0 0 1-16 16m48-24h-17a32.06 32.06 0 0 0-31-24v-32h48Z"/></g></svg></span>
<span class="st__go" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg></span>
<a class="st__link" href="#delivery">配送状況</a>
</li>
<li class="st__tile">
<span class="st__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M224 128a96 96 0 0 1-144.07 83.11l-37.39 12.47a8 8 0 0 1-10.12-10.12l12.47-37.39A96 96 0 1 1 224 128" opacity=".2"/><path d="M128 24a104 104 0 0 0-91.82 152.88l-11.35 34.05a16 16 0 0 0 20.24 20.24l34.05-11.35A104 104 0 1 0 128 24m0 192a87.87 87.87 0 0 1-44.06-11.81a8 8 0 0 0-4-1.08a7.9 7.9 0 0 0-2.53.42L40 216l12.47-37.4a8 8 0 0 0-.66-6.54A88 88 0 1 1 128 216m12-88a12 12 0 1 1-12-12a12 12 0 0 1 12 12m-44 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12m88 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12"/></g></svg></span>
<span class="st__go" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg></span>
<a class="st__link" href="#support">問い合わせ</a>
</li>
<li class="st__tile">
<span class="st__ic" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="m195.88 195.88l-39.6-39.6a40 40 0 0 0 0-56.56l39.6-39.6a96 96 0 0 1 0 135.76M60.12 60.12a96 96 0 0 0 0 135.76l39.6-39.6a40 40 0 0 1 0-56.56Z" opacity=".2"/><path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m39.1 131.79a47.84 47.84 0 0 0 0-55.58l28.5-28.49a87.83 87.83 0 0 1 0 112.56ZM96 128a32 32 0 1 1 32 32a32 32 0 0 1-32-32m88.28-67.6l-28.49 28.5a47.84 47.84 0 0 0-55.58 0L71.72 60.4a87.83 87.83 0 0 1 112.56 0M60.4 71.72l28.5 28.49a47.84 47.84 0 0 0 0 55.58l-28.5 28.49a87.83 87.83 0 0 1 0-112.56M71.72 195.6l28.49-28.5a47.84 47.84 0 0 0 55.58 0l28.49 28.5a87.83 87.83 0 0 1-112.56 0"/></g></svg></span>
<span class="st__go" aria-hidden="true"><svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256"><g fill="currentColor"><path d="M192 64v104L88 64Z" opacity=".2"/><path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z"/></g></svg></span>
<a class="st__link" href="#help">ヘルプ</a>
</li>
</ul>
</main>@use "../../../../site/static/demo-tokens" as *;
// 바로가기 타일 — 폭이 얼마든 aspect-ratio 로 정사각을 지키는 게 이 항목의 뼈대다.
// 높이를 px 로 박지 않으니 열 수가 바뀌어도(4열 → 2열) 타일은 늘 정사각이다.
// 움직이는 건 화살표 하나뿐 — transform 이 아니라 독립 속성 translate 를 쓴다.
// 타일 전체가 링크가 아니라 라벨이 링크다. 그래서 링크 글자만 읽어도 어디로 가는지 안다.
$duration: $dur-quick;
$easing: $ease-spring;
$color: $subject-blue;
$reach: 8px;
@include stage($stage-orange);
.st {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: $sp-2;
box-sizing: border-box; width: min(100%, 440px);
margin: 0; padding: 0; list-style: none;
}
.st__tile {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto 1fr auto;
aspect-ratio: 1 / 1;
box-sizing: border-box;
padding: $sp-2;
border-radius: $r-card;
background: $subject-cream;
}
.st__ic {
grid-column: 1; grid-row: 1;
display: inline-grid; place-items: center;
color: $subject-ink;
font-size: clamp(16px, 5.5vw, 24px);
}
// 화살표 칩. 모서리 쪽(오른쪽 위)으로 밀려 나가며 행선지를 가리킨다
.st__go {
grid-column: 2; grid-row: 1;
display: inline-grid; place-items: center;
width: clamp(16px, 5.5vw, 22px); height: clamp(16px, 5.5vw, 22px);
border-radius: 50%;
background: rgba($color, .20); color: $subject-ink;
font-size: clamp(10px, 3.6vw, 15px);
translate: 0 0;
transition: translate $duration $easing;
}
.st__tile:hover .st__go,
.st__tile:focus-within .st__go { translate: $reach (-$reach); }
.st__link {
grid-column: 1 / -1; grid-row: 3;
color: $subject-ink;
font-weight: 700; font-size: clamp(9px, 3vw, 12px); line-height: 1.25;
text-decoration: underline; text-underline-offset: 2px;
}
.st__link:focus-visible { outline: 2px solid $subject-ink; outline-offset: 2px; border-radius: $r-xs; }
// ── 격자 미리보기 루프 (2초). 네 타일이 0 / .4 / .8 / 1.2초 차례로 화살표를 내보낸다.
// animation-delay 를 쓰면 animation-fill-mode: backwards 가 반드시 함께 간다 (첫 프레임 번쩍임).
.st.is-demo .st__go {
animation-name: stGo;
animation-duration: $dur-loop;
animation-timing-function: $easing;
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.st.is-demo .st__tile:nth-child(2) .st__go { animation-delay: .4s; }
.st.is-demo .st__tile:nth-child(3) .st__go { animation-delay: .8s; }
.st.is-demo .st__tile:nth-child(4) .st__go { animation-delay: 1.2s; }
@keyframes stGo {
0% { translate: 0 0; }
12% { translate: $reach (-$reach); }
24% { translate: $reach (-$reach); }
36%, 100% { translate: 0 0; }
}
// ── 좁은 화면: 네 칸과 정사각을 그대로 두고 여백만 줄인다
@media (max-width: 340px) {
.st { gap: $sp-1; }
.st__tile { padding: $sp-1; }
}
@media (prefers-reduced-motion: reduce) {
.st.is-demo .st__go {
animation: none !important;
translate: 0 0 !important;
transition: none !important;
}
}html, body {
margin: 0;
overflow: hidden;
}
.stage {
box-sizing: border-box;
min-height: 100vh;
display: grid;
place-items: center;
margin: 0;
background: #ff4d1f;
font-family: -apple-system, "Segoe UI", "Noto Sans KR", sans-serif;
padding: clamp(8px, 4vw, 24px);
}
.st {
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 8px;
box-sizing: border-box;
width: min(100%, 440px);
margin: 0;
padding: 0;
list-style: none;
}
.st__tile {
display: grid;
grid-template-columns: 1fr auto;
grid-template-rows: auto 1fr auto;
aspect-ratio: 1/1;
box-sizing: border-box;
padding: 8px;
border-radius: 16px;
background: #fff7e6;
}
.st__ic {
grid-column: 1;
grid-row: 1;
display: inline-grid;
place-items: center;
color: #17141a;
font-size: clamp(16px, 5.5vw, 24px);
}
.st__go {
grid-column: 2;
grid-row: 1;
display: inline-grid;
place-items: center;
width: clamp(16px, 5.5vw, 22px);
height: clamp(16px, 5.5vw, 22px);
border-radius: 50%;
background: rgba(47, 109, 246, 0.2);
color: #17141a;
font-size: clamp(10px, 3.6vw, 15px);
translate: 0 0;
transition: translate 220ms cubic-bezier(0.2, 0.8, 0.2, 1);
}
.st__tile:hover .st__go,
.st__tile:focus-within .st__go {
translate: 8px -8px;
}
.st__link {
grid-column: 1/-1;
grid-row: 3;
color: #17141a;
font-weight: 700;
font-size: clamp(9px, 3vw, 12px);
line-height: 1.25;
text-decoration: underline;
text-underline-offset: 2px;
}
.st__link:focus-visible {
outline: 2px solid #17141a;
outline-offset: 2px;
border-radius: 4px;
}
.st.is-demo .st__go {
animation-name: stGo;
animation-duration: 2s;
animation-timing-function: cubic-bezier(0.2, 0.8, 0.2, 1);
animation-iteration-count: infinite;
animation-fill-mode: backwards;
}
.st.is-demo .st__tile:nth-child(2) .st__go {
animation-delay: 0.4s;
}
.st.is-demo .st__tile:nth-child(3) .st__go {
animation-delay: 0.8s;
}
.st.is-demo .st__tile:nth-child(4) .st__go {
animation-delay: 1.2s;
}
@keyframes stGo {
0% {
translate: 0 0;
}
12% {
translate: 8px -8px;
}
24% {
translate: 8px -8px;
}
36%, 100% {
translate: 0 0;
}
}
@media (max-width: 340px) {
.st {
gap: 4px;
}
.st__tile {
padding: 4px;
}
}
@media (prefers-reduced-motion: reduce) {
.st.is-demo .st__go {
animation: none !important;
translate: 0 0 !important;
transition: none !important;
}
}// 사람이 만지면 미리보기 루프를 뗀다 — 그 뒤로는 링크에 닿은 타일의 화살표만 밀려 나간다
var shortcutGrid = document.getElementById('st');
['pointerdown', 'focusin', 'pointerover'].forEach(function (evt) {
shortcutGrid.addEventListener(evt, function () { shortcutGrid.classList.remove('is-demo'); });
});// ShortcutTileGrid — vanilla 판(run/081/demo/03-content-card-shortcut-tile/index.html) 의 React 포트.
// 폭이 얼마든 aspect-ratio 로 정사각을 지키고, 화살표만 독립 속성 translate 로 모서리 쪽으로 밀려 나간다.
// 타일 전체가 링크가 아니라 라벨이 링크다 — 링크 글자만 읽어도 어디로 가는지 안다.
import { useState } from "react";
import type { CSSProperties } from "react";
import styles from "./ShortcutTileGrid.module.scss";
export type ShortcutTileGridProps = {
duration?: number;
easing?: string;
color?: string;
};
type Vars = CSSProperties & { "--duration"?: string; "--easing"?: string; "--color"?: string };
type Tile = { label: string; href: string; mark: string };
const TILES: Tile[] = [
{ label: "주문 내역", href: "#orders", mark: "receipt" },
{ label: "배송 조회", href: "#delivery", mark: "truck" },
{ label: "문의하기", href: "#support", mark: "chat" },
{ label: "도움말", href: "#help", mark: "help" },
];
function TileMark(props: { mark: string }) {
if (props.mark === "receipt") {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M224 56v152l-32-16l-32 16l-32-16l-32 16l-32-16l-32 16V56a8 8 0 0 1 8-8h176a8 8 0 0 1 8 8" opacity=".2" />
<path d="M72 104a8 8 0 0 1 8-8h96a8 8 0 0 1 0 16H80a8 8 0 0 1-8-8m8 40h96a8 8 0 0 0 0-16H80a8 8 0 0 0 0 16m152-88v152a8 8 0 0 1-11.58 7.15L192 200.94l-28.42 14.21a8 8 0 0 1-7.16 0L128 200.94l-28.42 14.21a8 8 0 0 1-7.16 0L64 200.94l-28.42 14.21A8 8 0 0 1 24 208V56a16 16 0 0 1 16-16h176a16 16 0 0 1 16 16m-16 0H40v139.06l20.42-10.22a8 8 0 0 1 7.16 0L96 199.06l28.42-14.22a8 8 0 0 1 7.16 0L160 199.06l28.42-14.22a8 8 0 0 1 7.16 0L216 195.06Z" />
</g>
</svg>
);
}
if (props.mark === "truck") {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M248 120v64a8 8 0 0 1-8 8h-24a24 24 0 0 0-48 0h-64a24 24 0 0 0-48 0H32a8 8 0 0 1-8-8v-40h160v-24Z" opacity=".2" />
<path d="m255.42 117l-14-35a15.93 15.93 0 0 0-14.84-10H192v-8a8 8 0 0 0-8-8H32a16 16 0 0 0-16 16v112a16 16 0 0 0 16 16h17a32 32 0 0 0 62 0h50a32 32 0 0 0 62 0h17a16 16 0 0 0 16-16v-64a7.9 7.9 0 0 0-.58-3M192 88h34.58l9.6 24H192ZM32 72h144v64H32Zm48 136a16 16 0 1 1 16-16a16 16 0 0 1-16 16m81-24h-50a32 32 0 0 0-62 0H32v-32h144v12.31A32.1 32.1 0 0 0 161 184m31 24a16 16 0 1 1 16-16a16 16 0 0 1-16 16m48-24h-17a32.06 32.06 0 0 0-31-24v-32h48Z" />
</g>
</svg>
);
}
if (props.mark === "chat") {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M224 128a96 96 0 0 1-144.07 83.11l-37.39 12.47a8 8 0 0 1-10.12-10.12l12.47-37.39A96 96 0 1 1 224 128" opacity=".2" />
<path d="M128 24a104 104 0 0 0-91.82 152.88l-11.35 34.05a16 16 0 0 0 20.24 20.24l34.05-11.35A104 104 0 1 0 128 24m0 192a87.87 87.87 0 0 1-44.06-11.81a8 8 0 0 0-4-1.08a7.9 7.9 0 0 0-2.53.42L40 216l12.47-37.4a8 8 0 0 0-.66-6.54A88 88 0 1 1 128 216m12-88a12 12 0 1 1-12-12a12 12 0 0 1 12 12m-44 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12m88 0a12 12 0 1 1-12-12a12 12 0 0 1 12 12" />
</g>
</svg>
);
}
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="m195.88 195.88l-39.6-39.6a40 40 0 0 0 0-56.56l39.6-39.6a96 96 0 0 1 0 135.76M60.12 60.12a96 96 0 0 0 0 135.76l39.6-39.6a40 40 0 0 1 0-56.56Z" opacity=".2" />
<path d="M128 24a104 104 0 1 0 104 104A104.11 104.11 0 0 0 128 24m39.1 131.79a47.84 47.84 0 0 0 0-55.58l28.5-28.49a87.83 87.83 0 0 1 0 112.56ZM96 128a32 32 0 1 1 32 32a32 32 0 0 1-32-32m88.28-67.6l-28.49 28.5a47.84 47.84 0 0 0-55.58 0L71.72 60.4a87.83 87.83 0 0 1 112.56 0M60.4 71.72l28.5 28.49a47.84 47.84 0 0 0 0 55.58l-28.5 28.49a87.83 87.83 0 0 1 0-112.56M71.72 195.6l28.49-28.5a47.84 47.84 0 0 0 55.58 0l28.49 28.5a87.83 87.83 0 0 1-112.56 0" />
</g>
</svg>
);
}
function ArrowMark() {
return (
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 256 256">
<g fill="currentColor">
<path d="M192 64v104L88 64Z" opacity=".2" />
<path d="M192 56H88a8 8 0 0 0-5.66 13.66L128.69 116l-70.35 70.34a8 8 0 0 0 11.32 11.32L140 127.31l46.34 46.35A8 8 0 0 0 200 168V64a8 8 0 0 0-8-8m-8 92.69l-38.34-38.34L107.31 72H184Z" />
</g>
</svg>
);
}
export default function ShortcutTileGrid(props: ShortcutTileGridProps) {
const duration = props.duration ?? 220;
const easing = props.easing ?? "cubic-bezier(.2, .8, .2, 1)";
const color = props.color ?? "#2f6df6";
const [preview, setPreview] = useState<boolean>(true);
const vars: Vars = { "--duration": duration + "ms", "--easing": easing, "--color": color };
const stop = () => setPreview(false);
return (
<main className={styles.stage}>
<ul
className={styles.st + (preview ? " " + styles.isDemo : "")}
style={vars}
onPointerDown={stop}
onPointerOver={stop}
onFocus={stop}
>
{TILES.map((tile) => (
<li className={styles.tile} key={tile.href}>
<span className={styles.ic} aria-hidden="true"><TileMark mark={tile.mark} /></span>
<span className={styles.go} aria-hidden="true"><ArrowMark /></span>
<a className={styles.link} href={tile.href}>{tile.label}</a>
</li>
))}
</ul>
</main>
);
}03 ショートカットタイルカード — 計測 累積面積 1.637% · 動いたフレーム 19/23GODRICH オリジナル · SCSS と React は zip に同梱 · css カード デザイン 9種 — 街・チーム・ショートカット
このデモが属する記事
css カード デザイン 9種 — 街・チーム・ショートカットcss カード デザインとは、画面に内容を載せて並べるための最も基本的な容器を組む仕事です。この記事の原則は、クリックをカード全体ではなくリンクに任せることです。街紹介・チーム・ショートカット・自己紹介タブ・レビュー・スペック・イベント・イ この記事のデモをすべて見る


