Tailwind List: 17 Examples of Styled, Stacked and Todo Lists

Drop a plain ul into a Tailwind project and the bullets are gone. Preflight, Tailwind's base stylesheet, sets list-style to none on ol, ul and menu elements, so every list renders without markers until a class adds them.

Below are 17 Tailwind list examples you can copy, followed by a guide to markers, spacing, custom bullets and what changed in v4.

By Bogdan Sandu ยท Updated October 29, 2026

Every example is built with Tailwind CSS v4.3 utility classes. To try one quickly, add the Play CDN tags below to your page and paste the example's HTML, CSS and JS. The dark variant line makes dark: classes follow a .dark class instead of the system setting. For production, install Tailwind with Vite or the CLI.

<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style type="text/tailwindcss">
  @custom-variant dark (&:where(.dark, .dark *));
</style>

Markers, spacing and icon lists

Preflight strips every bullet and number, so these demos bring them back on purpose: list-disc and list-decimal, the marker: variant, inside versus outside positions, SVG check icons, a CSS counter and the space-y versus gap difference that v4 changed.

Bullets, numbers and nested markers

  • list-disc
  • list-decimal
  • list-[circle]
  • marker:
  • pl-5

Preflight sets list-style: none on ul and ol, so each list names its marker again: list-disc, list-decimal or an arbitrary list-[circle] and list-[square] for deeper levels. The marker: variant targets ::marker on the list and on its descendants, so one marker:text-rose-500 on the ul colors every bullet while the text keeps its own color. pl-5 gives outside markers room, because Preflight also zeroes the padding.

View code
<div class="grid w-full max-w-3xl gap-4 md:grid-cols-3">
  <section class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
    <h3 class="text-base font-semibold text-slate-900">Day hike packing list</h3>
    <p class="mt-1 font-mono text-xs text-slate-600">list-disc marker:text-rose-500</p>
    <ul class="mt-4 list-disc space-y-1.5 pl-5 text-sm text-slate-700 marker:text-rose-500">
      <li>Rain shell and a wool mid layer</li>
      <li>Headlamp with spare batteries</li>
      <li>Paper map of the valley trails</li>
      <li>Two liters of water per person</li>
    </ul>
  </section>
  <section class="rounded-2xl bg-amber-50 p-5 ring-1 ring-amber-200">
    <h3 class="text-base font-semibold text-amber-950">Pour-over in four steps</h3>
    <p class="mt-1 font-mono text-xs text-amber-900">list-decimal marker:font-bold</p>
    <ol class="mt-4 list-decimal space-y-1.5 pl-5 text-sm text-amber-950 marker:font-bold marker:text-amber-800">
      <li>Grind 18 g of coffee medium-fine</li>
      <li>Rinse the paper filter with hot water</li>
      <li>Bloom with 40 g of water for 30 seconds</li>
      <li>Pour to 300 g in slow circles</li>
    </ol>
  </section>
  <section class="rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200">
    <h3 class="text-base font-semibold text-slate-900">Portfolio site map</h3>
    <p class="mt-1 font-mono text-xs text-slate-600">disc, [circle], [square]</p>
    <ul class="mt-4 list-disc space-y-1.5 pl-5 text-sm text-slate-700 marker:text-teal-600">
      <li>Work
        <ul class="mt-1.5 list-[circle] space-y-1.5 pl-5">
          <li>Case studies
            <ul class="mt-1.5 list-[square] space-y-1.5 pl-5">
              <li>Harbor Line rebrand</li>
              <li>Fieldnote mobile app</li>
            </ul>
          </li>
          <li>Process</li>
        </ul>
      </li>
      <li>Journal</li>
      <li>Contact</li>
    </ul>
  </section>
</div>

list-outside versus list-inside

  • list-outside
  • list-inside
  • aria-pressed:
  • outline-dashed

Switch between the two positions.

The dashed outline shows the ul box. With list-outside and pl-5 the markers sit in the padding and wrapped lines align under the first word, a hanging indent. list-inside puts the marker inside the text line, so no padding is needed but wrapped lines run under the bullet. The two buttons swap complete class names and carry aria-pressed, which also drives their selected look through aria-pressed:bg-white.

View code
<div class="w-full max-w-md rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <h3 class="text-base font-semibold text-slate-900">Launch checklist</h3>
    <div class="inline-flex rounded-lg bg-slate-100 p-1 text-sm font-medium" role="group" aria-label="Marker position">
      <button type="button" data-pos="outside" aria-pressed="true" class="tls-pos-btn min-h-9 rounded-md px-3 font-mono text-xs text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-pressed:bg-white aria-pressed:text-slate-900 aria-pressed:shadow-sm">list-outside</button>
      <button type="button" data-pos="inside" aria-pressed="false" class="tls-pos-btn min-h-9 rounded-md px-3 font-mono text-xs text-slate-600 hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-violet-600 aria-pressed:bg-white aria-pressed:text-slate-900 aria-pressed:shadow-sm">list-inside</button>
    </div>
  </div>
  <ul id="tls-pos-list" class="mt-5 list-outside list-disc space-y-3 pl-5 text-sm/6 text-slate-700 outline-1 outline-offset-4 outline-violet-300 outline-dashed marker:text-violet-600">
    <li>Run the accessibility pass on every template, including the empty states and the 404 page.</li>
    <li>Convert hero images to WebP and keep the largest one under 200 KB.</li>
    <li>Point the old blog URLs at their new paths with 301 redirects.</li>
  </ul>
  <p id="tls-pos-caption" class="mt-6 rounded-lg bg-violet-50 px-3 py-2 text-xs/5 text-violet-900" aria-live="polite">list-outside pl-5: wrapped lines line up with the first line of text.</p>
</div>

Pricing feature checklist with SVG checks

  • role="list"
  • flex gap-3
  • size-5
  • sr-only
  • md:grid-cols-[1fr_1.2fr]

Icon lists keep Preflight list-style: none and draw the marker as an inline SVG in a flex row, so the check takes size-5 and text-emerald-600 like any icon. role="list" keeps the list semantics that Safari VoiceOver drops from unstyled lists, and the excluded features carry sr-only text so the cross icon is not the only signal. The grid splits into plan and features from md: up.

View code
<div class="grid w-full max-w-3xl overflow-hidden rounded-3xl bg-white shadow-sm ring-1 ring-stone-200 md:grid-cols-[1fr_1.2fr]">
  <div class="bg-stone-900 p-6 text-stone-100 sm:p-8">
    <h3 class="text-lg font-semibold text-white">Studio plan</h3>
    <p class="mt-2 text-sm/6 text-stone-300">For small teams that ship client sites every month.</p>
    <p class="mt-6 flex items-baseline gap-1"><span class="text-4xl font-semibold tracking-tight text-white">$24</span><span class="text-sm text-stone-300">per editor / month</span></p>
    <a href="#" class="mt-8 flex min-h-11 items-center justify-center rounded-xl bg-lime-300 px-4 text-sm font-semibold text-stone-900 hover:bg-lime-200 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-lime-300">Start a 14-day trial</a>
  </div>
  <div class="p-6 sm:p-8">
    <h4 class="text-sm font-semibold text-stone-900">Everything in Starter, plus</h4>
    <ul role="list" class="mt-4 space-y-3 text-sm/6 text-stone-700">
      <li class="flex gap-3">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mt-0.5 size-5 flex-none text-emerald-600" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg>
        Unlimited client projects and staging links
      </li>
      <li class="flex gap-3">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mt-0.5 size-5 flex-none text-emerald-600" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg>
        Shared component library with version history
      </li>
      <li class="flex gap-3">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mt-0.5 size-5 flex-none text-emerald-600" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg>
        Custom domains with automatic HTTPS
      </li>
      <li class="flex gap-3">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="mt-0.5 size-5 flex-none text-emerald-600" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg>
        Client review mode with pinned comments
      </li>
      <li class="flex gap-3 text-stone-500">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="mt-0.5 size-5 flex-none text-stone-400" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg>
        <span><span class="sr-only">Not included: </span>Single sign-on (Agency plan)</span>
      </li>
      <li class="flex gap-3 text-stone-500">
        <svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" class="mt-0.5 size-5 flex-none text-stone-400" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg>
        <span><span class="sr-only">Not included: </span>Audit log export (Agency plan)</span>
      </li>
    </ul>
  </div>
</div>

Ordered steps with a CSS counter

  • before:
  • after:
  • last:after:hidden
  • counter()
  • aria-current="step"

The ol keeps its semantics with role="list" while a CSS counter replaces the native numbers: three short rules reset, increment and print counter(tls-step, decimal-leading-zero) into ::before. Everything else on the badge is a before: utility (size, circle, colors per step state), and after: draws the connector line, hidden on the last step with last:after:hidden. The current step carries aria-current="step".

View code
<div class="w-full max-w-md rounded-2xl bg-white p-6 shadow-sm ring-1 ring-slate-200">
  <h3 class="text-base font-semibold text-slate-900">Open your online shop</h3>
  <p class="mt-1 text-sm text-slate-600">Two of five steps done.</p>
  <ol role="list" class="tls-steps mt-6">
    <li class="relative pb-7 pl-14 before:absolute before:top-0 before:left-0 before:flex before:size-10 before:items-center before:justify-center before:rounded-full before:bg-emerald-600 before:text-sm before:font-semibold before:text-white after:absolute after:top-11 after:bottom-1 after:left-5 after:w-0.5 after:bg-emerald-600 last:after:hidden">
      <h4 class="pt-0.5 text-sm font-semibold text-slate-900">Create your account</h4>
      <p class="mt-0.5 text-sm text-slate-600">Store name, currency and time zone.</p>
    </li>
    <li class="relative pb-7 pl-14 before:absolute before:top-0 before:left-0 before:flex before:size-10 before:items-center before:justify-center before:rounded-full before:bg-emerald-600 before:text-sm before:font-semibold before:text-white after:absolute after:top-11 after:bottom-1 after:left-5 after:w-0.5 after:bg-slate-200 last:after:hidden">
      <h4 class="pt-0.5 text-sm font-semibold text-slate-900">Add your first products</h4>
      <p class="mt-0.5 text-sm text-slate-600">Photos, prices and stock levels.</p>
    </li>
    <li aria-current="step" class="relative pb-7 pl-14 before:absolute before:top-0 before:left-0 before:flex before:size-10 before:items-center before:justify-center before:rounded-full before:bg-white before:text-sm before:font-semibold before:text-indigo-700 before:ring-2 before:ring-indigo-600 after:absolute after:top-11 after:bottom-1 after:left-5 after:w-0.5 after:bg-slate-200 last:after:hidden">
      <h4 class="pt-0.5 text-sm font-semibold text-indigo-700">Set up shipping zones</h4>
      <p class="mt-0.5 text-sm text-slate-600">Rates for domestic and EU orders.</p>
      <a href="#" class="mt-3 inline-flex min-h-10 items-center rounded-lg bg-indigo-600 px-3.5 text-sm font-semibold text-white hover:bg-indigo-500 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-indigo-600">Continue setup</a>
    </li>
    <li class="relative pb-7 pl-14 before:absolute before:top-0 before:left-0 before:flex before:size-10 before:items-center before:justify-center before:rounded-full before:bg-slate-100 before:text-sm before:font-semibold before:text-slate-600 after:absolute after:top-11 after:bottom-1 after:left-5 after:w-0.5 after:bg-slate-200 last:after:hidden">
      <h4 class="pt-0.5 text-sm font-semibold text-slate-700">Connect payments</h4>
      <p class="mt-0.5 text-sm text-slate-600">Cards, bank transfer or cash on pickup.</p>
    </li>
    <li class="relative pb-7 pl-14 before:absolute before:top-0 before:left-0 before:flex before:size-10 before:items-center before:justify-center before:rounded-full before:bg-slate-100 before:text-sm before:font-semibold before:text-slate-600 after:absolute after:top-11 after:bottom-1 after:left-5 after:w-0.5 after:bg-slate-200 last:pb-0 last:after:hidden">
      <h4 class="pt-0.5 text-sm font-semibold text-slate-700">Publish the shop</h4>
      <p class="mt-0.5 text-sm text-slate-600">Pick a domain and go live.</p>
    </li>
  </ol>
