サーバーサイドのみのテンプレートエンジンとしてのReact

最近の仕事ではJSがあんまりなくてページ数はそこそこあるみたいなサイトを作ってることが多い。作り方として、コンポーネントごとにPugのmixinとかNunjucksのmacroで抽象化してマークアップが壊れないようにしてるんだけど、これらだとコンポーネントを実装するための機能として微妙。具体的には、ノードを挿入できる箇所が1箇所に限定されてることとエディタの補完がない。

mixin Disclosure(params = {})
  -
    const props = Object.assign({
      initialExpanded: false,
      detailsId: ulid(),
    }, params)

  .Disclosure(role="group")&attributes(attributes)
    button.Disclosure__summary(type="button" aria-expanded=String(props.initialExpanded) aria-controls=props.detailsId)!= props.summaryContent
    .Disclosure__details(id=props.detailsId hidden=!props.initialExpanded)
      block

+Disclosure({ summaryContent: '最高の<em>コンテンツ</em>' }).u-mt5
  p 立派なインターネットコンテンツになったなあ。
{% macro Disclosure(params = {}) %}
{% set rootClass = params.rootClass %}
{% set initialExpanded = params.initialExpanded %}
{% set summaryContent = params.summaryContent %}
{% set detailsId = params.detailsId | default(ulid()) %}

<div class="Disclosure {{ rootClass }}" role="group">
  <button class="Disclosure__summary" type="button" aria-expanded="{{ initialExpanded }}" aria-controls="{{ detailsId }}">{{ summaryContent | safe }}</button>
  <div id="{{ detailsId }}" class="Disclosure__details" {% if not initialExpanded %}hidden{% endif %}>
    {{ caller() }}
  </div>
</div>
{% endmacro %}

{% call Disclosure({
  rootClass: 'u-mt5',
  summaryContent: '最高の<em>コンテンツ</em>'
}) %}
<p>立派なインターネットコンテンツになったなあ。</p>
{% endcall %}

例ではsummaryContentの中身が平坦化されたテキストだと限らないので、仕方なくそこだけ生のHTML書くとかになっちゃう。テンプレートエンジンの機能が使えなくなるのでコンポーネント入れ子になったりすると詰む。

VS Codeではシンタックスハイライトしてくれるだけで補完とかはなんも出ない。tsxでReact Componentのpropsの型までわかるのと比べるとだいぶ非効率的になる。Pugのmixinとかはそもそもコンポーネント専用の機能じゃないというのもあると思うし、言語のコミュニティの勢い的にも大きな進歩は望めなそう。自分で拡張書くほどのガッツもない。

じゃあもうtsxそのまま使えばいいじゃんって感じになってきたのでサーバーサイドテンプレートエンジンとしてReactを試した。<body>の中身が空の状態から始めるって話じゃなくて、この場合ではReactはクライアント側に一切介入しない。サーバーでしか動かさない。

似たところだとDocusaurusが同じような発想でReactを使ってる。Facebook製だからだと思うけど。

素直に長いものに巻かれるとNext.jsとかGatsbyJS使えばいいんだけど、あんまりJSを使わないサイトだとやり過ぎになったり、あと納品形態がいろいろなのでいろいろある(ビルド後のHTMLファイルを人間が編集できるようにしておきたい)みたいな理由。

最近Eleventyという静的サイトジェネレータが気に入ってるのでこれを使う。便利なのがテンプレートエンジンの選択肢が豊富なところで、ピュアなJavaScriptでテンプレートを書くこともできる。

import React from 'react'
import ReactDOMServer from 'react-dom/server'
import { Layout } from './components/Layout'
import { Disclosure } from './components/Disclosure'

// https://www.11ty.io/docs/data/#eleventy-provided-data-variables
type DefaultProvidedData = {
  pkg: unknown
  page: {
    url: string
  }
  collections: unknown
}

type PageData = {
  title: string
}

