Classy-UIを触った

Classy-UIを触ってみたので書き残します。

サンプル

React
Nuxt.js

Classy-UIとは

CSSプロパティを抽象化するJavaScript/TypeScriptライブラリです。
ざっくり言うと「プロパティで組み立てるCSSフレームワーク」です。

比較対象に挙げられるのが、Tailwind CSSです。
Tailwind CSSがクラス名を組み合わせるのに対して、Classy-UIはプロパティの定数を組み合わせます。
ライブラリ特有の知識でなく、CSSプロパティの知識で構築できるのが良いところです。
IDEの補完も受けられるので、「SMALLって言われても、どんぐらいSMALLよ」な問題もありません。

欠点は、クラス名ベースでのデザイン構築に比べて、CSSの知識に依存しがちなことです。
プロパティの記述も人によってまちまちで、styleLintなども使えないのですが、そこはご愛嬌…。

あと、スタイルがまとめて出力されるようなので、Above the Fold向けのCSSを書く時は注意です

使い方

Classy-UIの実態は、プロパティをクラス名に変換するBabelプラグインです。
また、型の恩恵を受けられるのが望ましいので、実質 Babel + TypeScript 環境が必須になります。
(その導入方法は今回書きません)

この記事の公開日時点では、classy-ui@betaがインストール可能です。

shell
yarn add -D classy-ui@beta

さらに、babelrcでプラグインの設定が必要です。

babelrc
{
  "plugins": [["classy-ui/plugin"]]
}

compose関数内でtokenを使って、プロパティを流し込んでいきます。

classy-ui-sample.ts
import { compose, tokens } from 'classy-ui'

const composedElement = compose(
  tokens.borderWidth.WIDTH_1,
  tokens.borderColor.GREEN_700,
  tokens.color.GREEN_700,
  tokens.borderRadius.SMALL,
)

作成したスタイルは、出力時にはクラス名として変換されます。
以下はdevでのものです。

classy-ui-sample.ts
// border-top-width__WIDTH_1 border-right-width__WIDTH_1 border-bottom-width__WIDTH_1 border-left-width__WIDTH_1 border-top-color__GREEN_700 border-right-color__GREEN_700 border-bottom-color__GREEN_700 border-left-color__GREEN_700 color__GREEN_700 border-top-left-radius__SMALL border-top-right-radius__SMALL border-bottom-right-radius__SMALL border-bottom-left-radius__SMALL

prodではclassy-ui.cssが、buildフォルダのルートに出力されるようになっています。
それをHTML側で読み込んで使用することになります。