</div>

space-y versus gap with a hidden item

  • space-y-3
  • flex flex-col gap-3
  • hidden attribute
  • aria-pressed

Hide the last item and compare the bottom edges.

In v4 space-y-3 adds margin-bottom to every child that is not :last-child. Hide the last item and the one before it still keeps its margin, so a trailing 12px gap appears inside the list. A flex column with gap-3 only spaces visible items, which is why the v4 upgrade guide suggests flex and gap. The JS only toggles the hidden attribute and measures the leftover space.

View code
<div class="w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex flex-wrap items-center justify-between gap-3">
    <h3 class="text-base font-semibold text-slate-900">Same list, two spacing methods</h3>
    <button type="button" id="tls-sp-toggle" aria-pressed="false" class="min-h-10 rounded-lg px-3.5 text-sm font-semibold text-sky-800 ring-1 ring-sky-300 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-sky-600 aria-pressed:bg-sky-700 aria-pressed:text-white aria-pressed:ring-sky-700">Hide last item</button>
  </div>
  <div class="mt-5 grid gap-5 sm:grid-cols-2">
    <div>
      <p class="flex justify-between font-mono text-xs text-slate-700"><span>space-y-3</span><span>trailing <span id="tls-sp-space-out">0px</span></span></p>
      <ul id="tls-sp-space" role="list" class="mt-2 space-y-3 rounded-xl bg-sky-100 p-3 text-sm text-slate-800">
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Wireframes approved</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Copy deck in review</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Icons exported</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Staging build</li>
      </ul>
    </div>
    <div>
      <p class="flex justify-between font-mono text-xs text-slate-700"><span>flex flex-col gap-3</span><span>trailing <span id="tls-sp-gap-out">0px</span></span></p>
      <ul id="tls-sp-gap" role="list" class="mt-2 flex flex-col gap-3 rounded-xl bg-emerald-100 p-3 text-sm text-slate-800">
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Wireframes approved</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Copy deck in review</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Icons exported</li>
        <li class="rounded-lg bg-white px-3 py-2 shadow-xs">Staging build</li>
      </ul>
    </div>
  </div>
</div>

Wrapping tag list with remove buttons

  • flex flex-wrap
  • gap-2
  • ring-inset
  • <template>
  • aria-live

Add a tag or remove one.

A horizontal list is just flex flex-wrap gap-2 on the ul, and the tags wrap to new rows at any width without media queries. Each remove button has an aria-label with the tag name, and after a removal focus moves to the next tag or back to the input. New tags are cloned from a template element, so the JS never builds markup from strings.

View code
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 class="text-base font-semibold text-slate-900">Article tags</h3>
  <p class="mt-1 text-sm text-slate-600">Up to 12 tags. Readers see them under the title.</p>
  <ul id="tls-tag-list" role="list" class="mt-4 flex flex-wrap gap-2">
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">CSS Grid</span><button type="button" aria-label="Remove tag CSS Grid" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Responsive design</span><button type="button" aria-label="Remove tag Responsive design" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Typography</span><button type="button" aria-label="Remove tag Typography" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Accessibility</span><button type="button" aria-label="Remove tag Accessibility" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Dark mode</span><button type="button" aria-label="Remove tag Dark mode" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Design tokens</span><button type="button" aria-label="Remove tag Design tokens" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
    <li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name">Forms</span><button type="button" aria-label="Remove tag Forms" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li>
  </ul>
  <template id="tls-tag-tpl"><li class="inline-flex items-center gap-1 rounded-full bg-violet-50 py-1 pr-1 pl-3 text-sm font-medium text-violet-800 ring-1 ring-violet-200 ring-inset"><span class="tls-tag-name"></span><button type="button" class="tls-tag-remove grid size-6 place-items-center rounded-full text-violet-600 hover:bg-violet-200 hover:text-violet-900 focus-visible:outline-2 focus-visible:outline-violet-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.75" stroke-linecap="round" class="size-3.5" aria-hidden="true"><path d="M4.5 4.5l7 7M11.5 4.5l-7 7"/></svg></button></li></template>
  <form id="tls-tag-form" class="mt-5 flex gap-2">
    <label for="tls-tag-input" class="sr-only">New tag</label>
    <input id="tls-tag-input" type="text" maxlength="24" autocomplete="off" placeholder="Add a tag" class="min-h-11 min-w-0 flex-1 rounded-lg border border-slate-300 bg-white px-3 text-sm text-slate-900 placeholder:text-slate-500 focus:border-violet-600 focus:outline-2 focus:outline-violet-600/30">
    <button type="submit" class="min-h-11 rounded-lg bg-violet-700 px-4 text-sm font-semibold text-white hover:bg-violet-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-violet-700">Add</button>
  </form>
  <p id="tls-tag-status" class="mt-2 min-h-5 text-xs text-slate-600" aria-live="polite"></p>
</div>

Stacked, grouped and data lists

Lists that carry real data: rows split with divide-y, initials avatars, sticky letter headings, a description list in a grid, a file grid with thumbnails and an activity timeline drawn with before: pseudo-elements.

Stacked list with initials avatars

  • divide-y
  • fill-*
  • truncate
  • min-w-0
  • hidden sm:flex

divide-y divide-slate-100 draws the row lines on the ul, so no li needs a border and the first row stays clean. The avatars are inline SVG with fill-* utilities on the circle and the initials, min-w-0 plus truncate keeps long emails inside the row, and the role and last-seen column only appears from sm: up. A stretched link (after:absolute after:inset-0) makes each row one click target.

View code
<div class="w-full max-w-2xl overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200">
  <div class="flex items-center justify-between gap-3 border-b border-slate-200 px-4 py-3 sm:px-5">
    <h3 class="text-base font-semibold text-slate-900">Team members</h3>
    <a href="#" class="rounded-md px-2 py-1 text-sm font-medium text-indigo-700 hover:bg-indigo-50 focus-visible:outline-2 focus-visible:outline-indigo-600">Invite</a>
  </div>
  <ul role="list" class="divide-y divide-slate-100">
    <li class="relative flex items-center justify-between gap-4 px-4 py-4 hover:bg-slate-50 sm:px-5">
      <div class="flex min-w-0 items-center gap-3">
        <svg viewBox="0 0 40 40" class="size-10 flex-none" aria-hidden="true"><circle cx="20" cy="20" r="20" class="fill-indigo-100"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" class="fill-indigo-800">MA</text></svg>
        <div class="min-w-0">
          <p class="text-sm font-semibold text-slate-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-2 focus-visible:outline-indigo-600">Marta Alvarez</a></p>
          <p class="truncate text-sm text-slate-600">marta.alvarez@example.com</p>
        </div>
      </div>
      <div class="hidden flex-none flex-col items-end sm:flex">
        <p class="text-sm text-slate-900">Design lead</p>
        <p class="mt-1 text-xs text-slate-600">Last seen <time datetime="2026-10-29T09:12">3h ago</time></p>
      </div>
    </li>
    <li class="relative flex items-center justify-between gap-4 px-4 py-4 hover:bg-slate-50 sm:px-5">
      <div class="flex min-w-0 items-center gap-3">
        <svg viewBox="0 0 40 40" class="size-10 flex-none" aria-hidden="true"><circle cx="20" cy="20" r="20" class="fill-amber-100"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" class="fill-amber-900">KO</text></svg>
        <div class="min-w-0">
          <p class="text-sm font-semibold text-slate-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-2 focus-visible:outline-indigo-600">Kwame Osei</a></p>
          <p class="truncate text-sm text-slate-600">kwame.osei@example.com</p>
        </div>
      </div>
      <div class="hidden flex-none flex-col items-end sm:flex">
        <p class="text-sm text-slate-900">Front-end developer</p>
        <p class="mt-1 flex items-center gap-1.5 text-xs text-slate-600"><span class="size-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>Online</p>
      </div>
    </li>
    <li class="relative flex items-center justify-between gap-4 px-4 py-4 hover:bg-slate-50 sm:px-5">
      <div class="flex min-w-0 items-center gap-3">
        <svg viewBox="0 0 40 40" class="size-10 flex-none" aria-hidden="true"><circle cx="20" cy="20" r="20" class="fill-teal-100"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" class="fill-teal-900">LB</text></svg>
        <div class="min-w-0">
          <p class="text-sm font-semibold text-slate-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-2 focus-visible:outline-indigo-600">Linnea Berg</a></p>
          <p class="truncate text-sm text-slate-600">linnea.berg.contractor@example.com</p>
        </div>
      </div>
      <div class="hidden flex-none flex-col items-end sm:flex">
        <p class="text-sm text-slate-900">Content strategist</p>
        <p class="mt-1 text-xs text-slate-600">Last seen <time datetime="2026-10-27T16:40">2 days ago</time></p>
      </div>
    </li>
    <li class="relative flex items-center justify-between gap-4 px-4 py-4 hover:bg-slate-50 sm:px-5">
      <div class="flex min-w-0 items-center gap-3">
        <svg viewBox="0 0 40 40" class="size-10 flex-none" aria-hidden="true"><circle cx="20" cy="20" r="20" class="fill-rose-100"/><text x="20" y="25" text-anchor="middle" font-size="14" font-weight="600" class="fill-rose-900">DN</text></svg>
        <div class="min-w-0">
          <p class="text-sm font-semibold text-slate-900"><a href="#" class="after:absolute after:inset-0 focus-visible:outline-2 focus-visible:outline-indigo-600">Duc Nguyen</a></p>
          <p class="truncate text-sm text-slate-600">duc.nguyen@example.com</p>
        </div>
      </div>
      <div class="hidden flex-none flex-col items-end sm:flex">
        <p class="text-sm text-slate-900">Project manager</p>
        <p class="mt-1 flex items-center gap-1.5 text-xs text-slate-600"><span class="size-1.5 rounded-full bg-emerald-500" aria-hidden="true"></span>Online</p>
      </div>
    </li>
  </ul>