module.exports = class {
  data(): PageData {
    return {
      title: 'Home',
    }
  }

  render({ page, title }: DefaultProvidedData & PageData) {
    return (
      '<!doctype html>' +
      ReactDOMServer.renderToStaticMarkup(
        <Layout url={page.url} title={title}>
          <Disclosure
            rootClassName="u-mt5"
            summaryContent={
              <>
                最高の<em>コンテンツ</em>
              </>
            }
          >
            <p>立派なインターネットコンテンツになったなあ。</p>
          </Disclosure>
        </Layout>,
      )
    )
  }
}

こういうのをページごとに書いていく。よさそう。

JSXの代わりにJSXっぽい構文をTagged templatesで書けるdevelopit/htmを使えばプリコンパイルもなくせそうと思ってそれも試した。

const htm = require('htm')
const vhtml = require('vhtml')
const Layout = require('./components/Layout')
const Disclosure = require('./components/Disclosure')

const html = htm.bind(vhtml)

module.exports = class {
  data() {
    return {
      title: 'Home',
    }
  }

  render({ page, title }) {
    return (
      '<!doctype html>' +
      html`
        <${Layout} url=${page.url} title=${title}>
          <${Disclosure}
            rootClass="u-mt5"
            summaryContent=${
              html`
                <span>最高の<em>コンテンツ</em></span>
              `
            }
            ><p>立派なインターネットコンテンツになったなあ。</p><//
          >
        <//>
      `
    )
  }
}

確かにプリコンパイルはなくせたけど、そもそも解決したい補完が弱い。TypeScriptじゃないというのはあるけどもうちょっとがんばって欲しい。lit-htmlの拡張を使ってて進化を期待はできそうではある。

テンプレートの構文の細かい仕様を確認したりするのが地味にめんどい。あとReact.Fragmentみたいなやつが実装されてないとか。そういう風に考えるとしばらくはReactよりいい選択肢はなさそう。

加えて別の視点だとWeb Componentsを使って解決できる線はある気がする。コンポーネントはCustom Elementsで実装して、それをNunjucksとか今あるテンプレートエンジンで普通に使う。クライアントサイドから見ればオーバースペックだけど、JSで実装する部分があればこっちの方が安心できる。

---
layout: base
title: Home
---

<x-disclosure class="u-mt5">
  <span slot="summary">最高の<em>コンテンツ</em></span>
  <p>立派なインターネットコンテンツになったなあ。</p>
</x-disclosure>

Custom Elementsの補完は微妙だけど、開いてるファイルからcustomElements.define(...)を拾っていい感じにしてくれるようになるのを期待できなくもない。というのを書きながら希望的観測過ぎる気はしてきた。

今回試したもののソース全部入りのリポジトリ

完全に問題を解決する場所を間違えている、みたいな意識はない。

なゆくんとのおもいで

ナユコロニー2 Advent Calendar 2018の8日目の記事です。

なゆくんはおぼえていますか? ぼくです、しばらくぶりだね。しらないうちになゆくんはとうきょうではたらくようになっていたんだね。きくとそのわかさでほんをかいたというじゃないか。じつはぼくもきになってとりよせてよんでみたんだけど、せんもんがいのないようだからむずかしくてさっぱりだったよ。しらないうちにたくさんべんきょうしていたんだね。プログラマーというんだっけか? やっぱりいいね、てにしょくつけるというのは。ぼくもそういうしごとをえらべばよかったのかなあとすこしこうかいしています。さて、ぜんだんのはなしはこれくらいでいいかな。ほんだいのまえのはなしがすこしながくなってしまうことがぼくのわるいくせです。

さいしょにおぼえていますかときいたけどわざわざきくまでもないよね。なゆくんはむかしからとてもあたまのいいこだったから、ぼくがこれからなにをつたえるかなんてもうしっているよね。というよりきっと、なんねんもまえから、きょうというひにこのしらせがくるということをいちにちたりともわすれられなかったんじゃないかなあ。でもしかたない、ぼくはわるくないよ、ぜんぶなゆくんがかんがえたことなんだからね。またおしゃべりをしてしまったね、はやくほんだいをだったね。

