HTMLとCSS全く知らないがJetpack Compose for Webでポートフォリオサイトつくった

# はじめに
私は、普段Jetpack ComposeをAndroidアプリ開発で趣味程度に触ってきました。

最近stableになったAndroidのJetpack Composeですが、今年5月にJetpack Compose for Webの[Technology Preview](https://blog.jetbrains.com/kotlin/2021/05/technology-preview-jetpack-compose-for-web/)が公開されました。

私の体感ではあるのですが、触ってHello worldなどしている記事は何件か見つけたのですが、何か作って公開して解説などしてる記事などは見かけなかったので今回触ってみようとなったのがきっかけです。

タイトルにある通り、私は今までiOSをほんのちょっとやったことがあって普段はAndroidアプリ開発とFlutterを使ったモバイルアプリ開発をしているので、HTMLやCSSを始めReactなどのWebフロントを開発する技術にとても疎いです。

なので、「あっているのかわからんがとりあえず動いたからこれでいこう」というスタンスで今回Jetpack Composeを使ってポートフォリオサイトを作りました。

色々詳しく調べたりしてないので間違ってることだらけだとは思いますが、ぜひWebフロント周りに知見のある方に気軽に指摘して頂けたら大変嬉しいです。

また、ComposeでAndroidアプリを作ったことはあるけど、for Web気になってる人に向けて最初の学習コストのハードルが下がって触るきっかけになると嬉しいなと思います。

今回作ったのはこちら。

https://b4tchkn.github.io/

![Screen Shot 2021-08-22 at 13.52.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/80aa3bf4-5630-838e-1848-fb017560e003.png)


軽く解説を入れながらどのように作ったのか書いていきたいと思います。

# やっていき
## Getting Started
まず最初何から始めればいいかわかないので、[公式のGetting Started](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Web/Getting_Started)を見ました。

これを見ればまずは、Hello world的な数字をカウントアップ・ダウンさせるページを作ることができます。

## Hello worldを超えるために
Jetbrainsが公開している[このページ](https://compose-web.ui.pages.jetbrains.team/)もComposeで作られているようで、[GitHub](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Web/Getting_Started)にソースコードが公開されています。

ComposeでAndroidアプリ作るときみたいにColumnとRowやBoxがたくさん使われているのかと思いきや見たことない景色が広がっていてまずは軽くHTMLのタグ一覧を調べました。
http://www.fureai.or.jp/~irie/html-tag/

正直今も使い分けが全くわからず、雰囲気でレイアウトは作っています。


開発するときは実際のページのレイアウトとソースコードを比較して作りを見たりするために他に参考にするものがまだあまりなかったので、Jetbrainsのリポジトリはめちゃめちゃ参考にしました。


## Webでレイアウトを作るときの初見殺し
私が今回作ったポートフォリオでは画面いっぱいにページを表示して中央にコンテンツを表示させ、縦と横もスクロールさせたくなかったです。

Composeでどう設定するかは後ほど説明するとして、そこで調べると、縦と横いっぱいに表示するにはCSSで`100vh`と`100vw`で縦と横の設定をするようです。
それをComposeで設定したのですが、なんかちょっとだけ縦と横にスクロールバーが表示されてスクロールできてしまう現象に陥りました。

知り合いのWebフロントに知見のあるエンジニアに尋ねると、どうやらデフォルトで当たるCSSのスタイルがあってそれが最初に勝手に設定されていてそれが原因そうとのこと。

それは初見殺しすぎると思いました。

Webフロントの開発はまずそのデフォルトで当たっているCSSのスタイルを無効化して更地に戻すところから始まるようで、その設定をしました。
更地に戻す方法はいくつかあるようなのですが、どれもそのデフォルトのCSSを無効にするリセットCSSと呼ばれるスタイルを設定するようです。


今回私は、[こちらの記事](https://osakanav.com/css-design-prevent-resetcss-how-to-use)でも紹介されている[ress](https://github.com/filipelinhares/ress)というリセットCSSを使いました。

これを記事で紹介されている通りに設定すると、ページが画面いっぱいに表示されてちょっとスクロールされてしまう問題も解消しました。

`index.html`に下記を追記しただけです。

```html:index.html
    <link href="https://unpkg.com/ress/dist/ress.min.css" rel="stylesheet">
```

めでたしめでたし。

## ComposeでCSSを用意してフルスクリーンのコンテンツを中央に配置したページを作る
ではさきほど触れた、ページを全画面に表示して中央にコンテンツを表示させるやり方をComposeで設定していきます。
Composeでは、`StyleSheet()`を継承した`object`を作ってその中にCSSの内容のようなものを書いていきます。

そこに、画面いっぱいにページを表示して、縦と横もスクロールさせずにコンテンツを中央に配置するために[この記事](https://mae.chab.in/archives/2749)を参考に実装しました。

CSSでは以下のような設定をしてHTMLのタグにそのスタイルを当てると要件を満たせるそう。

```css
.box {
  width: 100vw;
  height: 100vh;
  display: flex; /* 要素をflexboxに対応させる */
  align-items: center; /* 縦方向の中央揃え */
  justify-content: center; /* 横方向の中央揃え */
  flex-direction: column; /* 子要素の並びを上から下にする（要素の改行に対応） */
}
```

それをComposeで書くとこんな感じです。`StyleSheet()`を継承した`AppStyleSheet`という`object`を定義しました。
ちょっと背景色を設定してるなどの違いがありますが、雰囲気でCSSとComposeでのスタイルの定義の違いがわかると思います。
今回ではCSSの`.box`で定義している内容が`centerContainer`に定義している内容に当たります。

```kotlin
object AppStyleSheet : StyleSheet() {
    val centerContainer by style {
        display(DisplayStyle.Flex)
        justifyContent(JustifyContent.Center)
        flexDirection(FlexDirection.Column)
        alignItems(AlignItems.Center)
        width(100.vw)
        height(100.vh)
        background("#F1F1F1")
    }
}
```

これをComposableに設定するのはこんな感じです。
これもHTMLにCSSを当てるのと同じような雰囲気で書けるのでなんとなくわかると思います。

```kotlin
@Composable
fun Layout(content: @Composable () -> Unit) {
    Div(attrs = {
        classes(AppStyleSheet.centerContainer)
    }) {
        content()
    }
}
```

この`Layout()`を使って根本の`main()`でページの内容をwrapするとページの内容が全画面かつ中央に配置されます。

```kotlin
fun main() {
    renderComposable(rootElementId = "root") {
        Style(AppStyleSheet)
        Layout {
            // ページの内容
        }
    }
}
```

## レイアウトの組み方
Compose for Webにも`Column`と`Row`はあります。HTMLタグの`attrs`に`flexDirection`と`display`を設定するとその中のコンテンツが`Column`になったり`Row`になったりします。
HTMLのタグである`Div`だったり`P`だったり`A`だったりのComposableが用意されていてそれらを使ってレイアウトを作っていけます。

```kotlin
    Div(attrs = {
        style {
            flexDirection(FlexDirection.Column)
            display(DisplayStyle.Flex)
        }
    }) {
        // コンテンツの中身
    }
```

スタイルを当てる際は、これは普通のHTMLとCSSと同じように、タグに直接`style`で`attrs`にスタイルを設定することもできますが、`AppStyleSheet`に書いたスタイルをタグに設定することもできます。

上で書いた`@Composable Div()`の`style`に直接`flexDirection`と`display`の設定をするのと、下のように`AppStyleSheet`に書いたスタイルを`@Composable Div()`の`classes`で設定するのは同等のことです。


```kotlin:AppStyleSheet
    val columnContainer by style {
        flexDirection(FlexDirection.Column)
        display(DisplayStyle.Flex)
    }
```


```kotlin:Main
    Div(attrs = {
        classes(columnContainer)
    }) {
        // コンテンツの中身
    }
```

Compose for Webではこんな感じでどんどんレイアウトを作っていきます。

## テキストスタイルを設定する
テキストのフォントやフォントサイズを変更するスタイルはこんな感じで定義しました。

```kotlin
object TextStyleSheet : StyleSheet(AppStyleSheet) {
    val text by style {
        property(
            "font-family",
            "Lato, Noto Sans JP, system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif",
        )
    }
}
```

使うときはこんな感じ。正直これも`Div`でwrapしていますがあっているのかはわかりません。

```kotlin
        Div(attrs = {
            classes(TextStyleSheet.text)
        }) {
            Text("あいうえお")
        }
```

フォントはCSSでも同様に、読み込んでほしいフォントを優先順位付けして複数定義するようです。今回は`Lato`というフォントを最優先に表示するようにしています。
さきほどのレイアウト組むときのスタイルでは、`flexDirection()`などCompose側で用意されている設定を使ってスタイルを定義していきましたが、今回出てきた`property`を使えば、文字列で`font-family`などのCSSでも設定できる設定項目をComposeでも設定できます。色々試してみると面白いかもしれません。


Textにフォントサイズなどを変更させたいときはこんな感じです。

```kotlin
    val text by style {
        fontSize(24.px)
        fontWeight(800)
        property(
            "font-family",
            "Lato, Noto Sans JP, system-ui,-apple-system,BlinkMacSystemFont,Segoe UI,Roboto,Oxygen,Ubuntu,Cantarell,Droid Sans,Helvetica Neue,Arial,sans-serif",
        )
    }
```

## フッター
フッターを作るときは用意されている`@Composable Footer()`を使って作れます。
今回のポートフォリオでもフッターを実装したのですが、スタイルはこのように定義して`Footer()`に設定しました。
これで下にへばりついたコンテンツを画面に描画することができます。

```kotlin
    val footer by style {
        display(DisplayStyle.Flex)
        position(Position.Fixed)
        bottom(0.px)
        justifyContent(JustifyContent.Center)
        paddingBottom(16.px)
        alignItems(AlignItems.Center)
        width(100.vw)
    }
```

```kotlin
    Footer(attrs = { classes(AppStyleSheet.footer, TextStyleSheet.caption) }) {
        Text("© 2021 · Powered by Jetpack Compose")
    }
```

## Composeで動くボタンを作る
今回実装したページにはボタンにカーソルを当てると沈み込む「Contact me」ボタンを実装してみました。

![download.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/392121f1-fae0-e9f5-6e14-48a6c237dcbb.gif)


これは[このサイト](https://125naroom.com/web/2846)でいい感じのボタンを見つけて、それぞれCSSの設定を見ていくつか組み合わせて作りました。

ここまでくると、CSS初見の私でもだんだん理解してきて適宜読み替えてComposeの`StyleSheet`に書いて好きなように動くボタンを作ることができました。

少し定義の仕方が難しかったのはカーソルがボタンに当たったとき（ホバーされたとき）のスタイルの定義の仕方です。
これは、`hover()`という設定項目が用意されていたので、その`style`の中にホバーされたときのレイアウト設定を書きました。

今回実装した、後ろにうっすら影があってカーソルを当てたときに沈み込むボタンは以下のようなコードで実装しました。
沈み込む動作に加えて、最初はうっすらついている影(`property("box-shadow", "0px 5px 12px #CCCCCC, -6px -6px 12px #FFF")`)を沈み込んだときに影を濃くするようにしたりしてみました。(`property("box-shadow", "0px 0px 4px #000000, -2px -2px 4px #FFF")`)

```kotlin:AppStyleSheet
    val bouncedButton by style {
        display(DisplayStyle.Flex)
        justifyContent(JustifyContent.Center)
        alignItems(AlignItems.Center)
        height(44.px)
        width(240.px)
        boxSizing("border-box")
        color(Color("#000000"))
        letterSpacing(0.1.em)
        textDecoration("none")
        position(Position.Relative)
    }

    val bouncedButtonSpan by style {
        display(DisplayStyle.Flex)
        justifyContent(JustifyContent.Center)
        alignItems(AlignItems.Center)
        height(44.px)
        width(240.px)
        background("#FFFFFF")
        border {
            width = 1.px
            style = LineStyle.Solid
            color = Color("#000000")
        }
        boxSizing("border-box")
        top((-6).px)
        left((-6).px)
        property("transition-duration", "0.2s")
        property("box-shadow", "0px 5px 12px #CCCCCC, -6px -6px 12px #FFF")
        hover(self) style {
            left((-1).px)
            top((-1).px)
            property("box-shadow", "0px 0px 4px #000000, -2px -2px 4px #FFF")
        }
    }
```

```kotlin:BouncedButton
@Composable
fun BouncedButton(href: String, title: String) {
    A(
        attrs = {
            classes(AppStyleSheet.bouncedButton, AppStyleSheet.bouncedButtonSpan)
        },
        href = href,
    ) {
        Div(attrs = {
            classes(TextStyleSheet.caption)
        }) {
            Text(title)
        }
    }
}
```

ホバーされる前のボタンの背後のデザインなど少しデザインを変えて作っているとこもありますが、参考にしたCSSはこんな感じでした。
これらをいい感じに読み替えて`StyleSheet`に設定しました。

```css
section {
  max-width: 300px;
  margin: 0 auto;
}
a.btn_06 {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 50px;
  box-sizing: border-box;
  background: repeating-linear-gradient(45deg, #ffffff, #ffffff 3px, #e7e7e7 3px, #e7e7e7 30px);
  color: #333;
  font-size: 14px;
  letter-spacing: 0.1em;
  text-decoration: none;
  position: relative;
}
a.btn_06 span {
  display: flex;
  justify-content: center;
  align-items: center;
  width: 100%;
  height: 50px;
  background: #fff;
  border: 1px solid #000;
  box-sizing: border-box;
  position: absolute;
  top: -6px;
  left: -6px;
  transition-duration: 0.2s;
}
a.btn_06:hover span {
  left: -1px;
  top: -1px;
}
```

## レスポンシブ対応
今回作ったポートフォリオは自分の名前などの情報とアイコンをRowで横並びにレイアウトしたことにより、スマートフォンのサイズなど横幅が狭くなったときのレイアウトがいまいちになる問題があったのでそれを解消すべくレスポンシブ対応もしてみました。

この部分
![Screen Shot 2021-08-22 at 14.37.48.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/d038a7dd-cf45-c718-6672-3578075f3cb4.png)

画面広いとき
![Screen Shot 2021-08-22 at 14.38.20.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/59b6f151-93e8-f51e-be4f-2566d8a45cba.png)

狭いとき
![Screen Shot 2021-08-22 at 14.30.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/9f0e2884-3bd4-2b02-37dd-249f6e8c43ad.png)



幅が`640`になったらRowにしていた自分の情報のテキストとアイコンをColumnにして縦にしていい感じに見えるようにしました。
左から順番にテキスト→アイコンの順番にRowで設定していたので、そのままColumnにすると上から順番にテキスト→アイコンと表示され、個人的にアイコン→テキストの順番でレイアウトしてほしかったため、`flexDirection(FlexDirection.ColumnReverse)`を使って上からアイコン→テキストの順番でレイアウトするようにしています。

```kotlin:Container
    val bioContainer by style {
        flexDirection(FlexDirection.Row)
        display(DisplayStyle.Flex)
        alignItems(AlignItems.Center)

        media(mediaMaxWidth(640.px)) {
            self style {
                flexDirection(FlexDirection.ColumnReverse)
                display(DisplayStyle.Flex)
                alignItems(AlignItems.Center)
            }
        }
    }

```

```kotlin:BioSection
@Composable
fun BioSection() {
    Div(attrs = {
        classes(bioContainer)
    }) {
        InfoSection()
        Div(attrs = { style { marginLeft(32.px) } }) {
            Icon()
        }
    }
}
```

レスポンシブ対応後
![Screen Shot 2021-08-22 at 18.50.35.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/185927/48f83889-45e7-afce-420d-b219d58ebbcf.png)


`おわりに`のセクションでソースコードを公開しているので、詳しくはそちらを見てもらえればと思うのですが、画面幅が狭くなったときにフォントサイズを小さくしたりなどもしています。

## できたポートフォリオを公開する
さあ、Compose for Webでサイトができたら外部に公開してみましょう。

公開するときに必要なファイル群を生成するには、以下のコマンドで生成します。

```
$ ./gradlew jsBrowserProductionWebpack
```

生成されたファイルたちは`build/distributions`配下にいます。

今回私は、[GitHub Pages](https://pages.github.com/)を使って公開しました。無料で手軽にできるので最初はおすすめです。

私のGitHub Pages用のリポジトリは[こちら](https://github.com/b4tchkn/b4tchkn.github.io)です。これらは全てさきほどのコマンドで生成されたファイルたちです。

# おわりに
今回作ったComposeのコードは[ここ](https://github.com/b4tchkn/compose-portfolio)に置いてあります。少しでも参考になると嬉しいです。

今まで、スマホアプリしか作れなかった自分をWebサイトまでも作れるようにしてくれるComposeとKotlinは最高のツールだなと改めて思いました。iOSのUI部分は置いておいて、KMMも含めてこれでクライアントサイドは一通り作れるようになりそうです。

今回はシンプルな静的サイトを作ったのですが、よくあるGitHubクライアントやQiitaクライアントみたいなのもCompose for Webで次は作ってみたいですね。

# 参考
- [【CSS】デザイン崩れを防ぐ？！「リセットCSS」の使い方をわかりやすく解説！](https://osakanav.com/css-design-prevent-resetcss-how-to-use)
- [ress](https://github.com/filipelinhares/ress)
- [Compose for Web](https://compose-web.ui.pages.jetbrains.team/)
- [JetBrains/compose-jb](https://github.com/jetbrains/compose-jb)
- [Getting Started With Compose for Web](https://github.com/JetBrains/compose-jb/tree/master/tutorials/Web/Getting_Started)
- [【CSS】シンプルなボタンデザイン実装サンプル集](https://125naroom.com/web/2846)
- [CSSだけ！レスポンシブなサイトの作り方と鬼ほど使うプロパティ](https://pecopla.net/web-column/how-to-responsive)
- [Jetpack Compose for Web をやってみた](https://dev.classmethod.jp/articles/jetpack-compose-for-web-gettingstarted/)
- [CSSでフルスクリーン(全画面) ＋ 縦横中央配置レイアウトを作る【3ステップ + おまけ】](https://mae.chab.in/archives/2749)