</div>

Contact list with sticky letter headings

  • sticky top-0
  • max-h-80
  • overflow-y-auto
  • backdrop-blur-sm
  • tabindex="0"

Scroll the list.

Each letter is an li that holds a sticky top-0 heading and its own ul, so a heading sticks only while its group is in view and the next letter pushes it out. The scroll box is max-h-80 overflow-y-auto with tabindex="0", role="region" and a label, so keyboard users can scroll it too. bg-white/90 with backdrop-blur-sm keeps the heading readable over rows passing beneath.

View code
<div class="w-full max-w-sm overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200">
  <div class="flex items-center justify-between border-b border-slate-200 px-4 py-3">
    <h3 class="text-base font-semibold text-slate-900">Contacts</h3>
    <span class="text-sm text-slate-600">14 people</span>
  </div>
  <div id="tls-ct-scroll" class="max-h-80 overflow-y-auto focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-teal-600" tabindex="0" role="region" aria-label="Contacts, alphabetical">
    <ul role="list">
      <li>
        <h4 class="sticky top-0 z-10 border-y border-slate-200 bg-white/90 px-4 py-1.5 text-xs font-semibold text-teal-800 backdrop-blur-sm">A</h4>
        <ul role="list" class="divide-y divide-slate-100">
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-teal-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-teal-900">AB</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Aiko Brandt</p><p class="text-xs text-slate-600">(555) 014-2280</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-sky-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-sky-900">AF</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Amir Farahani</p><p class="text-xs text-slate-600">(555) 014-9013</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-amber-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-amber-900">AR</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Ana Rocha</p><p class="text-xs text-slate-600">(555) 014-3377</p></div></li>
        </ul>
      </li>
      <li>
        <h4 class="sticky top-0 z-10 border-y border-slate-200 bg-white/90 px-4 py-1.5 text-xs font-semibold text-teal-800 backdrop-blur-sm">B</h4>
        <ul role="list" class="divide-y divide-slate-100">
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-rose-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-rose-900">BH</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Bea Halvorsen</p><p class="text-xs text-slate-600">(555) 014-6612</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-violet-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-violet-900">BO</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Ben Okafor</p><p class="text-xs text-slate-600">(555) 014-0458</p></div></li>
        </ul>
      </li>
      <li>
        <h4 class="sticky top-0 z-10 border-y border-slate-200 bg-white/90 px-4 py-1.5 text-xs font-semibold text-teal-800 backdrop-blur-sm">C</h4>
        <ul role="list" class="divide-y divide-slate-100">
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-lime-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-lime-900">CD</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Camille Dubois</p><p class="text-xs text-slate-600">(555) 014-7720</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-orange-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-orange-900">CM</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Chidi Mensah</p><p class="text-xs text-slate-600">(555) 014-1196</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-teal-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-teal-900">CW</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Clara Wรณjcik</p><p class="text-xs text-slate-600">(555) 014-5834</p></div></li>
        </ul>
      </li>
      <li>
        <h4 class="sticky top-0 z-10 border-y border-slate-200 bg-white/90 px-4 py-1.5 text-xs font-semibold text-teal-800 backdrop-blur-sm">D</h4>
        <ul role="list" class="divide-y divide-slate-100">
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-sky-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-sky-900">DK</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Dana Kowalczyk</p><p class="text-xs text-slate-600">(555) 014-2049</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-rose-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-rose-900">DS</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Diego Salas</p><p class="text-xs text-slate-600">(555) 014-8861</p></div></li>
        </ul>
      </li>
      <li>
        <h4 class="sticky top-0 z-10 border-y border-slate-200 bg-white/90 px-4 py-1.5 text-xs font-semibold text-teal-800 backdrop-blur-sm">E</h4>
        <ul role="list" class="divide-y divide-slate-100">
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-violet-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-violet-900">EL</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Elif Arslan Lee</p><p class="text-xs text-slate-600">(555) 014-3102</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-amber-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-amber-900">EM</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Emeka Mbah</p><p class="text-xs text-slate-600">(555) 014-6590</p></div></li>
          <li class="flex items-center gap-3 px-4 py-3"><svg viewBox="0 0 32 32" class="size-8 flex-none" aria-hidden="true"><circle cx="16" cy="16" r="16" class="fill-lime-100"/><text x="16" y="20.5" text-anchor="middle" font-size="12" font-weight="600" class="fill-lime-900">ES</text></svg><div class="min-w-0"><p class="truncate text-sm font-medium text-slate-900">Eva Svensson</p><p class="text-xs text-slate-600">(555) 014-4415</p></div></li>
        </ul>
      </li>
    </ul>
  </div>
</div>

Description list in a grid

  • <dl>
  • sm:grid-cols-[9rem_1fr]
  • divide-y
  • list-disc
  • tabular-nums

A dl with div wrappers around each dt and dd pair, which HTML allows, so each pair can become a grid row: one column on phones and sm:grid-cols-[9rem_1fr] from sm: up. divide-y on the dl draws the separators between pairs. The Items value holds a nested ul that brings its bullets back with list-disc and marker:text-orange-500.

View code
<div class="w-full max-w-2xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-stone-200 sm:p-7">
  <div class="flex flex-wrap items-start justify-between gap-3">
    <div>
      <h3 class="text-base font-semibold text-stone-900">Order RB-10482</h3>
      <p class="mt-1 text-sm text-stone-600">Placed <time datetime="2026-10-26">October 26, 2026</time></p>
    </div>
    <span class="inline-flex items-center rounded-full bg-orange-50 px-2.5 py-1 text-xs font-medium text-orange-800 ring-1 ring-orange-600/20 ring-inset">Packing</span>
  </div>
  <dl class="mt-5 divide-y divide-stone-200 border-t border-stone-200 text-sm">
    <div class="grid gap-1 py-3 sm:grid-cols-[9rem_1fr] sm:gap-4">
      <dt class="font-medium text-stone-600">Customer</dt>
      <dd class="text-stone-900">Priya Raman, priya.raman@example.com</dd>
    </div>
    <div class="grid gap-1 py-3 sm:grid-cols-[9rem_1fr] sm:gap-4">
      <dt class="font-medium text-stone-600">Ship to</dt>
      <dd class="text-stone-900">48 Juniper Row, Apt 3<br>Portland, OR 97205</dd>
    </div>
    <div class="grid gap-1 py-3 sm:grid-cols-[9rem_1fr] sm:gap-4">
      <dt class="font-medium text-stone-600">Delivery</dt>
      <dd class="text-stone-900">Ground, estimated <time datetime="2026-10-31">Oct 31</time></dd>
    </div>
    <div class="grid gap-1 py-3 sm:grid-cols-[9rem_1fr] sm:gap-4">
      <dt class="font-medium text-stone-600">Items</dt>
      <dd class="text-stone-900">
        <ul class="list-disc space-y-1 pl-5 marker:text-orange-500">
          <li>Stoneware pour-over set, sand glaze</li>
          <li>Linen tea towels, set of 3</li>
          <li>Walnut serving board, medium</li>
        </ul>
      </dd>
    </div>
    <div class="grid gap-1 py-3 sm:grid-cols-[9rem_1fr] sm:gap-4">
      <dt class="font-medium text-stone-600">Total</dt>
      <dd class="font-semibold text-stone-900 tabular-nums">$186.40 <span class="font-normal text-stone-600">paid by card ending 4242</span></dd>
    </div>
  </dl>
</div>

Grid list of files with thumbnails

  • grid-cols-2
  • md:grid-cols-4
  • aspect-4/3
  • object-cover
  • group-hover:

The ul itself is the grid: grid-cols-2 on phones and md:grid-cols-4 on wider frames, with role="list" because the markers are gone. Photos fill an aspect-4/3 box with object-cover, documents get an SVG file icon on a tinted tile of the same ratio, and truncate keeps long file names on one line. Each card is a link with a visible focus ring, and group-hover: darkens the thumbnail.