なゆくんはあしたのしはつでじもとにかえります。ひるすぎにはついてるだろうから、ふるさとのくうきをすってかんがいぶかいきもちになったりせずに、すぐにそうこのまえまできてください。もちろんじもとのしりあいにみつかったりしたらだめだよ。ぼくはうらのあきやにかくれておもてのようすをみてるからついたらすぐにわかるとおもいます。さいきんぼくはずっとなゆくんのためにそうこのようすをしらべてあげてるんだけど、ふたつきにいっかいさとしのおばさんがみにくるだけでそれいがいのひとはよりつきもしてないね。なゆくんはほんとうにラッキーなひとだよ。

わかってるってば。なゆくんがしりたいのはそんなことじゃなくて、あのせいまいきのおくにあるたなをだれもさわってないかってことだよね。だれもあんなところにあるたなをつかおうとするはずないじゃないか。そうこはずっとぎゅうぎゅうずめでせいりするおとこでがたりないってずっとさとしのおばさんがなげいてるよ。このしごとがおわったらてつだってあげるといいんじゃないかな。もちろんぼくはごめんだけどね。どっちにしてもまずはれいのごとく、そうこにたくさんあるガラクタとゆかとのすきまをとおったらたなにかくしてあるかぎとごたいめんだ。あのやせぎすだったなゆくんがとうきょうのおいしいごはんでふとってつっかえてしまわないといいけどね。

かぎがてにはいったらどうするかはわかるよね。なゆくんはあたまがいいからちゃんとやりとげてくれるとしんじてます。ぼくだってまたともだちをなくすのはさみしいからね。

[WIP]CSSの命名について

下書き供養 Advent Calendar 2018の9日目の記事です。

CSS命名規則じゃない命名についての体系的な何かができないかを考えていた。どういう要素に命名するためにどのように言葉を選定するのか、命名という切り口で具体的に説明する文書みたいなものを見たことない気がする。

きっかけはクラス名に使える単語リストみたいなのが流れてきたのを見て感じたなんか違う感。それはそれでわかるんだけど、そもそもどういった場合にどのような種類の言葉を探すのか、要素をどの視点から捉えて説明するのかみたいな話が先にあった方が良さそうに思う。

一旦コンポーネント名の命名に基準を合わせて、UI的な命名ドメイン的な命名に分解できるのではと考えた。有名なところではMCSSのベースとプロジェクト、及びそれに影響を受けたであろうFLOCSSのComponentとProjectのレイヤー分けに近いと思う。ただしこれらはレイヤーとして分離した先にある個々のコンポーネント名についての詳細な説明がない。どちらのレイヤーに属するのかわからないと言われるのもその辺が鍵になってきそう。

  • CardとかModalとかはUI的、NewsとかUserとかはドメイン
  • 特定のドメイン知識に依存しないコンポーネント、そのサイトの中では汎用的に使えるルールのコンポーネントはUI的な命名が妥当
  • デザインの共通化が進むとUI的な命名が増えてくる
  • コンポーネントを小さく砕いて共通化していくとUI的な命名に向かいやすい
  • 規模によってはドメイン的である方がネームスペース的には安全である可能性が高い
    • 同じ機能が出現する場所はだいたい決まっているし、だいたい同じUIで表現される
    • カンプよりワイヤーフレームの方が一貫性があることが多い、表現することの密度の問題
  • ドメイン的な命名はスケールしない、反面捨てやすい
  • UI的な命名ドメイン的な命名を掛け合わせることでより端的な命名に向かうこともある
  • Articleとかどっち側? 記事というUIで記事というデータ型。そのデータ型が特定のドメイン特有のものでないのであればUI的と言っても良い?
  • CardというのかTeaserというのかとかはUI側だけどそれぞれ別の側面から見た視点っぽい。Cardはパターンっぽい、Teaserは用法っぽい。対応する両方はイコールじゃなくて全然違いものを指す可能性があるけどイコールなこともある
  • Primary buttonとBlue buttonとかも違うものだけど、ドメイン的ではないという意味では同じものとして説明できる、Blueっていう名前の商品を買わせるための専用ボタンなのであればドメイン的かもしれない
  • ややこしい用語があったりするとなにがUI的な言葉でなにがドメイン的な言葉なのかプロジェクトごとにはっきりさせとく必要があることもありそう

