Radix UIを触ってみる
Next13 app dirで触ってみる
Installation
npx create-next-app@latest --experimental-app
Radixの方はTutorialどおりに
npm install @radix-ui/react-popover@latest -E
stitchesもいれよ
npm install @stitches/react
'use client'
案件出た
Popoverの定義ファイルで'use client'つけたら解決した。
これSSR対応と言えるのか?
no stylingだとこんな感じ。Tab→Enter→Escapeと一通りの動作ができる。
公式通りのスタイリングを実現しようと思ったらcolorsも必要だ、けっこうめんどいな
いけたかも
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { FC } from "react";
import { styled } from "@stitches/react";
interface PopoverProps {
label: string;
content: string;
}
export const Popover: FC<PopoverProps> = ({ label, content }) => (
<PopoverPrimitive.Root>
<PopoverTrigger>{label}</PopoverTrigger>
<PopoverPrimitive.Portal>
<PopoverContent sideOffset={5}>
{content}
<PopoverArrow />
</PopoverContent>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
const PopoverTrigger = styled(PopoverPrimitive.Trigger, {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
borderRadius: "4px",
padding: "0 15px",
fontSize: "15px",
lineHeight: 1,
fontWeight: 500,
height: "35px",
backgroundColor: "white",
color: "var(--violet11)",
boxShadow: "0 2px 10px var(--blackA7)",
"&:focus": {
boxShadow: "0 0 0 2px var(--violet11)",
},
"&:hover": {
backgroundColor: "var(--mauve3)",
},
});
const PopoverContent = styled(PopoverPrimitive.Content, {
borderRadius: "4px",
padding: "20px",
width: "260px",
fontSize: "15px",
lineHeight: 1,
color: "var(--violet11)",
backgroundColor: "white",
boxShadow:
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px",
"&:focus": {
outline: "none",
boxShadow:
"hsl(206 22% 7% / 35%) 0px 10px 38px -10px, hsl(206 22% 7% / 20%) 0px 10px 20px -15px,\n 0 0 0 2px var(--violet7)",
},
});
const PopoverArrow = styled(PopoverPrimitive.Arrow, {
fill: "white",
});
@import "@radix-ui/colors/blackA.css";
@import "@radix-ui/colors/mauve.css";
@import "@radix-ui/colors/violet.css";
/* reset */
button {
all: unset;
}
body {
background-color: var(--violet11);
}
やっぱスタイリングを自分で考えるのが大変っすねー
さて、Tailwindと組み合わせてみますか
展望:Radix x Stitches x Radix Colors x Tailwindでぷちデザインシステム
- コンポーネント定義時は可読性の高いStitchesを使用
- 使用時にはTailwindで拡張できるように(なので定義時にもTailwindっぽくスタイリング)
- next themeを用いてdark theme対応 on Next.js(できれば13)
- Storybookで一覧に(Primitivesと見比べられるとなお良い)
- a11y testとかいろいろやりたい
あーでもあれだな
仮にここでclassNameを渡すことができても、どこにstyleがあたるのかわかりづらいな
<Popover label="More info" content="Some information..." />
こういうことしても上書きできないっぽい
<PopoverContent sideOffset={5} className="text-4xl">
する必要ないけど。
Overridingはここからしかできないのかなやっぱり
てことはclassName側からあれこれする必要はなくなったので、定義時に楽に書けると良い。
stitchesの設定をいじる。
とりあえずこれの色以外を採用してみる。色は普通にcloros使いたいので。
なるほど、こういう書き方もあるか
ta: (value: Stitches.PropertyValue<'textAlign'>) => ({ textAlign: value }),
fd: (value: Stitches.PropertyValue<'flexDirection'>) => ({ flexDirection: value }),
fw: (value: Stitches.PropertyValue<'flexWrap'>) => ({ flexWrap: value }),
ai: (value: Stitches.PropertyValue<'alignItems'>) => ({ alignItems: value }),
ac: (value: Stitches.PropertyValue<'alignContent'>) => ({ alignContent: value }),
jc: (value: Stitches.PropertyValue<'justifyContent'>) => ({ justifyContent: value }),
as: (value: Stitches.PropertyValue<'alignSelf'>) => ({ alignSelf: value }),
fg: (value: Stitches.PropertyValue<'flexGrow'>) => ({ flexGrow: value }),
fs: (value: Stitches.PropertyValue<'flexShrink'>) => ({ flexShrink: value }),
fb: (value: Stitches.PropertyValue<'flexBasis'>) => ({ flexBasis: value }),
こっちのがベース優秀だわ
設定ファイルが.tsだからこんなこともできるな
utils: {
...
items: (value: keyof typeof ALIGIN_ITEMS) => ({
alignItems: ALIGIN_ITEMS[value],
}),
...
}
const ALIGIN_ITEMS = {
start: "flex-start",
end: "flex-end",
center: "center",
baseline: "baseline",
stretch: "stretch",
} as const;
これけっこう体験良いな
作るのくっそめんどいけど
一旦flexとgridだけは用意しとこ
色々触ったけど公式が用意してくれている型のほうが柔軟っぽい
Stitches.PropertyValue<"justifyContent">
↑これでこんだけ出る
ムリにTaiwlind likeにする必要ないかーって感じ
いや、絞ることに意味があるのか?
とりあえずこうした
import type * as Stitches from "@stitches/react";
import { ALIGIN_ITEMS, FONT_WEIGHT, JUSTIFY_CONTENT } from "./tailwindMap";
export const tailwindSizing = {
space: {
0: "0px",
1: "0.25rem",
2: "0.5rem",
3: "0.75rem",
4: "1rem",
5: "1.25rem",
6: "1.5rem",
7: "1.75rem",
8: "2rem",
9: "2.25rem",
10: "2.5rem",
11: "2.75rem",
12: "3rem",
14: "3.5rem",
16: "4rem",
20: "5rem",
24: "6rem",
28: "7rem",
32: "8rem",
36: "9rem",
40: "10rem",
44: "11rem",
48: "12rem",
52: "13rem",
56: "14rem",
60: "15rem",
64: "16rem",
72: "18rem",
80: "20rem",
96: "24rem",
},
sizes: {
0: "0rem",
none: "none",
xs: "20rem",
sm: "24rem",
md: "28rem",
lg: "32rem",
xl: "36rem",
"2xl": "42rem",
"3xl": "48rem",
"4xl": "56rem",
"5xl": "64rem",
"6xl": "72rem",
"7xl": "80rem",
full: "100%",
},
fontSizes: {
xs: "0.75rem",
sm: "0.875rem",
base: "1rem",
lg: "1.125rem",
xl: "1.25rem",
"2xl": "1.5rem",
"3xl": "1.875rem",
"4xl": "2.25rem",
"5xl": "3rem",
"6xl": "3.75rem",
"7xl": "4.5rem",
"8xl": "6rem",
"9xl": "8rem",
},
radii: {
sm: "0.125rem",
md: "0.375rem",
lg: "0.5rem",
xl: "0.75rem",
"2xl": "1rem",
"3xl": "1.5rem",
full: "9999px",
},
fonts: {
system: "system-ui",
},
};
export const tailwindBreakpoints = {
sm: `@media (min-width: 640px)`,
md: `@media (min-width: 768px)`,
lg: `@media (min-width: 1024px)`,
xl: `@media (min-width: 1280px)`,
"2xl": `@media (min-width: 1536px)`,
};
export const tailwindUtils = {
p: (value: Stitches.PropertyValue<"padding">) => ({
padding: value,
}),
pt: (value: Stitches.PropertyValue<"paddingTop">) => ({
paddingTop: value,
}),
pr: (value: Stitches.PropertyValue<"paddingRight">) => ({
paddingRight: value,
}),
pb: (value: Stitches.PropertyValue<"paddingBottom">) => ({
paddingBottom: value,
}),
pl: (value: Stitches.PropertyValue<"paddingLeft">) => ({
paddingLeft: value,
}),
px: (value: Stitches.PropertyValue<"paddingLeft">) => ({
paddingLeft: value,
paddingRight: value,
}),
py: (value: Stitches.PropertyValue<"paddingTop">) => ({
paddingTop: value,
paddingBottom: value,
}),
m: (value: Stitches.PropertyValue<"margin">) => ({
margin: value,
}),
mt: (value: Stitches.PropertyValue<"marginTop">) => ({
marginTop: value,
}),
mr: (value: Stitches.PropertyValue<"marginRight">) => ({
marginRight: value,
}),
mb: (value: Stitches.PropertyValue<"marginBottom">) => ({
marginBottom: value,
}),
ml: (value: Stitches.PropertyValue<"marginLeft">) => ({
marginLeft: value,
}),
mx: (value: Stitches.PropertyValue<"marginLeft">) => ({
marginLeft: value,
marginRight: value,
}),
my: (value: Stitches.PropertyValue<"marginTop">) => ({
marginTop: value,
marginBottom: value,
}),
z: (value: Stitches.PropertyValue<"zIndex">) => ({
zIndex: value,
}),
rounded: (value: Stitches.PropertyValue<"borderRadius">) => ({
borderRadius: value,
}),
roundedTR: (value: Stitches.PropertyValue<"borderTopRightRadius">) => ({
borderTopRightRadius: value,
}),
roundedBR: (value: Stitches.PropertyValue<"borderBottomRightRadius">) => ({
borderBottomRightRadius: value,
}),
roundedBL: (value: Stitches.PropertyValue<"borderBottomLeftRadius">) => ({
borderBottomLeftRadius: value,
}),
roundedTL: (value: Stitches.PropertyValue<"borderTopLeftRadius">) => ({
borderTopLeftRadius: value,
}),
// NOTE: This may be confusing.
text: (value: Stitches.PropertyValue<"fontSize">) => ({
fontSize: value,
}),
fontW: (value: keyof typeof FONT_WEIGHT) => ({
fontWeight: FONT_WEIGHT[value],
}),
bg: (value: Stitches.PropertyValue<"backgroundColor">) => ({
backgroundColor: value,
}),
items: (value: keyof typeof ALIGIN_ITEMS) => ({
alignItems: ALIGIN_ITEMS[value],
}),
justify: (value: keyof typeof JUSTIFY_CONTENT) => ({
justifyContent: JUSTIFY_CONTENT[value],
}),
gridCols: (value: number | "none") => ({
gridTemplateColumns:
value === "none" ? "none" : `repeat(${value}, minmax(0, 1fr))`,
}),
gridRows: (value: number | "none") => ({
gridTemplateRows:
value === "none" ? "none" : `repeat(${value}, minmax(0, 1fr))`,
}),
};
import { createStitches } from "@stitches/react";
import {
gray,
mauve,
slate,
sage,
olive,
sand,
tomato,
red,
crimson,
pink,
plum,
purple,
violet,
indigo,
blue,
sky,
mint,
cyan,
teal,
green,
grass,
lime,
yellow,
amber,
orange,
brown,
bronze,
gold,
grayA,
mauveA,
slateA,
sageA,
oliveA,
sandA,
tomatoA,
redA,
crimsonA,
pinkA,
plumA,
purpleA,
violetA,
indigoA,
blueA,
skyA,
mintA,
cyanA,
tealA,
greenA,
grassA,
limeA,
yellowA,
amberA,
orangeA,
brownA,
bronzeA,
goldA,
whiteA,
blackA,
grayDark,
mauveDark,
slateDark,
sageDark,
oliveDark,
sandDark,
tomatoDark,
redDark,
crimsonDark,
pinkDark,
plumDark,
purpleDark,
violetDark,
indigoDark,
blueDark,
skyDark,
mintDark,
cyanDark,
tealDark,
greenDark,
grassDark,
limeDark,
yellowDark,
amberDark,
orangeDark,
brownDark,
bronzeDark,
goldDark,
grayDarkA,
mauveDarkA,
slateDarkA,
sageDarkA,
oliveDarkA,
sandDarkA,
tomatoDarkA,
redDarkA,
crimsonDarkA,
pinkDarkA,
plumDarkA,
purpleDarkA,
violetDarkA,
indigoDarkA,
blueDarkA,
skyDarkA,
mintDarkA,
cyanDarkA,
tealDarkA,
greenDarkA,
grassDarkA,
limeDarkA,
yellowDarkA,
amberDarkA,
orangeDarkA,
brownDarkA,
bronzeDarkA,
goldDarkA,
} from "@radix-ui/colors";
import type * as Stitches from "@stitches/react";
import {
tailwindBreakpoints,
tailwindSizing,
tailwindUtils,
} from "./stitchesTailwindUtil";
export type { VariantProps } from "@stitches/react";
export const {
config,
createTheme,
css,
getCssText,
globalCss,
styled,
theme,
} = createStitches({
theme: {
colors: {
...gray,
...mauve,
...slate,
...sage,
...olive,
...sand,
...tomato,
...red,
...crimson,
...pink,
...plum,
...purple,
...violet,
...indigo,
...blue,
...sky,
...mint,
...cyan,
...teal,
...green,
...grass,
...lime,
...yellow,
...amber,
...orange,
...brown,
...bronze,
...gold,
...grayA,
...mauveA,
...slateA,
...sageA,
...oliveA,
...sandA,
...tomatoA,
...redA,
...crimsonA,
...pinkA,
...plumA,
...purpleA,
...violetA,
...indigoA,
...blueA,
...skyA,
...mintA,
...cyanA,
...tealA,
...greenA,
...grassA,
...limeA,
...yellowA,
...amberA,
...orangeA,
...brownA,
...bronzeA,
...goldA,
...whiteA,
...blackA,
// Semantic colors
hiContrast: "$slate12",
// loContrast: '$slate1',
loContrast: "white",
canvas: "hsl(0 0% 93%)",
panel: "white",
transparentPanel: "hsl(0 0% 0% / 97%)",
shadowLight: "hsl(206 22% 7% / 35%)",
shadowDark: "hsl(206 22% 7% / 20%)",
},
...tailwindSizing,
},
utils: {
...tailwindUtils,
userSelect: (value: Stitches.PropertyValue<"userSelect">) => ({
WebkitUserSelect: value,
userSelect: value,
}),
appearance: (value: Stitches.PropertyValue<"appearance">) => ({
WebkitAppearance: value,
appearance: value,
}),
backgroundClip: (value: Stitches.PropertyValue<"backgroundClip">) => ({
WebkitBackgroundClip: value,
backgroundClip: value,
}),
bgGradient: (value: any) => ({
backgroundImage: `linear-gradient(${value})`,
}),
},
media: {
...tailwindBreakpoints,
motion: `@media (prefers-reduced-motion)`,
hover: `@media (hover: hover)`,
dark: `@media (prefers-color-scheme: dark)`,
light: `@media (prefers-color-scheme: light)`,
},
});
export type CSS = Stitches.CSS<typeof config>;
export const darkTheme = createTheme("dark-theme", {
colors: {
...grayDark,
...mauveDark,
...slateDark,
...sageDark,
...oliveDark,
...sandDark,
...tomatoDark,
...redDark,
...crimsonDark,
...pinkDark,
...plumDark,
...purpleDark,
...violetDark,
...indigoDark,
...blueDark,
...skyDark,
...mintDark,
...cyanDark,
...tealDark,
...greenDark,
...grassDark,
...limeDark,
...yellowDark,
...amberDark,
...orangeDark,
...brownDark,
...bronzeDark,
...goldDark,
...grayDarkA,
...mauveDarkA,
...slateDarkA,
...sageDarkA,
...oliveDarkA,
...sandDarkA,
...tomatoDarkA,
...redDarkA,
...crimsonDarkA,
...pinkDarkA,
...plumDarkA,
...purpleDarkA,
...violetDarkA,
...indigoDarkA,
...blueDarkA,
...skyDarkA,
...mintDarkA,
...cyanDarkA,
...tealDarkA,
...greenDarkA,
...grassDarkA,
...limeDarkA,
...yellowDarkA,
...amberDarkA,
...orangeDarkA,
...brownDarkA,
...bronzeDarkA,
...goldDarkA,
// Semantic colors
hiContrast: "$slate12",
loContrast: "$slate1",
canvas: "hsl(0 0% 15%)",
panel: "$slate3",
transparentPanel: "hsl(0 100% 100% / 97%)",
shadowLight: "hsl(206 22% 7% / 35%)",
shadowDark: "hsl(206 22% 7% / 20%)",
},
});
実際にシステム化するときは色を絞るんだろうな
色にも型ついてていいねえ
shadowも追加
export const SHADOW = {
none: "0 0 #0000",
sm: "0 1px 2px rgba(0, 0, 0, 0.05)",
base: "0 1px 3px 0 rgb(0 0 0 / 0.1), 0 1px 2px -1px rgb(0 0 0 / 0.1)",
md: "box-shadow: 0 4px 6px -1px rgb(0 0 0 / 0.1), 0 2px 4px -2px rgb(0 0 0 / 0.1)",
lg: "0 10px 15px -3px rgb(0 0 0 / 0.1), 0 4px 6px -4px rgb(0 0 0 / 0.1)",
xl: "0 20px 25px -5px rgb(0 0 0 / 0.1), 0 8px 10px -6px rgb(0 0 0 / 0.1)",
"2xl": "0 25px 50px -12px rgb(0 0 0 / 0.25)",
inner: "inset 0 2px 4px 0 rgba(0, 0, 0, 0.05)",
} as const;
Tailwindっぽくスタイリングし直してみる
"use client";
import * as PopoverPrimitive from "@radix-ui/react-popover";
import { FC } from "react";
import { styled } from "@/stitches.config";
interface PopoverProps {
label: string;
content: string;
}
export const Popover: FC<PopoverProps> = ({ label, content }) => (
<PopoverPrimitive.Root>
<PopoverTrigger>{label}</PopoverTrigger>
<PopoverPrimitive.Portal>
<PopoverContent sideOffset={5}>
{content}
<PopoverArrow />
</PopoverContent>
</PopoverPrimitive.Portal>
</PopoverPrimitive.Root>
);
const PopoverTrigger = styled(PopoverPrimitive.Trigger, {
display: "inline-flex",
alignItems: "center",
justifyContent: "center",
rounded: "$base",
px: "$4",
lineHeight: 1,
fontWeight: 500,
height: "35px",
backgroundColor: "white",
color: "$violet11",
boxShadow: "0 2px 10px var(--blackA7)",
"&:focus": {
boxShadow: "0 0 0 2px var(--violet11)",
},
"&:hover": {
backgroundColor: "$mauve3",
},
});
const PopoverContent = styled(PopoverPrimitive.Content, {
rounded: "$md",
padding: "$5",
width: "$md",
fontSize: "$base",
lineHeight: 1,
color: "$violet11",
backgroundColor: "white",
justify: "around",
shadow: "xl",
"&:focus": {
outline: "none",
shadow: "2xl",
},
});
const PopoverArrow = styled(PopoverPrimitive.Arrow, {
fill: "white",
});
書き味は型がついているからだいたいいいけど、$を書くのがめんどいなーって感じ
あと padding: "$5",
ってどれくらいだっけ?みたいなことある。普通にTaiwlindで書いているときはVSCodeの拡張機能でhoverすればpadding: 1.25rem/* 20px */;
って出てくるからここはイマイチ。
Tailwindで寄せるとChakraでよくね?感出てくるな
公式のデザインシステム、使用時にもstyleの上書きしやすいように個別にexportしてるわ
export { Popover, PopoverTrigger, PopoverContent, PopoverClose };
新たな展望
- Tailwind likeなstitches configをnpm packageとして公開
- Storybookを使いつつlocalぷちデザインシステムを構築
CSS in JSがServer Componentと相性悪い問題はあるな