View code
<div class="w-full max-w-3xl rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <div class="flex items-center justify-between gap-3">
    <h3 class="text-base font-semibold text-slate-900">Trip journal assets</h3>
    <p class="text-sm text-slate-600">8 files, 31.6 MB</p>
  </div>
  <ul role="list" class="mt-5 grid grid-cols-2 gap-x-3 gap-y-5 md:grid-cols-4 md:gap-x-4">
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><img src="fjord.webp" width="800" height="600" alt="Deep blue fjord between grey cliffs, seen from a crowded rock ledge" class="aspect-4/3 w-full rounded-xl object-cover group-hover:brightness-90" loading="lazy"><p class="mt-2 truncate text-sm font-medium text-slate-900">fjord-overlook.webp</p><p class="text-xs text-slate-600">4.8 MB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><img src="canal-houses.webp" width="800" height="600" alt="Old brick houses along a calm canal with two small boats moored" class="aspect-4/3 w-full rounded-xl object-cover group-hover:brightness-90" loading="lazy"><p class="mt-2 truncate text-sm font-medium text-slate-900">canal-morning.webp</p><p class="text-xs text-slate-600">5.1 MB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><img src="snow-camp.webp" width="800" height="600" alt="Yellow tent half buried in snow below a sharp Himalayan peak" class="aspect-4/3 w-full rounded-xl object-cover group-hover:brightness-90" loading="lazy"><p class="mt-2 truncate text-sm font-medium text-slate-900">base-camp-day-four.webp</p><p class="text-xs text-slate-600">6.3 MB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><span class="flex aspect-4/3 w-full items-center justify-center rounded-xl bg-rose-50 text-rose-700 group-hover:bg-rose-100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" class="size-10" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z"/><path d="M14 3v5h5"/></svg></span><p class="mt-2 truncate text-sm font-medium text-slate-900">itinerary-final.pdf</p><p class="text-xs text-slate-600">PDF, 820 KB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><span class="flex aspect-4/3 w-full items-center justify-center rounded-xl bg-emerald-50 text-emerald-700 group-hover:bg-emerald-100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" class="size-10" aria-hidden="true"><rect x="4" y="4" width="16" height="16" rx="2"/><path d="M4 9.5h16M4 15h16M10 4v16"/></svg></span><p class="mt-2 truncate text-sm font-medium text-slate-900">budget-by-day.csv</p><p class="text-xs text-slate-600">CSV, 14 KB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><span class="flex aspect-4/3 w-full items-center justify-center rounded-xl bg-sky-50 text-sky-700 group-hover:bg-sky-100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" class="size-10" aria-hidden="true"><path d="M14 3H7a2 2 0 0 0-2 2v14a2 2 0 0 0 2 2h10a2 2 0 0 0 2-2V8l-5-5z"/><path d="M14 3v5h5M8.5 13h7M8.5 16.5h5"/></svg></span><p class="mt-2 truncate text-sm font-medium text-slate-900">packing-notes.txt</p><p class="text-xs text-slate-600">Text, 3 KB</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><span class="flex aspect-4/3 w-full items-center justify-center rounded-xl bg-amber-50 text-amber-700 group-hover:bg-amber-100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" class="size-10" aria-hidden="true"><path d="M3 7a2 2 0 0 1 2-2h4l2 2h8a2 2 0 0 1 2 2v8a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2V7z"/></svg></span><p class="mt-2 truncate text-sm font-medium text-slate-900">raw-photos</p><p class="text-xs text-slate-600">Folder, 212 items</p></a></li>
    <li><a href="#" class="group block rounded-xl focus-visible:outline-2 focus-visible:outline-offset-4 focus-visible:outline-sky-600"><span class="flex aspect-4/3 w-full items-center justify-center rounded-xl bg-violet-50 text-violet-700 group-hover:bg-violet-100"><svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" class="size-10" aria-hidden="true"><rect x="3" y="5" width="18" height="14" rx="2"/><path d="M10 9.5v5l4.5-2.5L10 9.5z"/></svg></span><p class="mt-2 truncate text-sm font-medium text-slate-900">summit-timelapse.mp4</p><p class="text-xs text-slate-600">Video, 14.6 MB</p></a></li>
  </ul>
</div>

Activity timeline

  • before:
  • last:before:hidden
  • ring-4 ring-white
  • <time>
  • role="list"

An ol in time order where every li draws its own connector with before:absolute before:left-4 before:w-px, and last:before:hidden stops the line at the final event. The icon circles sit on the line with ring-4 ring-white, which cuts a clean gap around them. Dates use time elements with a datetime attribute, and a quoted comment gets its own bordered block inside the item.

View code
<div class="w-full max-w-lg rounded-2xl bg-white p-5 shadow-sm ring-1 ring-slate-200 sm:p-6">
  <h3 class="text-base font-semibold text-slate-900">Activity on Homepage redesign</h3>
  <ol role="list" class="mt-6">
    <li class="relative flex gap-4 pb-6 before:absolute before:top-9 before:bottom-0 before:left-4 before:w-px before:bg-slate-200 last:pb-0 last:before:hidden">
      <span class="relative flex size-8 flex-none items-center justify-center rounded-full bg-slate-100 text-slate-600 ring-4 ring-white"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M4 10h12M10 4v12"/></svg></span>
      <div class="min-w-0 flex-1 pt-1.5 text-sm">
        <p class="text-slate-700"><span class="font-semibold text-slate-900">Noor Haddad</span> created the project</p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-20T10:02">Oct 20, 10:02</time></p>
      </div>
    </li>
    <li class="relative flex gap-4 pb-6 before:absolute before:top-9 before:bottom-0 before:left-4 before:w-px before:bg-slate-200 last:pb-0 last:before:hidden">
      <span class="relative flex size-8 flex-none items-center justify-center rounded-full bg-sky-100 text-sky-700 ring-4 ring-white"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M7.5 9a2.5 2.5 0 1 0 0-5 2.5 2.5 0 0 0 0 5zM3 16c0-2.5 2-4.5 4.5-4.5S12 13.5 12 16M15 7v5M12.5 9.5h5"/></svg></span>
      <div class="min-w-0 flex-1 pt-1.5 text-sm">
        <p class="text-slate-700"><span class="font-semibold text-slate-900">Noor Haddad</span> added <span class="font-semibold text-slate-900">Jonah Pike</span> as reviewer</p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-21T14:30">Oct 21, 14:30</time></p>
      </div>
    </li>
    <li class="relative flex gap-4 pb-6 before:absolute before:top-9 before:bottom-0 before:left-4 before:w-px before:bg-slate-200 last:pb-0 last:before:hidden">
      <span class="relative flex size-8 flex-none items-center justify-center rounded-full bg-amber-100 text-amber-700 ring-4 ring-white"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M4 5.5A1.5 1.5 0 0 1 5.5 4h9A1.5 1.5 0 0 1 16 5.5v6a1.5 1.5 0 0 1-1.5 1.5H9l-3.5 3v-3A1.5 1.5 0 0 1 4 11.5v-6z"/></svg></span>
      <div class="min-w-0 flex-1 pt-1.5 text-sm">
        <p class="text-slate-700"><span class="font-semibold text-slate-900">Jonah Pike</span> commented</p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-23T09:15">Oct 23, 09:15</time></p>
        <blockquote class="mt-2 rounded-lg border border-slate-200 bg-slate-50 px-3 py-2 text-slate-700">The hero copy reads well. Can we drop the second button on mobile?</blockquote>
      </div>
    </li>
    <li class="relative flex gap-4 pb-6 before:absolute before:top-9 before:bottom-0 before:left-4 before:w-px before:bg-slate-200 last:pb-0 last:before:hidden">
      <span class="relative flex size-8 flex-none items-center justify-center rounded-full bg-violet-100 text-violet-700 ring-4 ring-white"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M3.5 10.5V4.5a1 1 0 0 1 1-1h6l6 6-7 7-6-6z"/><circle cx="7.5" cy="7.5" r="1"/></svg></span>
      <div class="min-w-0 flex-1 pt-1.5 text-sm">
        <p class="text-slate-700"><span class="font-semibold text-slate-900">Noor Haddad</span> added the label <span class="inline-flex rounded-full bg-violet-50 px-2 py-0.5 text-xs font-medium text-violet-800 ring-1 ring-violet-600/20 ring-inset">Ready for QA</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-24T16:48">Oct 24, 16:48</time></p>
      </div>
    </li>
    <li class="relative flex gap-4 pb-6 before:absolute before:top-9 before:bottom-0 before:left-4 before:w-px before:bg-slate-200 last:pb-0 last:before:hidden">
      <span class="relative flex size-8 flex-none items-center justify-center rounded-full bg-emerald-600 text-white ring-4 ring-white"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M5 10.5l3 3 7-7"/></svg></span>
      <div class="min-w-0 flex-1 pt-1.5 text-sm">
        <p class="text-slate-700">Deployed to <span class="font-semibold text-slate-900">production</span> by <span class="font-semibold text-slate-900">Jonah Pike</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-28T11:05">Oct 28, 11:05</time></p>
      </div>
    </li>
  </ol>
</div>

Lists people act on

Tailwind styles the states and a few lines of vanilla JS change them: unread rows with data-* variants, a to-do list that strikes through with has-checked:, keyboard reordering with DOM moves and a folder tree built on native details elements.

Notification list with unread state

  • data-unread:
  • group-data-unread:
  • divide-y
  • disabled:
  • aria-label

Mark a row as read, dismiss one or mark all as read.

Unread rows carry a bare data-unread attribute, and the styling reads it: data-unread:bg-sky-50 tints the row, while group-data-unread: sets the dot, the bold title and which of the two action icons shows. The JS only adds or removes that attribute, rewrites the sr-only Unread prefix and the button labels, and updates the count and the disabled state of Mark all as read. Dismissing a row moves focus to the next one.

