본문으로 건너뛰기
언어

@plott/webkit-design0.4.0

UI 라이브러리가 딛는 디자인 기초 — 움직임 토큰(길이·곡선, 움직임 줄이기), View Transition 대체 경로, 언어별 글꼴 스택(:lang 한자 글꼴), 자체 호스팅 글꼴 선언·대체 글꼴 지표, Google Fonts 주소, WCAG 대비 검사, OKLCH 팔레트. 컴포넌트는 두지 않는다

설치

pnpm
pnpm add @plott/webkit-design@0.4.0

진입점(exports)

import타입파일
@plott/webkit-design./dist/index.d.ts./dist/index.js
@plott/webkit-design/motion./dist/motion.d.ts./dist/motion.js
@plott/webkit-design/typography./dist/typography.d.ts./dist/typography.js
@plott/webkit-design/color./dist/color.d.ts./dist/color.js
@plott/webkit-design/palette./dist/palette.d.ts./dist/palette.js

UI 라이브러리가 딛고 설 디자인 기초 값을 모은 패키지입니다. 읽는 사람은 화면 스타일의 바탕(움직임·글꼴·색 대비)을 정하는 프론트엔드 개발자입니다. 컴포넌트와 React 코드는 두지 않습니다.

./palette 만 culori 에 의존하고 나머지는 의존성이 없습니다. 서버 컴포넌트·Node·브라우저 어디서나 불러올 수 있고, 브라우저 전역(matchMedia·document)은 함수를 부르는 순간에만 읽습니다. 이 패키지의 오류 메시지는 @plott/webkit-design: 로 시작합니다.

진입점

진입점어디서 도나내보내는 것
@plott/webkit-design서버·브라우저·빌드motion + typography + color 전부
@plott/webkit-design/motion서버·브라우저duration·easing(토큰), motionCss, prefersReducedMotion, startViewTransition, viewTransitionName, createViewTransitionRegistry
@plott/webkit-design/typography서버·브라우저·빌드fontStack, fontLangCss, googleFontsUrl, fontFace, fallbackMetrics, systemFontMetrics, normalizeFontLang
@plott/webkit-design/color서버·브라우저·테스트parseColor, relativeLuminance, contrast, meetsContrast, checkContrastPairs, ensureContrast, requiredRatio
@plott/webkit-design/palette서버·브라우저·빌드oklchScale, scaleCss, toOklch, mix, SCALE_LABELS (culori 를 가져오는 유일한 곳)

붙이기

ts
import { motionCss, fontLangCss, fontStack, fontFace } from "@plott/webkit-design";
const css = [  motionCss(),  `:root{--font-sans:${fontStack({ latin: ["Pretendard"] })}}`,  fontLangCss({ latin: ["Pretendard"], locales: ["ko", "ja", "zh-CN", "zh-TW"] }),  fontFace({ family: "Pretendard", src: "/fonts/Pretendard.woff2", weight: { min: 100, max: 900 } }),].join("\n");
ts
import { fallbackMetrics } from "@plott/webkit-design";
const adj = fallbackMetrics({ unitsPerEm: 2048, ascent: 1984, descent: -494, lineGap: 0, xWidthAvg: 978 }, "Arial");const fallbackCss = `@font-face{font-family:"Inter Fallback";src:local("Arial");size-adjust:${adj.sizeAdjust}%;ascent-override:${adj.ascentOverride}%;descent-override:${adj.descentOverride}%;line-gap-override:${adj.lineGapOverride}%}`;

adj 는 { sizeAdjust: 107.12, ascentOverride: 90.44, descentOverride: 22.52, lineGapOverride: 0 } 입니다. fontFace 는 url() 원본만 만들므로 대체 글꼴 규칙은 위처럼 직접 씁니다.

tsx
import { flushSync } from "react-dom";import { startViewTransition, viewTransitionName } from "@plott/webkit-design/motion";
export function Card({ id, setOpen }: { id: string; setOpen: (open: boolean) => void }) {  const open = () => startViewTransition(() => flushSync(() => setOpen(true)), { types: ["open"] });  return <div onClick={open} style={{ viewTransitionName: viewTransitionName(`card-${id}`) }} />;}
ts
import { checkContrastPairs, ensureContrast } from "@plott/webkit-design/color";import { expect } from "vitest";
declare const tokens: Record<string, string>;
expect(checkContrastPairs(tokens, [["text", "surface"], ["muted", "surface", "AA"], ["accent", "surface", { size: "large" }]])).toEqual([]);ensureContrast("#999999", "#ffffff", "AA");