しばらくはまとまる気配がない。いつか更新するかもしれないし永遠にしないかもしれない。

たまごまぜごはん

TKG Advent Calendar 2018の2日目の記事です。

人が卵かけご飯と言ったとき、僕の頭の中で連想されるのは「卵まぜご飯」と呼んだ方が適切であろうと思われるものだ。幼少期に母親の紹介を経ていわゆる卵かけご飯との出会いを果たしたとき、それは当たり前のように混ぜ合わされた後の状態になっていて、さも混ぜ合わされるという工程を経て初めて卵かけご飯という名を授かるのだと思わされた。

ご飯とおかずは均等な味の比率を保っていなければならない。当時の僕は暗黙的にこのようなルールを遵守していた。特定のおかずにご飯と合う種類の味がどの程度含まれているかによって同時に口に含むべきご飯の量は決まる。これは単なる目安というより、可能な限り守られなければならない規定だった。特定のおかずに対して必要な量のご飯が用意されていないとき、味の総量としてご飯を超えるだけのおかずは食べることができない。そしてそのルールを犯してしまうことに強い罪悪感があった。

チキンライスやチャーハンなどの色付きご飯は例外として扱われた。ルールに則るならば、その味付けの量が対応するご飯の量を超えている場合は均等になるだけの「白いご飯」を増やして中和するというのが筋だ。しかし僕の家庭では色付きご飯の日には炊飯器の釜は「洗い待ち」状態で食事が開始された。これは色付きご飯の日には白いご飯を提供する意思がないということを示す。僕はこの色付きご飯の場合にはルールに当てはまらないとして自分を納得させた。従って色付きご飯の日に白いご飯を食べることができる状態になっていても味を中和させることは必要ではない。

他に少し複雑な対応として、ちょっと茶色いキノコ混ぜ炊き込みご飯のような、色付きご飯ではありながら味の比率としてご飯の方が多いという薄い色付きご飯パターンもあった。これまでのルールに則れば、薄い色付きご飯にはおかずの味の量が不足しているためその分の別のおかずが入り込む余地があるはずだ。つまり特定のおかずを食べるときに、白いご飯の場合に比べれば必要なご飯の量は増えるが、薄い色付きご飯でも味の比率を均等にするルールを守ることができる。しかし当時の僕には別のルールとして、別の種類のおかずの味を混ぜてはならないという決まりがあった。そのため薄い色付きご飯を別のおかずと同時に食べると規定違反になる。

ただそうはいってもこれではその日の食事を食べることができない。これに対する対策として、僕は薄い色付きご飯は白いご飯と同一であるというように解釈した。このご飯には一切の色などなくて、白いご飯とまったく変わりがないのだと。もちろんこの解釈には無理があった。強く念じようとも薄い色付きご飯はやはりおかずとしての佇まいを残しているのだ。僕はこれに対する打開策を講じることができず、薄い味付けご飯の日は「捨て」であるということにした。

こうした背景を振り返って自分はなぜ卵かけご飯は卵混ぜご飯であると捉えているのかが分析できてきた。まず卵の味がご飯全体に均等に浸透していなければ食べることができない。それに塩や醤油が混ざると色付きご飯のパターンとして取り扱われる。そして色付きご飯であるということはこれまでのあらゆるルールの免罪符となり無限の進化の可能性を獲得するのだ。