View code
<div class="w-full max-w-lg overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-slate-200">
  <div class="flex flex-wrap items-center justify-between gap-3 border-b border-slate-200 px-4 py-3 sm:px-5">
    <h3 id="tls-inbox-title" tabindex="-1" class="flex items-center gap-2 text-base font-semibold text-slate-900 focus:outline-none">Notifications <span class="rounded-full bg-sky-100 px-2 py-0.5 text-xs font-medium text-sky-800"><span id="tls-inbox-count">3</span> unread</span></h3>
    <button type="button" id="tls-inbox-all" class="min-h-9 rounded-md px-2 text-sm font-medium text-sky-700 hover:bg-sky-50 focus-visible:outline-2 focus-visible:outline-sky-600 disabled:text-slate-400 disabled:hover:bg-transparent">Mark all as read</button>
  </div>
  <ul id="tls-inbox-list" role="list" class="divide-y divide-slate-100">
    <li data-unread class="group flex gap-3 px-4 py-4 data-unread:bg-sky-50 sm:px-5">
      <span class="mt-1.5 size-2 flex-none rounded-full bg-transparent group-data-unread:bg-sky-600" aria-hidden="true"></span>
      <div class="min-w-0 flex-1 text-sm">
        <p class="text-slate-800"><span class="tls-inbox-sr sr-only">Unread: </span><span class="tls-inbox-title font-normal group-data-unread:font-semibold">Lena Okafor commented on Homepage v3</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-29T11:42">12 min ago</time></p>
      </div>
      <div class="flex flex-none items-start gap-1">
        <button type="button" aria-label="Mark as read: Lena Okafor commented on Homepage v3" class="tls-inbox-toggle grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hidden size-5 group-data-unread:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" class="block size-5 group-data-unread:hidden" aria-hidden="true"><rect x="3" y="5" width="14" height="10" rx="1.5"/><path d="M3.5 5.5l6.5 5 6.5-5"/></svg></button>
        <button type="button" aria-label="Dismiss: Lena Okafor commented on Homepage v3" class="tls-inbox-dismiss grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-rose-700 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="size-5" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg></button>
      </div>
    </li>
    <li data-unread class="group flex gap-3 px-4 py-4 data-unread:bg-sky-50 sm:px-5">
      <span class="mt-1.5 size-2 flex-none rounded-full bg-transparent group-data-unread:bg-sky-600" aria-hidden="true"></span>
      <div class="min-w-0 flex-1 text-sm">
        <p class="text-slate-800"><span class="tls-inbox-sr sr-only">Unread: </span><span class="tls-inbox-title font-normal group-data-unread:font-semibold">Deploy to staging failed on build 418</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-29T10:58">1 h ago</time></p>
      </div>
      <div class="flex flex-none items-start gap-1">
        <button type="button" aria-label="Mark as read: Deploy to staging failed on build 418" class="tls-inbox-toggle grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hidden size-5 group-data-unread:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" class="block size-5 group-data-unread:hidden" aria-hidden="true"><rect x="3" y="5" width="14" height="10" rx="1.5"/><path d="M3.5 5.5l6.5 5 6.5-5"/></svg></button>
        <button type="button" aria-label="Dismiss: Deploy to staging failed on build 418" class="tls-inbox-dismiss grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-rose-700 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="size-5" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg></button>
      </div>
    </li>
    <li data-unread class="group flex gap-3 px-4 py-4 data-unread:bg-sky-50 sm:px-5">
      <span class="mt-1.5 size-2 flex-none rounded-full bg-transparent group-data-unread:bg-sky-600" aria-hidden="true"></span>
      <div class="min-w-0 flex-1 text-sm">
        <p class="text-slate-800"><span class="tls-inbox-sr sr-only">Unread: </span><span class="tls-inbox-title font-normal group-data-unread:font-semibold">Ravi Mehta invited you to Brand kit 2027</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-29T08:20">3 h ago</time></p>
      </div>
      <div class="flex flex-none items-start gap-1">
        <button type="button" aria-label="Mark as read: Ravi Mehta invited you to Brand kit 2027" class="tls-inbox-toggle grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hidden size-5 group-data-unread:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" class="block size-5 group-data-unread:hidden" aria-hidden="true"><rect x="3" y="5" width="14" height="10" rx="1.5"/><path d="M3.5 5.5l6.5 5 6.5-5"/></svg></button>
        <button type="button" aria-label="Dismiss: Ravi Mehta invited you to Brand kit 2027" class="tls-inbox-dismiss grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-rose-700 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="size-5" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg></button>
      </div>
    </li>
    <li class="group flex gap-3 px-4 py-4 data-unread:bg-sky-50 sm:px-5">
      <span class="mt-1.5 size-2 flex-none rounded-full bg-transparent group-data-unread:bg-sky-600" aria-hidden="true"></span>
      <div class="min-w-0 flex-1 text-sm">
        <p class="text-slate-800"><span class="tls-inbox-sr sr-only"></span><span class="tls-inbox-title font-normal group-data-unread:font-semibold">Invoice INV-2207 was paid</span></p>
        <p class="mt-0.5 text-xs text-slate-600"><time datetime="2026-10-28T17:03">Yesterday</time></p>
      </div>
      <div class="flex flex-none items-start gap-1">
        <button type="button" aria-label="Mark as unread: Invoice INV-2207 was paid" class="tls-inbox-toggle grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-slate-900 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="hidden size-5 group-data-unread:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linejoin="round" class="block size-5 group-data-unread:hidden" aria-hidden="true"><rect x="3" y="5" width="14" height="10" rx="1.5"/><path d="M3.5 5.5l6.5 5 6.5-5"/></svg></button>
        <button type="button" aria-label="Dismiss: Invoice INV-2207 was paid" class="tls-inbox-dismiss grid size-9 place-items-center rounded-lg text-slate-600 hover:bg-white hover:text-rose-700 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" class="size-5" aria-hidden="true"><path d="M6 6l8 8M14 6l-8 8"/></svg></button>
      </div>
    </li>
  </ul>
  <p id="tls-inbox-empty" hidden class="px-5 py-10 text-center text-sm text-slate-600">You are all caught up.</p>
</div>

To-do list with has-checked: strike-through

  • has-checked:line-through
  • peer-checked:
  • appearance-none
  • checked:bg-teal-600
  • <template>

Tick a task, add one or clear completed tasks.

The strike-through needs no JS: each label has has-checked:line-through has-checked:text-slate-500, so ticking the checkbox inside it restyles the whole row. The checkbox is a native input with appearance-none and checked:bg-teal-600, and the check icon stacked on it shows through peer-checked:block. The JS adds tasks from a template, clears finished ones and keeps the remaining count in sync.

View code
<div class="w-full max-w-md rounded-2xl bg-white shadow-sm ring-1 ring-slate-200">
  <div class="px-5 pt-5">
    <h3 class="text-base font-semibold text-slate-900">Friday before launch</h3>
    <form id="tls-todo-form" class="mt-4 flex gap-2">
      <label for="tls-todo-input" class="sr-only">New task</label>
      <input id="tls-todo-input" type="text" maxlength="60" autocomplete="off" placeholder="Add a task" class="min-h-11 min-w-0 flex-1 rounded-lg border border-slate-300 px-3 text-sm text-slate-900 placeholder:text-slate-500 focus:border-teal-600 focus:outline-2 focus:outline-teal-600/30">
      <button type="submit" class="min-h-11 rounded-lg bg-teal-700 px-4 text-sm font-semibold text-white hover:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-700">Add</button>
    </form>
  </div>
  <ul id="tls-todo-list" role="list" class="mt-4 divide-y divide-slate-100 border-t border-slate-100">
    <li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" checked class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span>Freeze the content on staging</span></label></li>
    <li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" checked class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span>Back up the production database</span></label></li>
    <li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span>Test the checkout with a sandbox card</span></label></li>
    <li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span>Send the launch email to the client</span></label></li>
    <li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span>Switch DNS and watch the error logs</span></label></li>
  </ul>
  <template id="tls-todo-tpl"><li><label class="flex min-h-12 cursor-pointer items-center gap-3 px-5 py-2.5 text-sm text-slate-800 hover:bg-slate-50 has-checked:text-slate-500 has-checked:line-through"><span class="grid flex-none place-items-center"><input type="checkbox" class="tls-todo-check peer col-start-1 row-start-1 size-5 appearance-none rounded-md border border-slate-400 bg-white checked:border-teal-600 checked:bg-teal-600 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-teal-600"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round" class="pointer-events-none col-start-1 row-start-1 hidden size-3.5 text-white peer-checked:block" aria-hidden="true"><path d="M4.5 10.5l3.5 3.5 7.5-8"/></svg></span><span class="tls-todo-text"></span></label></li></template>
  <div class="flex items-center justify-between gap-3 border-t border-slate-100 px-5 py-3">
    <p id="tls-todo-left" class="text-sm text-slate-600" aria-live="polite">3 of 5 left</p>
    <button type="button" id="tls-todo-clear" class="min-h-9 rounded-md px-2 text-sm font-medium text-teal-800 hover:bg-teal-50 focus-visible:outline-2 focus-visible:outline-teal-600">Clear completed</button>
  </div>
</div>

Reorderable list with Up and Down buttons

  • cursor-grab
  • disabled:opacity-30
  • tabular-nums
  • aria-live
  • insertBefore

Move a row with the arrow buttons.

The grip icon shows the row can move, but the working controls are the Up and Down buttons, which give keyboard and single-pointer users the same result as dragging (WCAG 2.2 criterion 2.5.7). The JS moves the li itself with insertBefore, renumbers the rows, disables Up on the first row and Down on the last through disabled:opacity-30, keeps focus on the pressed button and announces the new position in a live region.

View code
<div class="w-full max-w-md rounded-2xl bg-stone-50 p-5 ring-1 ring-stone-200 sm:p-6">
  <h3 class="text-base font-semibold text-stone-900">Homepage section order</h3>
  <p class="mt-1 text-sm text-stone-600">Top of the list renders first.</p>
  <ol id="tls-ro-list" role="list" class="mt-4 space-y-2">
    <li class="flex items-center gap-3 rounded-xl bg-white py-2 pr-2 pl-3 shadow-xs ring-1 ring-stone-200">
      <svg viewBox="0 0 20 20" fill="currentColor" class="size-5 flex-none cursor-grab text-stone-400" aria-hidden="true"><circle cx="7" cy="5" r="1.4"/><circle cx="13" cy="5" r="1.4"/><circle cx="7" cy="10" r="1.4"/><circle cx="13" cy="10" r="1.4"/><circle cx="7" cy="15" r="1.4"/><circle cx="13" cy="15" r="1.4"/></svg>
      <span class="tls-ro-pos w-5 flex-none text-sm font-semibold text-orange-700 tabular-nums">1</span>
      <span class="tls-ro-name min-w-0 flex-1 truncate text-sm font-medium text-stone-900">Hero with signup</span>
      <button type="button" aria-label="Move Hero with signup up" class="tls-ro-up grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 12l5-5 5 5"/></svg></button>
      <button type="button" aria-label="Move Hero with signup down" class="tls-ro-down grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 8l5 5 5-5"/></svg></button>
    </li>
    <li class="flex items-center gap-3 rounded-xl bg-white py-2 pr-2 pl-3 shadow-xs ring-1 ring-stone-200">
      <svg viewBox="0 0 20 20" fill="currentColor" class="size-5 flex-none cursor-grab text-stone-400" aria-hidden="true"><circle cx="7" cy="5" r="1.4"/><circle cx="13" cy="5" r="1.4"/><circle cx="7" cy="10" r="1.4"/><circle cx="13" cy="10" r="1.4"/><circle cx="7" cy="15" r="1.4"/><circle cx="13" cy="15" r="1.4"/></svg>
      <span class="tls-ro-pos w-5 flex-none text-sm font-semibold text-orange-700 tabular-nums">2</span>
      <span class="tls-ro-name min-w-0 flex-1 truncate text-sm font-medium text-stone-900">Client logos</span>
      <button type="button" aria-label="Move Client logos up" class="tls-ro-up grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 12l5-5 5 5"/></svg></button>
      <button type="button" aria-label="Move Client logos down" class="tls-ro-down grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 8l5 5 5-5"/></svg></button>
    </li>
    <li class="flex items-center gap-3 rounded-xl bg-white py-2 pr-2 pl-3 shadow-xs ring-1 ring-stone-200">
      <svg viewBox="0 0 20 20" fill="currentColor" class="size-5 flex-none cursor-grab text-stone-400" aria-hidden="true"><circle cx="7" cy="5" r="1.4"/><circle cx="13" cy="5" r="1.4"/><circle cx="7" cy="10" r="1.4"/><circle cx="13" cy="10" r="1.4"/><circle cx="7" cy="15" r="1.4"/><circle cx="13" cy="15" r="1.4"/></svg>
      <span class="tls-ro-pos w-5 flex-none text-sm font-semibold text-orange-700 tabular-nums">3</span>
      <span class="tls-ro-name min-w-0 flex-1 truncate text-sm font-medium text-stone-900">Feature grid</span>
      <button type="button" aria-label="Move Feature grid up" class="tls-ro-up grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 12l5-5 5 5"/></svg></button>
      <button type="button" aria-label="Move Feature grid down" class="tls-ro-down grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 8l5 5 5-5"/></svg></button>
    </li>
    <li class="flex items-center gap-3 rounded-xl bg-white py-2 pr-2 pl-3 shadow-xs ring-1 ring-stone-200">
      <svg viewBox="0 0 20 20" fill="currentColor" class="size-5 flex-none cursor-grab text-stone-400" aria-hidden="true"><circle cx="7" cy="5" r="1.4"/><circle cx="13" cy="5" r="1.4"/><circle cx="7" cy="10" r="1.4"/><circle cx="13" cy="10" r="1.4"/><circle cx="7" cy="15" r="1.4"/><circle cx="13" cy="15" r="1.4"/></svg>
      <span class="tls-ro-pos w-5 flex-none text-sm font-semibold text-orange-700 tabular-nums">4</span>
      <span class="tls-ro-name min-w-0 flex-1 truncate text-sm font-medium text-stone-900">Customer stories</span>
      <button type="button" aria-label="Move Customer stories up" class="tls-ro-up grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 12l5-5 5 5"/></svg></button>
      <button type="button" aria-label="Move Customer stories down" class="tls-ro-down grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 8l5 5 5-5"/></svg></button>
    </li>
    <li class="flex items-center gap-3 rounded-xl bg-white py-2 pr-2 pl-3 shadow-xs ring-1 ring-stone-200">
      <svg viewBox="0 0 20 20" fill="currentColor" class="size-5 flex-none cursor-grab text-stone-400" aria-hidden="true"><circle cx="7" cy="5" r="1.4"/><circle cx="13" cy="5" r="1.4"/><circle cx="7" cy="10" r="1.4"/><circle cx="13" cy="10" r="1.4"/><circle cx="7" cy="15" r="1.4"/><circle cx="13" cy="15" r="1.4"/></svg>
      <span class="tls-ro-pos w-5 flex-none text-sm font-semibold text-orange-700 tabular-nums">5</span>
      <span class="tls-ro-name min-w-0 flex-1 truncate text-sm font-medium text-stone-900">Pricing table</span>
      <button type="button" aria-label="Move Pricing table up" class="tls-ro-up grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 12l5-5 5 5"/></svg></button>
      <button type="button" aria-label="Move Pricing table down" class="tls-ro-down grid size-10 place-items-center rounded-lg text-stone-600 hover:bg-stone-100 hover:text-stone-900 focus-visible:outline-2 focus-visible:outline-orange-600 disabled:opacity-30 disabled:hover:bg-transparent"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" class="size-5" aria-hidden="true"><path d="M5 8l5 5 5-5"/></svg></button>
    </li>
  </ol>
  <p id="tls-ro-status" class="mt-3 min-h-5 text-xs text-stone-600" aria-live="polite"></p>
