Closed50

Radix UIを触ってみる

hajimismhajimism

https://www.radix-ui.com/

Next13 app dirで触ってみる

hajimismhajimism

Popoverの定義ファイルで'use client'つけたら解決した。
これSSR対応と言えるのか?

hajimismhajimism


no stylingだとこんな感じ。Tab→Enter→Escapeと一通りの動作ができる。

hajimismhajimism
Popover.tsx
"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",
});
globals.css
@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);
}
hajimismhajimism

やっぱスタイリングを自分で考えるのが大変っすねー

hajimismhajimism

さて、Tailwindと組み合わせてみますか

hajimismhajimism

展望:Radix x Stitches x Radix Colors x Tailwindでぷちデザインシステム

  • コンポーネント定義時は可読性の高いStitchesを使用
  • 使用時にはTailwindで拡張できるように(なので定義時にもTailwindっぽくスタイリング)
  • next themeを用いてdark theme対応 on Next.js(できれば13)
  • Storybookで一覧に(Primitivesと見比べられるとなお良い)
  • a11y testとかいろいろやりたい
hajimismhajimism

あーでもあれだな
仮にここでclassNameを渡すことができても、どこにstyleがあたるのかわかりづらいな

      <Popover label="More info" content="Some information..." />
hajimismhajimism

こういうことしても上書きできないっぽい

      <PopoverContent sideOffset={5} className="text-4xl">
hajimismhajimism

てことはclassName側からあれこれする必要はなくなったので、定義時に楽に書けると良い。
stitchesの設定をいじる。

hajimismhajimism

なるほど、こういう書き方もあるか

  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 }),
hajimismhajimism

設定ファイルが.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;

hajimismhajimism

色々触ったけど公式が用意してくれている型のほうが柔軟っぽい

Stitches.PropertyValue<"justifyContent">
hajimismhajimism

とりあえずこうした

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))`,
  }),
};
hajimismhajimism
stitches.config.ts
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%)",
  },
});

hajimismhajimism

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;

hajimismhajimism

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",
});

hajimismhajimism

書き味は型がついているからだいたいいいけど、$を書くのがめんどいなーって感じ

hajimismhajimism

あと padding: "$5",ってどれくらいだっけ?みたいなことある。普通にTaiwlindで書いているときはVSCodeの拡張機能でhoverすればpadding: 1.25rem/* 20px */;って出てくるからここはイマイチ。

hajimismhajimism

新たな展望

  1. Tailwind likeなstitches configをnpm packageとして公開
  2. Storybookを使いつつlocalぷちデザインシステムを構築
hajimismhajimism

CSS in JSがServer Componentと相性悪い問題はあるな
https://github.com/stitchesjs/stitches/issues/1109

hajimismhajimism

なんかもうここまで頑張るんなら最初からTaiwlindでゴリゴリ書いてったほうがいい気もする。classNameのinterfaceだけ提供して

このスクラップは2023/03/04にクローズされました