Turbo Rails Tutorial を Rails 7.2.1 / ruby 3.3.5 / Dev Container / tailwindcss でやってみた (4)
前回の続き です。Dev Container と tailwindcss を利用して Turbo Rails Tutorial をやっています。
今回は CSS ファイルの分割と CSS 入れ子 (CSS nesting) をできるようにします。
前回までソースコードは takaokouji/quote-editor:7e07238 にあります。
環境
- Apple M3 (MacBook Air 13 2024)
- macOS Sonoma 14.6.1
- Homebrew
- Visual Studio Code 1.93.0
- Dev Container 機能拡張をインストール済み
- ruby 3.3.5
- rails 7.2.1 gem
gem install rails
- 他のソフトウェアは Docker コンテナ上にインストール
PostCSSの導入
公式のドキュメント には tailwindcss を使いつつ、ファイル分割と CSS 入れ子を実現するには PostCSS を使う、とあります。
One of the most useful features preprocessors offer is the ability to organize your CSS into multiple files and combine them at build time by processing @import statements in advance, instead of in the browser.
The canonical plugin for handling this with PostCSS is postcss-import.
To add support for nested declarations, we recommend our bundled tailwindcss/nesting plugin, which is a PostCSS plugin that wraps postcss-nested or postcss-nesting and acts as a compatibility layer to make sure your nesting plugin of choice properly understands Tailwind’s custom syntax.
早速 PostCSS を導入します。railsコマンド一発で完了です。railsすごい!
bin/rails css:install:postcss
ただ、これが何しているのかが知っておきたいので、実行ログを詳しく見てみます。
apply /home/vscode/.rbenv/versions/3.3.5/lib/ruby/gems/3.3.0/gems/cssbundling-rails-1.4.1/lib/install/postcss/install.rb
apply /home/vscode/.rbenv/versions/3.3.5/lib/ruby/gems/3.3.0/gems/cssbundling-rails-1.4.1/lib/install/install.rb
Build into app/assets/builds
exist app/assets/builds
identical app/assets/builds/.keep
unchanged app/assets/config/manifest.js
Stop linking stylesheets automatically
gsub app/assets/config/manifest.js
unchanged .gitignore
unchanged .gitignore
Remove app/assets/stylesheets/application.css so build output can take over
remove app/assets/stylesheets/application.css
Add stylesheet link tag in application layout
unchanged app/views/layouts/application.html.erb
unchanged Procfile.dev
Add bin/dev to start foreman
identical bin/dev
Install PostCSS w/ nesting and autoprefixer
create postcss.config.js
create app/assets/stylesheets/application.postcss.css
run yarn add postcss postcss-cli postcss-import postcss-nesting autoprefixer from "."
yarn add v1.22.22
[1/4] Resolving packages...
[2/4] Fetching packages...
warning Pattern ["postcss@^8.4.45"] is trying to unpack in the same destination "/home/vscode/.cache/yarn/v6/npm-postcss-8.4.45-538d13d89a16ef71edbf75d895284ae06b79e603-integrity/node_modules/postcss" as pattern ["postcss@^8.4.23"]. This could result in non-deterministic behavior, skipping.
[3/4] Linking dependencies...
[4/4] Building fresh packages...
success Saved lockfile.
success Saved 29 new dependencies.
info Direct dependencies
├─ autoprefixer@10.4.20
├─ postcss-cli@11.0.0
├─ postcss-import@16.1.0
├─ postcss-nesting@13.0.0
└─ postcss@8.4.45
info All dependencies
├─ @csstools/selector-resolve-nested@2.0.0
├─ @csstools/selector-specificity@4.0.0
├─ @sindresorhus/merge-streams@2.3.0
├─ autoprefixer@10.4.20
├─ cliui@8.0.1
├─ dependency-graph@0.11.0
├─ escalade@3.2.0
├─ fs-extra@11.2.0
├─ get-caller-file@2.0.5
├─ get-stdin@9.0.0
├─ globby@14.0.2
├─ graceful-fs@4.2.11
├─ ignore@5.3.2
├─ jsonfile@6.1.0
├─ path-type@5.0.0
├─ postcss-cli@11.0.0
├─ postcss-import@16.1.0
├─ postcss-nesting@13.0.0
├─ postcss-reporter@7.1.0
├─ postcss@8.4.45
├─ pretty-hrtime@1.0.3
├─ require-directory@2.1.1
├─ slash@5.1.0
├─ thenby@1.3.4
├─ unicorn-magic@0.1.0
├─ wrap-ansi@7.0.0
├─ y18n@5.0.8
├─ yargs-parser@21.1.1
└─ yargs@17.7.2
Done in 12.35s.
Add build:css script
Add build:css script
run npm pkg set scripts.build:css="postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css" from "."
run yarn build:css from "."
yarn run v1.22.22
$ postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css
Done in 0.70s.
run bundle install --quiet
ここから次のことがわかりました。
- PostCSS の設定ファイル
postcss.config.js
が追加されたこと - PostCSS の CSS ファイル
app/assets/stylesheets/application.postcss.css
が追加されたこと - npm の
postcss
postcss-cli
postcss-import
postcss-nesting
autoprefixer
パッケージが追加されたこと - CSS をビルドするコマンドが
postcss ./app/assets/stylesheets/application.postcss.css -o ./app/assets/builds/application.css
に変更されたこと
これだけで PostCSS を使う準備がほとんど整いました。
あとは、前回作成した tailwindcss の CSS を PostCSS から使えるようにするだけです。
postcss.config.js
に tailwindcss の設定を追加して、
module.exports = {
plugins: [
require('postcss-import'),
require('postcss-nesting'),
require('tailwindcss'),
require('autoprefixer'),
]
}
app/assets/stylesheets/application.tailwind.css
の内容を app/assets/stylesheets/application.postcss.css
に転記します。そして、@tailwind base;
を @import "tailwindcss/base";
に変えます。他の @tailwind
も変えます。
/* Entry point for your PostCSS build */
@import "tailwindcss/base";
@import "tailwindcss/components";
@import "tailwindcss/utilities";
/* ここから下は app/assets/stylesheets/application.tailwind.css のまま */
tailwind.config.js
はそのままで OK。
最後に app/assets/stylesheets/application.tailwind.css
を削除して、 bin/dev
を再起動します。
これで OK。rails がコマンドを提供してくれていたので、簡単に PostCSS を導入できました。
ファイル分割
次はファイル分割です。
app/assets/stylesheets/application.postcss.css
の内容を複数のファイルに分割して転記します。ファイルのパスはチュートリアルに従います。
app/assets/stylesheets/config/reset.css
html {
@apply overflow-y-scroll h-full;
}
body {
@apply flex flex-col min-h-full bg-background text-text-body leading-normal font-sans;
}
img,
picture,
svg {
@apply block max-w-full;
}
h1,
h2,
h3,
h4,
h5,
h6 {
@apply text-text-header leading-tight;
}
h1 {
@apply text-3xl;
}
h2 {
@apply text-2xl;
}
h3 {
@apply text-xl;
}
h4 {
@apply text-lg;
}
a {
@apply text-primary no-underline transition-colors duration-200
hover:text-primary-rotate focus:text-primary-rotate active:text-primary-rotate;
}
app/assets/stylesheets/components/btn.css
@layer components {
.btn {
@apply
inline-block
py-1.5 px-4
rounded-md
bg-origin-border
bg-transparent
border-solid border-2 border-transparent
font-bold
no-underline
cursor-pointer
outline-none
[transition:filter_400ms,color_200ms]
hover:[transition:filter_250ms,color_200ms]
focus:[transition:filter_250ms,color_200ms]
focus-within:[transition:filter_250ms,color_200ms]
active:[transition:filter_250ms,color_200ms];
}
.btn--primary {
@apply text-white bg-gradient-to-r from-primary to-primary-rotate
hover:text-white hover:saturate-[1.4] hover:brightness-[115%]
focus:text-white focus:saturate-[1.4] focus:brightness-[115%]
focus-within:text-white focus-within:saturate-[1.4] focus-within:brightness-[115%]
active:text-white active:saturate-[1.4] active:brightness-[115%];
}
.btn--secondary {
@apply text-white bg-gradient-to-r from-secondary to-secondary-rotate
hover:text-white hover:saturate-[1.2] hover:brightness-[110%]
focus:text-white focus:saturate-[1.2] focus:brightness-[110%]
focus-within:text-white focus-within:saturate-[1.2] focus-within:brightness-[110%]
active:text-white active:saturate-[1.2] active:brightness-[110%];
}
.btn--light {
@apply text-dark bg-light
hover:text-dark hover:brightness-[92%]
focus:text-dark focus:brightness-[92%]
focus-within:text-dark focus-within:brightness-[92%]
active:text-dark active:brightness-[92%];
}
.btn--dark {
@apply text-white border-dark bg-dark
hover:text-white
focus:text-white
focus-within:text-white
active:text-white;
}
}
app/assets/stylesheets/components/quote.css
@layer components {
.quote {
@apply flex justify-between text-center items-center gap-3 bg-white rounded-md shadow-sm mb-4 p-2
md:py-2 md:px-4;
}
.quote__actions {
@apply flex flex-[0_0_auto] gap-2 self-start;
}
}
app/assets/stylesheets/components/form.css
@layer components {
.form {
@apply flex flex-wrap gap-2;
}
.form__group {
@apply flex-1;
}
.form__input {
@apply block w-full max-w-full py-1.5 px-2 border-solid border-2 border-light rounded-md outline-none
transition-shadow duration-200
focus:[box-shadow:0_0_0_2px_theme(colors.glint)];
}
.form__input--invalid {
@apply border-primary;
}
}
app/assets/stylesheets/components/visually-hidden.css
@layer components {
/* Shamelessly stolen from Bootstrap */
.visually-hidden {
@apply !absolute !w-px !h-px !p-0 !-m-px !overflow-hidden ![clip:rect(0,0,0,0)] !whitespace-nowrap !border-0;
}
}
app/assets/stylesheets/components/error-message.css
@layer components {
.error-message {
@apply w-full text-primary bg-primary-bg p-2 rounded-md;
}
}
app/assets/stylesheets/layouts/container.css
@layer components {
.container {
@apply w-full pr-2 pl-2 ml-auto mr-auto md:pr-4 md:pl-4 md:max-w-[60rem];
}
}
app/assets/stylesheets/layouts/header.css
@layer components {
.header {
@apply flex flex-wrap gap-3 justify-between mt-4 mb-6 md:mb-8;
}
}
ここまで。
分割できたら app/assets/stylesheets/application.postcss.css
を修正して、それらを読み込むようにします。
/* Entry point for your PostCSS build */
@import "tailwindcss/base";
@import "./config/reset.css";
@import "tailwindcss/components";
@import "./components/btn.css";
@import "./components/quote.css";
@import "./components/form.css";
@import "./components/visually-hidden.css";
@import "./layouts/container.css";
@import "./layouts/header.css";
@import "tailwindcss/utilities";
これで CSS ファイルを分割できました。ブラウザで一通り操作して、見た目が変わっていないことを確認します。
CSS 入れ子
次は CSS 入れ子 (CSS nesting) に対応します。
postcss.config.js
の require('postcss-nesting'),
を require('tailwindcss/nesting')
に書き換えます。
module.exports = {
plugins: [
require('postcss-import'),
require('tailwindcss/nesting'),
require('tailwindcss'),
require('autoprefixer'),
]
}
これでSASSを使っているチュートリアルのように CSS ファイルで &
を使えるようになります。
余談ですが、「CSS 入れ子」といってもブラウザが標準で対応している CSS nesting (日本語訳) と、 SASS などの CSS プリプロセッサで処理する必要がある CSS nested があります。nesting と nested は、どちらもほぼ同じようなものなのですが、チュートリアルのように CSS のコーディング規約として BEM を採用する場合は後者の nested にする必要があります。
nested では、以下のようにクラス名の一部を &
で補うことができます。例では &--primary
の箇所で &
が .btn
として補われて .btn-primary
が定義されます。しかしながら、CSS nesting では対応していません。
@layer components {
.btn {
/* 省略 */
&--primary {
/* 省略 */
}
}
}
というわけで今回は CSS nested を使います。参考までに tailwindcss で CSS nesting を使う場合の PostCSS の設定は次に挙げておきます。 plugins
の指定が配列からオブジェクトになり、 tailwindcss/nesting
を読み込むときのパラメーターとして ‘postcss-nesting’ を指定しているのが変更点です。
/* 今回はこれは使わない。今後 CSS nesting を使うことがあればこのように設定する。 */
module.exports = {
plugins: {
'postcss-import': {},
'tailwindcss/nesting': 'postcss-nesting',
tailwindcss: {},
autoprefixer: {},
}
}
それでは、いくつかの CSS ファイルを CSS nested な記述に変えます。
app/assets/stylesheets/components/btn.css
@layer components {
.btn {
@apply
inline-block
py-1.5 px-4
rounded-md
bg-origin-border
bg-transparent
border-solid border-2 border-transparent
font-bold
no-underline
cursor-pointer
outline-none
[transition:filter_400ms,color_200ms]
hover:[transition:filter_250ms,color_200ms]
focus:[transition:filter_250ms,color_200ms]
focus-within:[transition:filter_250ms,color_200ms]
active:[transition:filter_250ms,color_200ms];
&--primary {
@apply text-white bg-gradient-to-r from-primary to-primary-rotate
hover:text-white hover:saturate-[1.4] hover:brightness-[115%]
focus:text-white focus:saturate-[1.4] focus:brightness-[115%]
focus-within:text-white focus-within:saturate-[1.4] focus-within:brightness-[115%]
active:text-white active:saturate-[1.4] active:brightness-[115%];
}
&--secondary {
@apply text-white bg-gradient-to-r from-secondary to-secondary-rotate
hover:text-white hover:saturate-[1.2] hover:brightness-[110%]
focus:text-white focus:saturate-[1.2] focus:brightness-[110%]
focus-within:text-white focus-within:saturate-[1.2] focus-within:brightness-[110%]
active:text-white active:saturate-[1.2] active:brightness-[110%];
}
&--light {
@apply text-dark bg-light
hover:text-dark hover:brightness-[92%]
focus:text-dark focus:brightness-[92%]
focus-within:text-dark focus-within:brightness-[92%]
active:text-dark active:brightness-[92%];
}
&--dark {
@apply text-white border-dark bg-dark
hover:text-white
focus:text-white
focus-within:text-white
active:text-white;
}
}
}
app/assets/stylesheets/components/quote.css
@layer components {
.quote {
@apply flex justify-between text-center items-center gap-3 bg-white rounded-md shadow-sm mb-4 p-2
md:py-2 md:px-4;
&__actions {
@apply flex flex-[0_0_auto] gap-2 self-start;
}
}
}
app/assets/stylesheets/components/form.css
@layer components {
.form {
@apply flex flex-wrap gap-2;
&__group {
@apply flex-1;
}
&__input {
@apply block w-full max-w-full py-1.5 px-2 border-solid border-2 border-light rounded-md outline-none
transition-shadow duration-200
focus:[box-shadow:0_0_0_2px_theme(colors.glint)];
&--invalid {
@apply border-primary;
}
}
}
}
ここまで。
ブラウザで見た目がまったく同じであることを確認。
前回の作業中に tailwindcss では @import と CSS nested が使えないことを発覚したため、かなり焦っていました。でも、これで CSS ファイル群がチュートリアルと同じディレクトリ構成・内容にできました。なんとかなってよかったです。
今回はここまで。
ソースコードは takaokouji/quote-editor においていますので、興味がある方は Watch していただけると励みになります。
協力者の募集
スモウルビー (GitHub) の開発にご協力いただける方を常に募集しています。
ご協力いただける方は、 contact@smalruby.jp までご連絡いただいてもいいですし、連絡なしで「xxx のブロックに対応しました」というPRを作成してもらってもかまいません。むしろその方が好都合です。スポンサーも募集しています。
また、 拙著:小学生から楽しむ きらきらRubyプログラミング をご購入いただけるとありがたいです。スモウルビーの使い方と教え方を学ぶことができる書籍です。特に小・中学校の先生に読んでいただきたいです。
日本中の小・中学生が学校の授業や地域のプログラミング教室でスモウルビーを使っています。みなさんのご協力で、たくさんの子どもたちがハッピーになります。ご協力、よろしくお願いします。