◆ lit-html の styleMap だと擬似クラスや子要素がサポートされないなど 機能不足なところがある
◆ Emotion を組み合わせる

スタイル適用の不便なところ

最近は lit-html と lit-element が統合された lit になってますが lit-element を使うほどでもない小さいページだと lit-html を直接使ってます
lit-html だけでも使い回す部分はテンプレートを関数に切り出して使い回すことはできます

const userTemplate = (user) => html`
<section>
<h1>${user.name}</h1>
<p>${user.description}</p>
</section>
`

const template = (values) => html`
<div>
<div>...</div>
${userTemplate(values.user)}
<div>...</div>
<div>...</div>
</div>
`

ただ コンポーネントではないので スタイルを考えると困ったことがおきます

const userTemplate = (user) => html`
<style>
section {
border: 1px solid silver;
}
section:hover {
background: #fcf;
}
h1 {
font-size: 18px;
}
p {
font-size: 12px;
color: #666;
}
</style>
<section>
<h1>${user.name}</h1>
<p>${user.description}</p>
</section>
`

こう書くことはできますが style タグの中身はこの section に限らずグローバルに適用されます
さらに userTemplate を一つのページで何度も使うと この style タグが毎回出力されてしまいます

lit-html で作るような小さいページなら コンポーネントに分割しないグローバルな CSS ファイルを用意してページ全体で読み込むというのもありかもしれません
でも 名前の重複を避けるために長い名前になるのは気が進みません
手動で入力も面倒ですし 名前を考えるのも面倒です

名前をつけずに HTML タグに直接スタイルを記述することは lit-html でも対応していて styleMap というディレクティブが使えます

html`
<div style=${styleMap({ color: "red", fontSize: "12px" })}></div>
`

オブジェクトの一部や全体を変数として外部におけば クラスのように名前をつけて参照することもできます
その時の状態に応じて動的にスタイルを変えることもできます

ですが この手のライブラリにあるような 擬似クラスや子要素のサポートはありません
↓ のようなことができればいいのですが styleMap の機能ではできません

html`
<div style=${styleMap({
color: "red",
fontSize: "12px",
":hover": {
color: "blue",
},
"& span": {
margin: "5px"
}
})}></div>
`

Emotion

lit-html はコンパクトさを売りにしているので こういう複雑な機能がないのは仕方ないです
将来的にも対応はしないでしょう
lit-html だけでやるのは諦めて 他のライブラリに頼ることにします

こういう書き方ができるもので思い当たるのは React でたまに見かける Emotion です
https://emotion.sh/docs/introduction

React 以外でも使えるのか調べてみると @emotion/css を使えば React なしで使えるようでした

このライブラリを最初に知ってからもう結構経っているので 流行り廃りの激しいこの界隈だと もう次の類似ライブラリがあるかもしれません
一応類似ライブラリを探してみると 今のところは Emotion がトップクラスでした
MUI も最近のメジャーアップデートで JSS から Emotion に移行したくらいですし しばらくは廃れないでしょう
ということで Emotion を採用します

使い方は単純で css 関数を使います

css({
color: "red",
fontSize: "12px",
})

すると head タグ内にこういう style タグが自動で追加されます

<style data-emotion="css" data-s="">.css-1i02r2p{color:red;font-size:12px;}</style>

自動で css-1i02r2p というクラス名が作られています
css 関数の返り値がこのクラス名なので これを div などのクラス名として設定すればスタイルを適用できます

ネストにも対応しているので さっき上で書いた例がほぼそのまま動きます

html`
<div class=${css({
color: "red",
fontSize: "12px",
"&:hover": {
color: "blue",
},
"& span": {
margin: "5px"
}
})}></div>
`

オブジェクト記法だけでなく テンプレートリテラルで CSS を書く記法もあります

css`
color: red;
font-size: 12px;
&:hover {
color: blue;
}
`

lit-element の css 関数はテンプレートリテラル用なので それに近い感じにできるテンプレートリテラルのほうがいいかもしれません

lit-html と Emotion をあわせて使った簡単な例です
プレビューできます

Emotion を使ってスタイルを当てたユーザ情報を表示するブロックを並べて表示するだけです
radio ボタンを切り替えると ユーザ表示が横に並ぶか縦に並ぶかを切り替えられます

devtools で確認するとわかりますが userTemplate は何度か呼び出されるのに同じ内容の style タグは head タグの中に 1 つだけになっています

Emotion は babel を想定しているみたいで process.env.NODE_ENV を参照する部分があります
直接 ES Modules で使うとここでエラーがでるので ダミーのオブジェクトを事前に作って回避しています

<!DOCTYPE html>
<meta charset="utf-8" />

<script>
// emotion 用
window.process = { env: {} }
</script>
<script type="module">
import { html, render } from "https://unpkg.com/lit-html@2.1.1/lit-html.js"
import { css, cx } from "https://unpkg.com/@emotion/css@11.7.1/dist/emotion-css.esm.js?module"

const values = {
users: [
{
name: "user1",
description: "description".repeat(5),
},
{
name: "user2",
description: "description".repeat(4),
},
{
name: "user3",
description: "description".repeat(7),
},
],
direction: "row",
}

const userTemplate = (user) => html`
<section class=${css`
border: 1px solid silver;
&:hover {
background: #fcf;
}
& h1 {
font-size: 18px;
}
& p {
font-size: 12px;
color: #666";
}
`}>
<h1>${user.name}</h1>
<p>${user.description}</p>
</section>
`

const radioTemplate = (items, name, selected, onchange) => items.map(item => html`
<label>
<input
type="radio"
name=${name}
?checked=${selected === item}
@change=${() => onchange(item)}
>
${item}
</label>
`)

const template = () => html`
<div>
<div>
${radioTemplate(
["row", "column"],
"direction",
values.direction,
update(selected => values.direction = selected)
)}
</div>
<div class=${css`
display:flex;
flex-direction: ${values.direction};
gap:10px;
align-items: flex-start;
`}>
${values.users.map(user => userTemplate(user))}
</div>
</div>
`

const update = (fn) => (...a) => {
fn && fn(...a)
render(template(), root)
}

update()()
</script>

<div id="root"></div>