ensureContrast("#999999", "#ffffff", "AA") 는 "#767676" 을 돌려줍니다.

ts
import { oklchScale, mix } from "@plott/webkit-design/palette";
const blue = oklchScale("#3b82f6", { chroma: "taper", hueShift: 10 });mix("#ffffff", "#000000", 0.5, "oklab");

Tailwind v4 의 @theme 로 내보낼 때는 이름공간을 맞춥니다.

ts
import { motionCss } from "@plott/webkit-design/motion";import { oklchScale, scaleCss } from "@plott/webkit-design/palette";
const motion = motionCss({ namespace: "tailwind" });const light = scaleCss("brand", oklchScale("#3b82f6", { anchor: true, output: "oklch" }), { block: "theme" });const darkScale = scaleCss("brand", oklchScale("#3b82f6", { anchor: true, output: "oklch", scheme: "dark" }), { selector: ".dark" });

어두운 테마의 반투명 토큰은 backdrop 에 그 아래 색을 줘서 판정합니다.

ts
import { checkContrastPairs } from "@plott/webkit-design/color";
declare const darkTokens: Record<string, string>;
checkContrastPairs(darkTokens, [["foreground", "input"], ["foreground", "card", "AA", "#000000"]], { backdrop: darkTokens["background"]! });

옵션과 기본값

함수옵션기본값
motionCss(options | prefix)prefix: CSS 변수 접두사, namespace: "tailwind": @theme{--ease-*;--transition-duration-*} 로 내고(prefix 는 쓰지 않음) 움직임 줄이기 규칙도 같은 이름으로 0ms 로 만듦, duration: { fast: 150 } 처럼 길이 토큰(ms, 0 이상)을 덮어씀, easing: 곡선 토큰을 덮어씀(글자·숫자·공백·( ) , . - 만)"motion" (--motion-duration-base), 토큰은 아래 값. 없는 토큰 이름은 RangeError
startViewTransition(update, options)types: 전환 유형 목록, skipIf: true 를 돌려주면 전환 없이 update 만 부름없음
viewTransitionName(id, options) · createViewTransitionRegistry(options)허용 글자 A-Z a-z 0-9 - 외에는 _<16진 코드>_ 로 바꿈. prefix: 이름 접두사(CSS 식별자)prefix: "vt-"
fontStack(lang | options)lang, latin(맨 앞 라틴 글꼴), mono, serif(mono 가 우선), scripts({ th: { sans, serif?, selectors? } } — 내장 CJK 표에 없는 언어를 더하거나 ko 같은 내장 언어를 덮어씀), fallback(마지막 대체 글꼴 목록)산세리프, 내장 한·중·일 글꼴 표, 끝은 Arial, sans-serif(serif 는 "Times New Roman", serif, mono 는 "Courier New", monospace)
fontLangCss(options)locales(필수), latin, variable, elementRule, mono, serif, scripts, fallback. scripts 의 selectors 는 :lang() 에 쓸 언어 태그(영문·숫자·-만)variable: "--font-sans", elementRule: true, 선택자는 언어 태그 하나
googleFontsUrl(families, options)display, text(글리프 서브셋), maxLength, baseUrl(https 주소, 쿼리·자격 증명 없음)display: "swap", maxLength: 2000, baseUrl: "https://fonts.googleapis.com/css2"
googleFontsUrl 굵기숫자(400), 목록([400, 700]), 가변 범위({ min, max })기본 굵기만
fontFace(options)family, src(주소 또는 { url, format } 목록), weight, style, display, unicodeRange, sizeAdjust, ascentOverride, descentOverride, lineGapOverridedisplay: "swap", format 은 확장자로 정함
fallbackMetrics(font, against)against: 지표 객체 또는 "Arial"·"Times New Roman""Arial"
contrast(fg, bg, backdrop)반투명 배경은 backdrop 위에 합성흰색
requiredRatio(spec)"AA"·"AAA" 또는 { level, size }AA, normal (AA 4.5/3, AAA 7/4.5)
meetsContrast(fg, bg, spec, backdrop)반투명 배경은 backdrop 위에 합성"AA", 흰색
checkContrastPairs(tokens, pairs, options)짝의 네 번째 원소 backdrop(색 값), 호출 전체 기본값 options.backdrop(짝의 값이 앞섬)흰색
ensureContrast(fg, bg, target, backdrop)target: 기준 또는 1–21 비율, backdrop: 반투명 배경 아래 색"AA", 흰색
oklchScale(base, options)steps(2–100), hueShift(도), chroma("keep"·"taper"), gamut("clamp"), lightness([밝은 끝, 어두운 끝]), anchor(true: 기준 색과 밝기가 가장 가까운 단계가 기준 색 그대로가 됨, 정수: 그 단계), output("hex"·"oklch" — oklch(L C H) 문자열), scheme("dark": 같은 단계를 거꾸로 늘어놓아 50 이 가장 어두움)steps: 11, hueShift: 0, chroma: "keep", lightness: [0.97, 0.22], anchor: false, output: "hex", scheme: "light"
scaleCss(name, colors, options)labels(색마다 이름), prefix, block("root"·"theme"), selector(block: "root" 의 선택자)labels: 11단계면 SCALE_LABELS, prefix: "color", block: "root", selector: ":root" — --color-<name>-<label> 변수를 냄
mix(a, b, t, space)space: oklch·oklab·lab·lch·rgb·hsl"oklch", t 는 0–1 로 맞춤