</div>

Nested folder tree with details

  • <details>
  • [&[open]>summary>svg]:rotate-90
  • list-none
  • [&::-webkit-details-marker]:hidden
  • border-l

Open and close the folders.

Every folder is a native details element, so opening and closing works with the keyboard and without JS. The summary drops its triangle with list-none plus [&::-webkit-details-marker]:hidden for Safari, and each details rotates only its own chevron through the arbitrary variant [&[open]>summary>svg]:rotate-90, which a nested group-open: would not do. Nested ul levels indent with ml-3 border-l pl-2, and the open file is marked aria-current="page".

View code
<nav class="w-full max-w-xs rounded-2xl bg-white p-3 font-mono text-[13px] shadow-sm ring-1 ring-slate-200" aria-label="Project files">
  <p class="px-2 pt-1 pb-2 font-sans text-xs font-semibold text-slate-600">harbor-site</p>
  <ul role="list" class="space-y-0.5">
    <li>
      <details open class="[&[open]>summary>svg]:rotate-90">
        <summary class="flex min-h-8 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-slate-800 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600 [&::-webkit-details-marker]:hidden"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 flex-none text-slate-500 motion-safe:transition-transform" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg><svg viewBox="0 0 16 16" fill="currentColor" class="size-4 flex-none text-sky-500" aria-hidden="true"><path d="M1.5 4A1.5 1.5 0 0 1 3 2.5h3l1.5 1.5H13A1.5 1.5 0 0 1 14.5 5.5v6.5A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4z"/></svg>src</summary>
        <ul role="list" class="mt-0.5 ml-3.5 space-y-0.5 border-l border-slate-200 pl-2">
          <li>
            <details open class="[&[open]>summary>svg]:rotate-90">
              <summary class="flex min-h-8 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-slate-800 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600 [&::-webkit-details-marker]:hidden"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 flex-none text-slate-500 motion-safe:transition-transform" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg><svg viewBox="0 0 16 16" fill="currentColor" class="size-4 flex-none text-sky-500" aria-hidden="true"><path d="M1.5 4A1.5 1.5 0 0 1 3 2.5h3l1.5 1.5H13A1.5 1.5 0 0 1 14.5 5.5v6.5A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4z"/></svg>components</summary>
              <ul role="list" class="mt-0.5 ml-3.5 space-y-0.5 border-l border-slate-200 pl-2">
                <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-amber-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>button.js</a></li>
                <li><a href="#" aria-current="page" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600 aria-[current=page]:bg-sky-50 aria-[current=page]:font-semibold aria-[current=page]:text-sky-800"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-amber-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>card-list.js</a></li>
                <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-amber-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>site-nav.js</a></li>
              </ul>
            </details>
          </li>
          <li>
            <details class="[&[open]>summary>svg]:rotate-90">
              <summary class="flex min-h-8 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-slate-800 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600 [&::-webkit-details-marker]:hidden"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 flex-none text-slate-500 motion-safe:transition-transform" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg><svg viewBox="0 0 16 16" fill="currentColor" class="size-4 flex-none text-sky-500" aria-hidden="true"><path d="M1.5 4A1.5 1.5 0 0 1 3 2.5h3l1.5 1.5H13A1.5 1.5 0 0 1 14.5 5.5v6.5A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4z"/></svg>styles</summary>
              <ul role="list" class="mt-0.5 ml-3.5 space-y-0.5 border-l border-slate-200 pl-2">
                <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-violet-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>app.css</a></li>
                <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-violet-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>theme.css</a></li>
              </ul>
            </details>
          </li>
          <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 pl-7 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-rose-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>index.html</a></li>
        </ul>
      </details>
    </li>
    <li>
      <details class="[&[open]>summary>svg]:rotate-90">
        <summary class="flex min-h-8 cursor-pointer list-none items-center gap-1.5 rounded-md px-2 text-slate-800 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600 [&::-webkit-details-marker]:hidden"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-3.5 flex-none text-slate-500 motion-safe:transition-transform" aria-hidden="true"><path d="M6 4l4 4-4 4"/></svg><svg viewBox="0 0 16 16" fill="currentColor" class="size-4 flex-none text-sky-500" aria-hidden="true"><path d="M1.5 4A1.5 1.5 0 0 1 3 2.5h3l1.5 1.5H13A1.5 1.5 0 0 1 14.5 5.5v6.5A1.5 1.5 0 0 1 13 13.5H3A1.5 1.5 0 0 1 1.5 12V4z"/></svg>public</summary>
        <ul role="list" class="mt-0.5 ml-3.5 space-y-0.5 border-l border-slate-200 pl-2">
          <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-emerald-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>favicon.svg</a></li>
          <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-emerald-600" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>robots.txt</a></li>
        </ul>
      </details>
    </li>
    <li><a href="#" class="flex min-h-8 items-center gap-1.5 rounded-md px-2 pl-7 text-slate-700 hover:bg-slate-100 focus-visible:outline-2 focus-visible:outline-sky-600"><svg viewBox="0 0 16 16" fill="none" stroke="currentColor" stroke-width="1.4" stroke-linejoin="round" class="size-4 flex-none text-slate-500" aria-hidden="true"><path d="M9.5 1.5H4A1.5 1.5 0 0 0 2.5 3v10A1.5 1.5 0 0 0 4 14.5h8a1.5 1.5 0 0 0 1.5-1.5V5.5l-4-4z"/></svg>package.json</a></li>
  </ul>
</nav>

Dark mode and theme tokens

One list that switches with the class-based dark: variant and one changelog colored entirely from @theme tokens, markers included.

Playlist queue with a dark mode switch

  • dark:
  • aria-[current=true]:
  • group-aria-[current=true]:
  • aria-pressed
  • divide-y

Switch the theme or pick another track.

Every color has a dark: partner, and the switch adds or removes the dark class on the card wrapper, which the class-based @custom-variant dark matches for all descendants. The playing track carries aria-current="true": aria-[current=true]:bg-rose-50 tints its row and group-aria-[current=true]: swaps the track number for an equalizer icon. The JS only moves aria-current and toggles the class plus aria-pressed.