卵と納豆を混ぜたご飯に目玉焼きを乗せて食べました

CSS in JSはCSSの書き方をどのように変えるのか

CSSの難しさの根源はセレクタにある。CSS設計のための方法論ではどのようにしてセレクタと関わるべきかについて語られる。

その関わり方がCSSのみで実現できなければならないという制約を捨てたのがいわゆるCSS in JSの類(定義的に微妙なやつも全部ひっくるめて)だ。可能性は一気に広がり無数のライブラリが生み出された。

ある程度の期間を経ていくつかの着目すべきアプローチが見えてきた。これから僕はどのようにセレクタと関わっていくべきかという視点で記してみたい。

擬似スコープ

通常CSSセレクタにはスコープはないが、HTMLやCSSハッシュ値を付与して特定のコンテキストを擬似的に閉じてしまおうというアイデア。実装としては、Vue.jsの単一ファイルコンポーネントAngularのコンポーネントスタイルstyled-jsxなど。関連するウェブ標準技術としてShadow DOMがある。

例えば、次のように書かれたスタイル宣言は同一コンポーネント内にしか適用されない。

<template>
  <p>Hello World!</p>
</template>

<style scoped>
p {
  font-size: 2em;
  text-align: center;
}
</style>

このコンポーネントの外にp要素があったとしても適用範囲外になる。

書き方としては、CSS in JSの類のアプローチの中では最もこれが普通のCSSに近い。スコープがあるということ以外、実質的に変わりがない。セレクタがグローバルであるという難しさを解消するためにスコープを作るということは、発想としてごく普通で受け入れられやすい。

BEMで規約として作っていたブロックというルールを仕組みとして取り入れたとも言える。BEMらしき姿は見えなくなっていても、これはより安全で楽になったBEMだ。

安パイだと思う。

スタイル宣言が付属したHTML要素を作る

styled-componentsを使うと、最初にスタイル宣言が付属したReactコンポーネントを作成し、それを配置していくという形でスタイリングを行うことになる。

import React from 'react'
import styled from 'styled-components'

const Wrapper = styled.button`
  background-color: lightgray;
  border: 1px solid gray;
`

const Icon = styled.img`
  width: 1.25em;
  height: 1.25em;
`

const Text = styled.span`
  margin-left: 1em;
`

const Button = () => {
  return <Wrapper type="button">
    <Icon src="/icon.svg" alt="" />
    <Text>click me</Text>
  </Wrapper>
}

従って、個々の宣言ブロックの中身を実装した後に作成した要素を配置して画面を組み上げていくという流れになるのが自然だ。通常、スタイリングはHTMLを書いた後に行う。対して、styled-componentsは宣言の記述のために個々の要素のReactコンポーネント化を要求するため、全体のHTMLが完成する前に個々の宣言ブロックの中身を実装することに意識を向けなければならない。

しかし実際にそのような流れで実装することには無理がある。そのためstyled-componentsを利用したスタイリングには不自然さが伴う。

宣言ブロックの中身は適用される対象の要素だけを見ても完成させられない。継承させるプロパティの兼ね合いや、兄弟要素とのレイアウト上の関係性など、対象となっている要素以外の要素も意識しなければどのような宣言をするかは決められない。

またこうして作成したReactコンポーネントを配置していく際にも難がある。どこにどの要素を配置するかは、それぞれのHTML要素の種類に依存して決まる。にも関わらずこれらを配置していく際には要素の種類が一目ではわからなくなる。さらには単なるスタイルが付属するコンポーネントなのか別の場所から読み込んだ真っ当なコンポーネントなのかも不明瞭になる。

分解したReactコンポーネントを利用するとき、その中の実装が見えないというゆえの扱いにくさがある。先述したようなHTMLやCSSとしての構造の問題がその一部だが、大抵はそれを上回るだけのコンポーネント化による利点がある。しかしstyled-componentsは全てのHTML要素をReactコンポーネント化することを要求してくる。剥き出しになっていて欲しい部分も覆い隠されてしまう。