oklchScale 이 11단계일 때 이름은 SCALE_LABELS(50…950)와 순서가 같습니다. duration 토큰은 instant 0, fast 120, base 200, slow 320, slower 480(ms)이고 easing 토큰은 standard·enter·exit·emphasized(cubic-bezier 문자열)입니다.

기본으로 지키는 것

  • 글꼴 스택은 항상 기본 글꼴과 일반 계열(sans-serif 등)로 끝납니다. 웹 글꼴이 실패해도 글자가 사라지지 않습니다.
  • 한자·한글은 언어별 시스템 글꼴을 일반 계열보다 앞에 둡니다. Pretendard 같은 한글 글꼴에는 한자 글리프가 없어서, lang="ja" 문서의 한자가 브라우저가 고른 글꼴(한국·중국 자형)로 그려질 수 있습니다. fontLangCss 가 :lang(ja) 같은 규칙으로 언어마다 알맞은 글꼴을 앞세웁니다. :lang(zh) 가 번체까지 잡으므로 간체 규칙을 먼저, 번체 규칙을 나중에 냅니다.
  • -apple-system·BlinkMacSystemFont 는 따옴표 없이 씁니다. 따옴표를 치면 키워드가 아니라 글꼴 이름으로 읽혀 시스템 글꼴 별칭이 동작하지 않습니다.
  • 글꼴 이름·주소는 따옴표로 감싸고 이스케이프합니다. 제어 문자(줄바꿈 등)와 빈 값은 던집니다. < 는 CSS 이스케이프(\3c )로 바꾸므로, 결과를 <style> 안에 그대로 넣어도 </style> 로 빠져나오지 못하고 값은 CSS 가 읽으면 원래 글자 그대로입니다.
  • 한 요소에 쉼표 목록(["Arial, sans-serif"])을 넣으면 던집니다. 쉼표 목록을 이름 하나로 따옴표 쳐서 아무 글꼴도 못 찾는 일을 막습니다. 글꼴마다 배열 요소를 나눠 주세요.
  • fontLangCss 는 :lang(xx){--font-sans:…} 와 함께 [lang]:lang(xx){font-family:var(--font-sans)} 를 냅니다. var() 는 변수를 선언한 요소에서 값이 정해져 상속되므로, 본문 안의 <span lang="ja"> 가 글꼴을 바꾸려면 그 요소가 스스로 font-family: var(--font-sans) 를 가져야 합니다. lang 속성이 있는 요소에만 적용되며, 이미 요소마다 쓰는 앱은 elementRule: false 로 끕니다. 글자를 그리는 요소가 font-family: var(--font-sans) 를 써야 효과가 있습니다.
  • Google Fonts 주소는 공백을 + 로, 굵기를 정렬·중복 제거해 만들고 maxLength 를 넘으면 던집니다. Google 이 문서로 밝힌 길이 한도는 없습니다. 2000 은 오래된 클라이언트·프록시(Internet Explorer 2083자 등)가 안전하게 다루는 길이에 맞춘 보수적 값입니다. 더 긴 주소가 필요하면 maxLength 를 올리고, 검사를 끄려면 Infinity 를 씁니다. text= 는 한글 한 자가 퍼센트 인코딩으로 9자라서 주소가 빨리 길어지므로, 쓸 글자가 많으면 빌드 때 글꼴을 서브셋해 자체 호스팅하는 편이 맞습니다. 빌드 시점에 외부 글꼴 서버를 받아 오는 방식은 네트워크 실패가 빌드 실패가 되므로, 글꼴 파일을 저장소에 두는 next/font/local 이나 fontFace 자체 호스팅을 권합니다. googleFontsUrl 은 런타임에 링크로 받는 경우를 위한 것입니다.
  • «움직임 줄이기» 설정이면 길이가 0 이 됩니다. motionCss 가 @media (prefers-reduced-motion: reduce) 안에서 모든 길이 변수를 0ms 로 만들고, startViewTransition 은 전환 없이 갱신만 합니다. 기본으로 움직이는 속성은 transform 과 opacity 뿐으로 하는 것을 권합니다(레이아웃을 다시 계산하지 않습니다).
  • View Transition 을 모르는 브라우저에서도 갱신은 일어납니다. document.startViewTransition 이 없으면 update() 만 부르고, types 객체 형태에서 TypeError 가 나면 함수 형태로 다시 시도합니다. 전환이 건너뛰어지기만 한 경우(AbortError·InvalidStateError)는 삼킵니다. update 가 던진 오류는 View Transition 경로에서도 updateCallbackDone 으로 받아 호출한 쪽으로 다시 던집니다.
  • view-transition-name 은 겹치지 않는 유효한 식별자입니다. 같은 id 는 같은 이름, 다른 id 는 다른 이름이고 vt- 접두사로 숫자 시작·none 예약어를 피합니다. 이름이 겹치면 브라우저가 전환 전체를 건너뛰므로, id 가 겹칠 수 있는 목록에서는 createViewTransitionRegistry().claim(id) 가 이미 차지된 id 에 _dupN_ 을 붙여 줍니다(release(name) 으로 놓고 size 로 차지된 수를 봅니다).
  • parseColor 는 #hex·rgb()·hsl()·oklch()·oklab()(%·none·deg/rad/grad/turn, 공백·슬래시 알파)·CSS 이름 색 148개·transparent 를 읽습니다. 그래서 checkContrastPairs 가 oklch(...) 토큰을 invalid-color 로 거절하지 않습니다. sRGB 밖의 OKLCH 색은 채널을 잘라 sRGB 안으로 옮기는 근사라서 브라우저의 색역 매핑과 조금 다를 수 있습니다. lab()·lch()·color()·currentcolor 는 읽지 않고 null 입니다.
  • 대비는 반투명 색을 배경 위에 합성해서 셉니다. rgba(0,0,0,.5) 를 불투명 검정으로 보지 않습니다. #767676 은 4.54 로 AA 통과, #777777 은 4.48 로 탈락입니다.
  • 반투명 배경의 바탕은 backdrop 으로 정합니다. 어두운 테마의 oklch(1 0 0 / 15%) 같은 토큰은 바탕을 주지 않으면 흰색 위에 합성돼 틀린 비율이 나오므로, 어두운 테마는 그 아래 색을 backdrop 으로 줍니다.
  • ensureContrast 는 HSL 밝기만 최소로 움직입니다. 이미 기준을 넘으면 (알파를 배경에 합성한) 원래 색을 돌려주고, 대비가 커지는 쪽을 먼저 시도한 뒤 반대쪽을 시도합니다. 어느 쪽도 닿지 못하는 목표는 틀린 색을 주지 않고 던집니다.
  • checkContrastPairs 는 __proto__·constructor 같은 키를 토큰으로 읽지 않습니다. 없는 토큰은 missing-token, 해석할 수 없는 색은 invalid-color, 기준 미달은 low-contrast(비율 포함) 실패로 보고합니다. 빈 배열이면 통과입니다.
  • 팔레트는 항상 sRGB 안의 #rrggbb 이고 밝기는 단계마다 엄격히 줄어듭니다. 색역 밖 색은 채도를 줄여 맞추고, 경계에 놓인 #0000ff 같은 색은 바꾸지 않습니다. 색 정확도는 16진수 반올림 오차(채널당 ±0.5/255) 안에서 보장합니다.

그 밖

  • 내장 대체 글꼴 지표는 Arial 과 Times New Roman 둘입니다. 출처는 Next.js 16.4.2 의 capsize-font-metrics.json(capsize @capsizecss/metrics, Regular)입니다. 다른 글꼴은 FontMetrics 객체를 직접 넘깁니다.
  • googleFontsUrl 의 굵기 범위는 { min, max } 객체이고, 배열([400, 700])은 «400 과 700 두 가지» 목록입니다.
  • 길이 토큰 용도: fast 는 호버·눌림, base 는 버튼·토글·툴팁, slow 는 패널·모달, slower 는 화면 전환·큰 이동입니다.
  • View Transition 시험은 가짜 document 로 합니다. 실제 브라우저 동작은 시험 범위 밖입니다.