View code
<div id="tls-dk" class="w-full max-w-md">
  <div class="overflow-hidden rounded-2xl bg-white shadow-sm ring-1 ring-zinc-200 dark:bg-zinc-900 dark:ring-zinc-800">
    <div class="flex items-center justify-between gap-3 px-4 py-3 sm:px-5">
      <div>
        <h3 class="text-base font-semibold text-zinc-900 dark:text-white">Up next</h3>
        <p class="text-xs text-zinc-600 dark:text-zinc-400">Late shift, 5 tracks</p>
      </div>
      <button type="button" id="tls-dk-toggle" aria-pressed="false" class="flex min-h-10 items-center gap-2 rounded-full px-3 text-sm font-medium text-zinc-700 ring-1 ring-zinc-300 hover:bg-zinc-100 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-rose-600 dark:text-zinc-200 dark:ring-zinc-700 dark:hover:bg-zinc-800"><svg viewBox="0 0 20 20" fill="none" stroke="currentColor" stroke-width="1.6" stroke-linecap="round" stroke-linejoin="round" class="size-4" aria-hidden="true"><path d="M16 12.5A6.5 6.5 0 0 1 7.5 4a6.5 6.5 0 1 0 8.5 8.5z"/></svg>Dark mode</button>
    </div>
    <ol role="list" class="divide-y divide-zinc-100 border-t border-zinc-100 dark:divide-zinc-800 dark:border-zinc-800">
      <li><button type="button" class="tls-dk-track group grid w-full grid-cols-[1.5rem_1fr_auto] items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600 aria-[current=true]:bg-rose-50 sm:px-5 dark:hover:bg-zinc-800/60 dark:aria-[current=true]:bg-rose-500/10"><span class="text-sm text-zinc-500 tabular-nums group-aria-[current=true]:hidden dark:text-zinc-400">1</span><svg viewBox="0 0 16 16" fill="currentColor" class="hidden size-4 text-rose-600 group-aria-[current=true]:block dark:text-rose-400" aria-hidden="true"><rect x="2" y="6" width="2.5" height="8" rx="1"/><rect x="6.75" y="2" width="2.5" height="12" rx="1"/><rect x="11.5" y="8" width="2.5" height="6" rx="1"/></svg><span class="min-w-0"><span class="block truncate text-sm font-medium text-zinc-900 group-aria-[current=true]:text-rose-700 dark:text-zinc-100 dark:group-aria-[current=true]:text-rose-300">Harbor Lights</span><span class="block truncate text-xs text-zinc-600 dark:text-zinc-400">The Low Tides</span></span><span class="text-xs text-zinc-600 tabular-nums dark:text-zinc-400">3:42</span></button></li>
      <li><button type="button" aria-current="true" class="tls-dk-track group grid w-full grid-cols-[1.5rem_1fr_auto] items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600 aria-[current=true]:bg-rose-50 sm:px-5 dark:hover:bg-zinc-800/60 dark:aria-[current=true]:bg-rose-500/10"><span class="text-sm text-zinc-500 tabular-nums group-aria-[current=true]:hidden dark:text-zinc-400">2</span><svg viewBox="0 0 16 16" fill="currentColor" class="hidden size-4 text-rose-600 group-aria-[current=true]:block dark:text-rose-400" aria-hidden="true"><rect x="2" y="6" width="2.5" height="8" rx="1"/><rect x="6.75" y="2" width="2.5" height="12" rx="1"/><rect x="11.5" y="8" width="2.5" height="6" rx="1"/></svg><span class="min-w-0"><span class="block truncate text-sm font-medium text-zinc-900 group-aria-[current=true]:text-rose-700 dark:text-zinc-100 dark:group-aria-[current=true]:text-rose-300">Neon Rain on Fifth Street</span><span class="block truncate text-xs text-zinc-600 dark:text-zinc-400">Mira Vance</span></span><span class="text-xs text-zinc-600 tabular-nums dark:text-zinc-400">4:15</span></button></li>
      <li><button type="button" class="tls-dk-track group grid w-full grid-cols-[1.5rem_1fr_auto] items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600 aria-[current=true]:bg-rose-50 sm:px-5 dark:hover:bg-zinc-800/60 dark:aria-[current=true]:bg-rose-500/10"><span class="text-sm text-zinc-500 tabular-nums group-aria-[current=true]:hidden dark:text-zinc-400">3</span><svg viewBox="0 0 16 16" fill="currentColor" class="hidden size-4 text-rose-600 group-aria-[current=true]:block dark:text-rose-400" aria-hidden="true"><rect x="2" y="6" width="2.5" height="8" rx="1"/><rect x="6.75" y="2" width="2.5" height="12" rx="1"/><rect x="11.5" y="8" width="2.5" height="6" rx="1"/></svg><span class="min-w-0"><span class="block truncate text-sm font-medium text-zinc-900 group-aria-[current=true]:text-rose-700 dark:text-zinc-100 dark:group-aria-[current=true]:text-rose-300">Paper Satellites</span><span class="block truncate text-xs text-zinc-600 dark:text-zinc-400">Quiet Orbit</span></span><span class="text-xs text-zinc-600 tabular-nums dark:text-zinc-400">3:08</span></button></li>
      <li><button type="button" class="tls-dk-track group grid w-full grid-cols-[1.5rem_1fr_auto] items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600 aria-[current=true]:bg-rose-50 sm:px-5 dark:hover:bg-zinc-800/60 dark:aria-[current=true]:bg-rose-500/10"><span class="text-sm text-zinc-500 tabular-nums group-aria-[current=true]:hidden dark:text-zinc-400">4</span><svg viewBox="0 0 16 16" fill="currentColor" class="hidden size-4 text-rose-600 group-aria-[current=true]:block dark:text-rose-400" aria-hidden="true"><rect x="2" y="6" width="2.5" height="8" rx="1"/><rect x="6.75" y="2" width="2.5" height="12" rx="1"/><rect x="11.5" y="8" width="2.5" height="6" rx="1"/></svg><span class="min-w-0"><span class="block truncate text-sm font-medium text-zinc-900 group-aria-[current=true]:text-rose-700 dark:text-zinc-100 dark:group-aria-[current=true]:text-rose-300">Slow Train North</span><span class="block truncate text-xs text-zinc-600 dark:text-zinc-400">Ellis and the Pines</span></span><span class="text-xs text-zinc-600 tabular-nums dark:text-zinc-400">5:01</span></button></li>
      <li><button type="button" class="tls-dk-track group grid w-full grid-cols-[1.5rem_1fr_auto] items-center gap-3 px-4 py-3 text-left hover:bg-zinc-50 focus-visible:outline-2 focus-visible:-outline-offset-2 focus-visible:outline-rose-600 aria-[current=true]:bg-rose-50 sm:px-5 dark:hover:bg-zinc-800/60 dark:aria-[current=true]:bg-rose-500/10"><span class="text-sm text-zinc-500 tabular-nums group-aria-[current=true]:hidden dark:text-zinc-400">5</span><svg viewBox="0 0 16 16" fill="currentColor" class="hidden size-4 text-rose-600 group-aria-[current=true]:block dark:text-rose-400" aria-hidden="true"><rect x="2" y="6" width="2.5" height="8" rx="1"/><rect x="6.75" y="2" width="2.5" height="12" rx="1"/><rect x="11.5" y="8" width="2.5" height="6" rx="1"/></svg><span class="min-w-0"><span class="block truncate text-sm font-medium text-zinc-900 group-aria-[current=true]:text-rose-700 dark:text-zinc-100 dark:group-aria-[current=true]:text-rose-300">Last Ferry Home</span><span class="block truncate text-xs text-zinc-600 dark:text-zinc-400">The Low Tides</span></span><span class="text-xs text-zinc-600 tabular-nums dark:text-zinc-400">3:56</span></button></li>
    </ol>
  </div>
</div>

Changelog with @theme marker colors

  • @theme
  • marker:text-tls-mint
  • list-[square]
  • dark:
  • border-l

An @theme block registers five prefixed colors, and Tailwind turns each one into utilities, so marker:text-tls-mint, marker:text-tls-coral and dark:bg-tls-panel work like built-in classes. The lists use list-[square] for a sharper bullet than disc, colored per change type through the marker: variant. The card has light classes plus dark: partners, and the dark stage puts the dark class on the html element.

View code
<article class="w-full max-w-lg rounded-2xl bg-white p-6 text-slate-700 ring-1 ring-slate-200 dark:bg-tls-panel dark:text-slate-300 dark:ring-tls-line">
  <h3 class="text-lg font-semibold text-slate-900 dark:text-white">Release notes</h3>
  <section class="mt-5 border-l-2 border-emerald-500 pl-4 dark:border-tls-mint">
    <h4 class="flex flex-wrap items-baseline gap-x-3 text-sm font-semibold text-slate-900 dark:text-white">Version 2.8.0 <time datetime="2026-10-27" class="text-xs font-normal text-slate-600 dark:text-slate-400">Oct 27, 2026</time></h4>
    <p class="mt-3 text-xs font-semibold text-emerald-700 dark:text-tls-mint">Added</p>
    <ul class="mt-1.5 list-[square] space-y-1.5 pl-5 text-sm marker:text-emerald-600 dark:marker:text-tls-mint">
      <li>Saved filters on the projects page</li>
      <li>Keyboard shortcut G then P jumps to projects</li>
      <li>CSV export for time reports</li>
    </ul>
    <p class="mt-4 text-xs font-semibold text-rose-700 dark:text-tls-coral">Fixed</p>
    <ul class="mt-1.5 list-[square] space-y-1.5 pl-5 text-sm marker:text-rose-600 dark:marker:text-tls-coral">
      <li>Date picker opened behind the sidebar on tablets</li>
      <li>Avatars flickered when the list re-sorted</li>
    </ul>
  </section>
  <section class="mt-6 border-l-2 border-slate-300 pl-4 dark:border-tls-line">
    <h4 class="flex flex-wrap items-baseline gap-x-3 text-sm font-semibold text-slate-900 dark:text-white">Version 2.7.2 <time datetime="2026-10-14" class="text-xs font-normal text-slate-600 dark:text-slate-400">Oct 14, 2026</time></h4>
    <p class="mt-3 text-xs font-semibold text-amber-700 dark:text-tls-sun">Changed</p>
    <ul class="mt-1.5 list-[square] space-y-1.5 pl-5 text-sm marker:text-amber-600 dark:marker:text-tls-sun">
      <li>Sessions now last 30 days instead of 14</li>
      <li>Comment threads collapse after five replies</li>
    </ul>
  </section>
</article>

What are Tailwind CSS list utilities?

Each list class in Tailwind maps to a single declaration from the plain CSS list-style rules, so the class name tells you the output.

Marker type comes from list-disc, list-decimal, list-none and arbitrary values. Position is list-inside or list-outside. Images use list-image-[url(…)] or list-image-none, which is the one people tend to forget exists.

The utility-first Tailwind framework wraps those properties as classes, so a list gets styled in the markup instead of in a separate stylesheet. Anything the CSS properties can do, a class or an arbitrary value can do too.

There’s a limit, though. These classes style the list as a whole. Bullet color and size need the marker variant, and spacing between items comes from separate utilities such as space-y.

Why does Preflight remove bullets and numbers from lists?

Because Preflight sets list-style to none on ol, ul and menu elements, a list stays markerless until a utility brings the markers back.

Tailwind injects Preflight into the base layer when you import tailwindcss, according to the Tailwind CSS v4 documentation. It builds on modern-normalize, a stylesheet that smooths out cross-browser differences.

The documentation wording differs by version. The v3 page describes lists as having no bullets, numbers, margin or padding, while the v4 page shows the list-style rule alone.

In practice, pair list-disc with left padding such as pl-5. Outside markers need room to render.

How to set bullet and number styles with list-style-type

list-disc on a ul gives you bullets, and list-decimal on an ol gives you numbers. list-none removes the marker wherever you put it.

The class decides the marker, not the element. A list-disc on an ol prints bullets, and list-decimal on a ul prints numbers.