最初にHTMLだけを組み上げた後に個々の要素をstyled-componentsに置き換えることもできるが、明らかに非効率的だ。結局は読みにくくもなってしまう。

ごく小さなコンポーネントであればともかく、HTMLとしてそれなりの大きさのコンポーネントを実装しているとまるでもう無理になってしまった。

HTML要素にスタイル宣言をリンクする

宣言ブロックを基にしてそれに対応するユニークなクラス名を生成するというアイデアもある。そのクラス名はHTML要素のclass属性を通してリンクされる。

/* style.css */
.wrapper {
  background-color: lightgray;
  border: 1px solid gray;
}

.icon {
  width: 1.25em;
  height: 1.25em;
}

.text {
  margin-left: 1em;
}
// button.js
import styles from "./style.css"

const Button = () => {
  return <button className={styles.wrapper} type="button">
    <img className={styles.icon} src="/icon.svg" alt="" />
    <span className={styles.text}>click me</span>
  </button>
}

これはCSS Modulesの例。

生成されるクラス名をCSSファイルから読み込み、対象の要素と宣言ブロックを直接繋ぎ合わせる。CSSファイル内のセレクタのようなものはクラス名がマッピングされるオブジェクトのキーに過ぎない。

セレクタをユーザーから隠して宣言ブロックと直接リンクさせるようにしたという意味ではstyled-componentsに近い。このアイデアが優れているのは、単にリンクさせるようにしただけであるというところ。

セレクタというのは書きやすくかつ読みにくいものだ。付属する宣言ブロックがどの要素に適用されるものなのか、実際に実行してみるまで本当に信頼はできない。このアプローチでは、スタイルを適用するためにはクラス名となる文字列を対象の要素に直接繋ぎ合わせるという前提ができることで、コンパイル前の段階でその関係性を確実に保証できる。

ただしCSS Modulesはビルドを複雑にしてしまう。styled-componentsが流行った理由は採用の手軽さにもあるのだろう。

幸い、このようにスタイル宣言をリンクさせるというアプローチが可能なライブラリとしてemotionがある。emotionはstyled-componentsと同等の機能の他に、The css Propという宣言ブロックを基にユニークなクラス名を生成できる機能を備えている。

import React from 'react'
import { css } from 'emotion'

const wrapperClass = css`
  background-color: lightgray;
  border: 1px solid gray;
`

const iconClass = css`
  width: 1.25em;
  height: 1.25em;
`

const textClass = css`
  margin-left: 1em;
`

const Button = () => {
  return <button className={wrapperClass} type="button">
    <img className={iconClass} src="/icon.svg" alt="" />
    <span className={textClass}>click me</span>
  </button>
}

さらにこのようにJavaScriptでスタイル宣言を管理することで、未使用の宣言ブロックを検出できるようになるという利点がある。ESLintやTypeScriptを使用すれば、デザインの変更時などに不要になったスタイルを確実に取り除けるようになる。コードを整理するという観点から、これまでのCSSではとても実現できなかったことだ。

感想

CSS in JSとかそんなものやめてしまえけしからんという気持ちもわかる。けど、これまでのCSSで事足りるというのは大抵、道具として完璧で最高にフィットしてるという感じでなくて、まあまあ不満もあるけど妥協してやっていけるよくらいの温度感のはずだ。

僕自身これ系のものを割と食わず嫌いしていたけどそれなりに学ぶこともあった。ので、とりあえずこれでなにか書いてみればよいのではという感想。

帰ってきた彼

姿を消したのは一夜の夢だったかのように、僕はあのころと何も変わらない彼と話をしていた。慣れというのは恐ろしいものだ。彼は再び当たり前の存在に舞い戻った。