build/classy-ui.css
.A__A{border-top-color:#fc8181}.B__A{border-right-color:#fc8181}.C__A{border-bottom-color:#fc8181}.D__A{border-left-color:#fc8181}.E__B{border-top-width:1px}.F__B{border-right-width:1px}.G__B{border-bottom-width:1px}.H__B{border-left-width:1px}.I__C{border-top-left-radius:.25rem}.J__C{border-top-right-radius:.25rem}.K__C{border-bottom-right-radius:.25rem}.L__C{border-bottom-left-radius:.25rem}.M__D{color:#fff}.M__A{color:#fc8181}.N__A{background-color:#fc8181}.N__D{background-color:#fff}

その特性からSPAフレームワーク/ライブラリ向けで、ドキュメントにはCRAやNext.jsでの導入手順があります。
今回は間をとってReact + webpack と Nuxt.jsで試しました。

やる

React

(webpack4.42, @babel/core7.8.7, React 16.13, TypeScript 3.8.3 時点)

Babelプラグインを、".babelrc"のpluginsに設定しておきます。
また、outputオプションを"dist"に変えます。(初期値が"build"フォルダなので…)

babelrc
{
  "presets": ["@babel/preset-typescript", "@babel/preset-react"],
  "plugins": [["classy-ui/plugin", { "output": "dist" }]]
}

{ isActive: boolean }を受け取る、ボタン向けのてきとうなstyleを作成してみます。
ネストを許さないような作りになっていて、擬似クラスを値の後に設定できたり、子要素まで擬似クラスを適用させるようなAPIがあります。(詳細は公式ドキュメントで)

src/components/styles.ts
import { compose, tokens, mobile } from 'classy-ui'

export const styleButton = (props: { isActive: boolean }) =>
  compose(
    tokens.borderColor.RED_400,
    tokens.borderWidth.WIDTH_1,
    tokens.borderRadius.MEDIUM,
    tokens.fontSize.EXTRA_LARGE_2,
    tokens.outline.NONE.focus,
    tokens.boxShadow.OUTLINE.focus, // 値の後に擬似クラスを指定できる
    props.isActive ? tokens.color.WHITE : tokens.color.RED_400,
    props.isActive
      ? tokens.backgroundColor.RED_400
      : tokens.backgroundColor.WHITE,
    mobile(tokens.fontSize.EXTRA_LARGE_4), // media query
  )

作ったstyleButtonを、コンポーネントのclassNameに噛ませるだけ。

src/components/SampleButton.tsx
import React, { useState } from 'react'
import { styleButton } from './styles'

export const SampleButton = () => {
  const [isActive, setIsActive] = useState(false)
  const handleClick = () => {
    setIsActive(!isActive)
  }
  return (
    <button
      className={styleButton({ isActive })} // ここ
      aria-pressed={isActive}
      onClick={handleClick}
    >
      Sample Button
    </button>
  )
}

これでOK。

Classy-UIで作成したボタンを確認しているところ

あとは、ビルド先でCSSファイルを読み込む必要があるので、html-webpack-plugin を使って出力先のindexファイルにLinkを追加します。

shell
yarn add -D html-webpack-plugin
webpack.config.jsのplugins
plugins: [
  new HtmlWebpackPlugin({
    template: path.resolve(__dirname, 'src/index.html'),
    isProduction: process.env.NODE_ENV === 'production',
  }),
],
src/index.html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Classy-UI-Sample</title>
    <% if (htmlWebpackPlugin.options.isProduction){ %>
    <link rel="stylesheet" href="classy-ui.css" />
    <% } %>
  </head>
  <body>
    <div id="app"></div>
  </body>
</html>

Nuxt.js

(nuxt 2.0, @nuxt/typescript-build 0.6で確認)

まずSFCから。
Reactの項で作成したstyleButtonをそのまま使っています。

components/ButtonComponent.vue
<template>
  <button
    :class="style"
    :aria-pressed="isActive ? 'true' : 'false'"
    @click="toggleActive"
  >
    Sample Button
  </button>
</template>

<script lang="ts">
import Vue from 'vue'
import { ref, computed } from '@vue/composition-api'
import { styleButton } from './styles' // classy-ui

export default Vue.extend({
  setup() {
    const isActive = ref(false)
    const style = computed(() => styleButton({ isActive: isActive.value })) // style
    function toggleActive() {
      isActive.value = !isActive.value
    }
    return {
      isActive,
      style,
      toggleActive
    }
  }
})
</script>

buildは邪悪ですが、"nuxt.config.js"のbuildにbabelオプションを追加。
outputを'.nuxt/dist/client'に。

nuxt.config.jsのbuild
build: {
  // ...others
  babel: {
    plugins: [['classy-ui/plugin', { output: '.nuxt/dist/client' }]]
  }
}

さらにheadのlinkを下記のようにします。

nuxt.config.jsのhead
head: {
  // ...others
  link: [
    process.env.NODE_ENV === 'production' && {
        rel: 'stylesheet',
        href: '/_nuxt/classy-ui.css'
      }
  ]
}

これでyarn buildするとclassy-ui.cssが出力されます。

generateの場合は、出力先、linkを"/dist"に当てるだけ。

感想

  • CSSのプロパティに寄り添う音楽性が好きです
  • beforeとかafterとかどうすんだろう
  • 書き忘れたのですが、独自の定数を追加することも可能なようです