list-disc outputs list-style-type: disc, list-decimal outputs list-style-type: decimal, and list-none outputs list-style-type: none. That last one works best as the base of a responsive pair such as list-none md:list-disc, a pattern the Tailwind docs use.

Arbitrary and custom values

Square brackets handle one-off markers. A list-[upper-roman] class on an ol gives Roman numerals (I, II, III), and list-[square] gives square bullets. In v4, list-(–my-marker) reads the value from a CSS variable.

Tailwind CSS v3 projects register named values instead, such as square and roman, under theme.listStyleType in tailwind.config.js (Tailwind CSS v3 documentation).

A typo in the value produces a declaration the browser drops. The list then shows no marker at all, because the Preflight reset stays in effect.

list-inside or list-outside: which marker position fits?

Items that wrap across several lines want list-outside. Short items or narrow boxes are fine with list-inside.

MDN Web Docs lists outside as the initial value of list-style-position, so list-outside matches the browser default. In Tailwind, list-inside outputs list-style-position: inside and list-outside outputs list-style-position: outside.

With list-outside, wrapped lines line up with the first line of text (a hanging indent) and the markers stay clear of the text block. The cost is left padding, which you need once Preflight is active.

list-inside needs no extra padding, and the marker stays inside the list’s own box. Against that, wrapped lines run under the marker, you lose the hanging indent, and rendering gets inconsistent when the li starts with a block element.

That last case is documented. MDN says a block element placed first inside an li with list-style-position: inside goes on the line after the marker, while Safari keeps the marker on the same line as the block.

If your items start with a p or div, test in Safari before choosing list-inside. Plain text items avoid the problem.

How to change marker color and size with the marker variant

Add marker:text-sky-500 to the ul or ol, and every bullet or number takes that color while the item text keeps its own.

One placement is enough. Tailwind’s variant definition in the tailwindlabs/tailwindcss repository matches both the element’s own ::marker and the markers of its descendants, so the class works on the parent instead of every li.

The ::marker pseudo-element accepts color, all font properties, content, the counter properties (counter-increment, counter-reset, counter-set), quotes, white-space, direction, unicode-bidi, text-combine-upright, and animation and transition properties (MDN Web Docs). Background-color and margin are not on that list, so filled or padded bullets need a different method.

Safari narrows the list further. Compatibility data from Can I Use and MDN limits Safari’s ::marker support to color and font-size.

Stick to color and font-size if your audience includes Safari.

Markers are rendered text, so they need enough color contrast against the background. WCAG 2.2 success criterion 1.4.3 sets 4.5:1 for normal text and 3:1 for large text (W3C).

How to use custom bullets and icons in a Tailwind list

list-image handles a static icon, while inline SVG is the pick for anything you need to size or recolor. A ::before glyph works for plain text symbols.

MethodSize controlRecolorExtra markup
list-image-[url(…)]None, image keeps its own sizeEdit the image fileNone
before:content-[‘+’] on liFont size utilitiesText color classesNone, needs padding
Inline SVG inside lisize-* classestext-* classesOne svg per item

The Tailwind docs demo list-image with list-image-[url(/img/checkmark.png)] on the ul. In v4, list-image-(–my-list-image) works as shorthand for list-image-[var(–my-list-image)].

The catch is sizing. list-style-image uses the picture as the marker itself, so CSS cannot resize or filter it directly.

When the icon has to match your palette, inline SVG markup from Heroicons is the cleaner route. The set ships as 24 pixel outline and solid icons, plus 20 and 16 pixel solid icons, and the icons take their color from the CSS color property (Tailwind Labs, Heroicons 2.2.0).

Put list-none on the ul and flex items-start gap-2 on each li. The flex class replaces the li’s list-item display, so the item shows no marker of its own.

A before:content-[‘+’] class on each li draws a text symbol instead. Tailwind’s content utilities are built for the before and after variants, and MDN discourages meaningful content inside pseudo-elements because screen readers do not reliably announce it.

My rule of thumb is simple. If the bullet changes color or size anywhere, inline SVG wins. If it never changes, list-image costs less markup.

How to space, indent and nest list items

Padding on the list handles indentation, and sibling margins handle the space between items. Nested levels each need their own marker type again.

Indentation

Left padding such as pl-5 or pl-6 on the ul or ol gives list-outside markers room to render. Left margin (ml-*) shifts the list box itself, so a background or border on the ul moves with it. list-inside removes the need for either, at the price of the hanging indent.

Item spacing

Space on the ul, not on every li.

The space-y-* classes add vertical margin between sibling items and leave the markers untouched.

space-y-2 on the ul is a single class and works with list-disc and list-decimal. Putting mb-2 on each li gives the same visual result, but it’s repeated on every item and the last one gets a bottom margin too. For lists without markers, such as icon lists and menus, gap-* on a flex or grid ul is the better fit.

Nested lists

Place the inner ul inside its parent li, not beside it. Each level then needs its own marker class and its own indent.

  • Level one: list-disc pl-5
  • Level two: list-[circle] pl-5
  • Level three: list-[square] pl-5

Past three levels a list stops reading as a list.

Horizontal lists with Flexbox

Put flex, flex-wrap and gap-4 on the ul, and each li becomes an item in a row.

Skip list-none unless another class or a wrapper brings markers back. The base styles already strip them.

How do responsive, state and dark mode variants work on lists?

Prefix any list utility with a variant, and the class applies only under that condition, whether that’s a breakpoint, a state such as hover, or dark mode.

md:list-image-[url(/img/checkmark.png)] shows the image marker from medium screens up (Tailwind CSS documentation). hover:list-image-[url(checkmark.png)] swaps the marker on hover, and dark:marker:text-sky-400 recolors the markers in dark mode.

Breakpoint prefixes follow a mobile-first design logic. The unprefixed class sets the small-screen style, and the prefixed class takes over from that breakpoint up.

By default the dark variant follows the operating system through the prefers-color-scheme media query, according to the Tailwind CSS v4 documentation. For a manual toggle, override it in your stylesheet with @custom-variant dark (&:where(.dark, .dark *)).

Tailwind CSS v3 handles the same switch through darkMode: ‘selector’ in tailwind.config.js. The selector strategy replaced the older class strategy in v3.4.1.

Image markers do not recolor with the theme. Give each mode its own file instead, for example list-image-[url(/img/checkmark.png)] dark:list-image-[url(/img/checkmark-light.png)].

What changed for lists between Tailwind CSS v3 and v4?

The list class names are the same in both versions. What changed is import syntax, custom value handling, the spacing selector and the browser floor.

AreaTailwind CSS v3Tailwind CSS v4
Import@tailwind base, components, utilities@import “tailwindcss”
Custom marker imagestheme.listStyleImage in tailwind.config.jslist-image-(–my-list-image) shorthand
space-y selector> :not([hidden]) ~ :not([hidden]), margin-top> :not(:last-child), margin-bottom
Browser floorOlder browsers still coveredSafari 16.4, Chrome 111, Firefox 128

The selector change is why space-y spacing on a list now lands on every item except the last. Tailwind’s upgrade guide even swaps space-y-4 for flex flex-col gap-4 in its own migration example.

Sources disagree on list images. Some older tutorials say Tailwind has no list-style-image utility.

The v3 documentation settles it. It already lists list-image-none, arbitrary list-image-[url(checkmark.png)] values and the theme.listStyleImage key.

If older browsers are on your list, check your cross-browser compatibility requirements before upgrading. The upgrade guide says to stay on v3.4 until your browser support requirements change.

Why are Tailwind list classes not working?

Most of the time the list has no marker class on itself, the li has a display other than list-item, Tailwind never generated the class name, or the HTML comes from somewhere you don’t control. The fixes below follow that order.

  1. Put list-disc or list-decimal on the ul or ol, not on a wrapper div.
  2. Check the li display. MDN documents that ::marker only exists on elements with display: list-item, so flex, grid or block classes on the li remove the bullet.
  3. Check class detection. Tailwind scans source files as plain text, so a string like list-${type} never exists as a full class name and is not generated. Map values to complete class names, or force generation with @source inline() (Tailwind CSS v4 documentation).
  4. For Markdown or CMS output, wrap the content in prose or add an arbitrary variant such as [&_ul]:list-disc on the wrapper.

The @tailwindcss/typography plugin styles ordered and unordered lists along with headings and tables, and not-prose opts a block back out (Tailwind CSS documentation). In v4 you register it with @plugin “@tailwindcss/typography” in your CSS.

Syncfusion documents this exact collision. Its Rich Text Editor shows lists without bullets or numbers when Preflight is loaded, and its Tailwind Preflight guide fixes it with !important overrides on ul and ol.

When a class on an item still loses, a CSS specificity calculator shows which selector wins. Fix the weaker rule instead of stacking !important.

Tailwind CSS List Utilities FAQ

Does list-none hurt screen reader accessibility?

Yes, in Safari with VoiceOver. WebKit treats a list without visible markers as a layout list and skips the list announcement, so add role=”list” to the ul. Rocket Validator flags that role as redundant, but the warning is safe to accept.

Can you turn off Preflight for lists only?

Not with a single switch. Omitting the Preflight import removes every reset, not just the list rule, and v3 behaves the same through corePlugins: { preflight: false }.

For lists alone, put your own rule in @layer base, such as ul { list-style-type: disc; padding-left: 1.25rem; } (Tailwind CSS documentation).

Does the marker variant recolor a list-image bullet?

No. The marker variant sets color on the ::marker box, which colors text bullets and numbers. An image from list-image keeps its own colors, so swap the file or switch to inline SVG, whose icons take color from text-* classes.

Are ready-made Tailwind list components worth using?

Rarely for plain bullets, since a ul with list-disc and pl-5 is shorter than any component.

Libraries pay off for richer rows. Material Tailwind ships list examples with profile pictures and badges. Tailwind Plus (formerly Tailwind UI), the paid component library from Tailwind Labs, closed to new customers in September 2026 when Tailwind Labs joined Shopify, although existing customers keep their access.

Where List Styles Belong in a Tailwind CSS Project

Site-wide defaults go in an @layer base rule. Repeated icon rows go in a shared component, and utility classes on the list handle the one-offs.

Work through them in this order, because the base layer reaches the widest surface first. Markdown and CMS output cannot carry per-list classes, so only a wrapper or base-level rule styles it.

  • Base rule for ul and ol defaults
  • Shared component for icon lists
  • Utility classes for exceptions

The trade-off is menus. A base rule that restores bullets restores them on every navigation ul too, so each menu needs list-none, and a navbar built with Tailwind is the natural place to see that cost.

Verified against the Tailwind CSS v4.3 documentation in October 2026. A change to Preflight’s list rule would reverse the first step.