「今でもそれを転がしているし、それなりに好きだよ」という話を聞いた。昔の僕は少し気負い過ぎていたせいか、反射的にそれを否定してしまっていた。とはいえ僕も少しは大人になれたのかもしれない。「君が好きならいいと思うよ」。投げやりになったのではなくて、本心からそんな言葉が出た。

たぶん人は自分の姿を消してしまいたいと思うことがたまにあるのだろう。事情はいろいろあるかもしれないけど、そうしたければそうするしかない。これまでそこにいた人が翌日すっかり影も形もなくなってしまっても、僕たちはそれを受け入れる他ないのだ。

彼はそのことを悪びれる様子もなく、その必要もないと思わされた。僕は日本人の平均以上には自由に生きているという自負があったが、それでも自分にまとわりついている鎖の重さを再確認することになった。

なにはともあれ彼には姿を消すという実績ができた。以降はもうないのかもしれないけど、一度あったことなので二度目もあるかもしれない。きっとそのときも僕にはなにもできないのだろう。僕が彼の失踪から学んだのは、諦めるのは諦めないのと同じくらい重要であるということだ。

それでも彼は今ここにいる。僕は楽しくやれてるし、彼の気持ちはわからないが、再び姿を消すようなことはしばらくしないはずだ。しばらく経ってからのことはわからないが。けれど今が楽しくやれてるということはおそらく素晴らしいことなのだ。今を感じることができるのは今だけなのだから。

より良いリンクの下線の実装

前回、リンクには下線を付けようという記事を書いた。が、実際のところ、デフォルトのリンクのスタイルはあまりイケてないと思ってる。リンク色と同色の下線は視覚的な主張として強すぎるし、下線の位置が文字の下端に隣接し過ぎていて見づらい。

幸いにも、CSS Text Decoration Moduleにはあまり知られていない便利なプロパティがあり、この野暮ったさを解消できそうに見える。text-decoration-colorでは下線の色を指定できる。IE以外では実装されているので、プログレッシブエンハンスメントということにすれば問題無さそうだ。

a {
  color: hsl(240, 100%, 47%);
  text-decoration: underline;
  text-decoration-color: hsla(240, 100%, 47%, 0.5);
}

text-underline-positionを使えば下線の位置を変更できる。次の宣言によってテキストの下に下線を配置できる。このプロパティの値は継承されるので、html要素に指定しておくと良いだろう。

html {
  text-underline-position: under;
}

ただし、こちらはChromeにしか実装されていない。手軽にはできるので、一部のユーザーだけでもいい感じにしたいのであればこれを指定しておくと良いかもしれない。

これらはCSS Text Decoration Module Level 3の範囲だが、CSS Text Decoration Module Level 4を眺めてみるともう少し可能性が広がりそうだ。前述の内容に関連したところだと、text-underline-offsetは下線の位置を<length>型で指定できる。また、text-decoration-widthでは下線の幅を指定できる。下線を引くためにborderとかでがんばるのはあまりきれいじゃないと思っているので、この辺には期待したいところ。

現段階において独自の下線を実装するためには、やはりborderを使うのが一番簡単だ。もし、文字のディセンダーを下線が横切らないようにしたいということなら、text-decoration-skip-inkとか、text-shadowlinear-gradient()を使ったハックがある。が、単に文字とぶつからないだけ下線の位置を下げるのが一番単純である。borderといっしょにpaddingを指定してやればいいだけだ。

a {
  padding-top: 2px;
  padding-bottom: 2px;
  color: hsl(240, 100%, 47%);
  text-decoration: none;
  border-bottom: 1px solid hsla(240, 100%, 47%, 0.5);
}

ターゲットサイズが下にだけ伸びしてまうのが気持ち悪いので、padding-bottomと同じサイズをpadding-topにも指定している。

デフォルトの下線に比べて、視覚的にも調整できたしテキストは見やすくなった。

見た目を調整してあげれば下線は取り除いてしまわなくても済みそうだ。

で、どうですか?