10 / 14
All 14 in 14 Material Design Components in Tailwind CSS
- 01Tailwind Material Design 3 Buttons
- 02Tailwind Material Design 3 Cards
- 03Tailwind Material Design Navigation Bar, Rail and Drawer
- 04Tailwind Material Design Text Fields and Form Controls
- 05Tailwind Material Design Dialog, Bottom Sheet and Snackbar
- 06Tailwind Material Design Data Table
- 07Tailwind Material Design Tabs with Sliding Indicator
- 08Tailwind Material Design Chips with Live Filtering
- 09Tailwind Material Design Stepper and Checkout Flow
- 10Tailwind Material Design Tooltips, Progress and Badges
- 11Tailwind Material Design Expansion Panels and Accordion
- 12Tailwind Material Design Lists and Swipeable Rows
- 13Tailwind Material Design Color Palette and Theme Switcher
- 14Tailwind Material Design Responsive App Shell Layout
Tailwind Material Design Tooltips, Progress and Badges
Plain tooltips in all four positions, a rich tooltip, linear and circular progress (determinate and indeterminate) and the badge family. Two things are better than the CSS original: tooltips appear on focus-visible as well as hover, so keyboard users get them; and the circular indicator animates a real @property angle instead of a rotating mask.
Untitled UI Pro: 5,000+ React + Tailwind CSS v4 components · from $349 one-time
Untitled UI React · Affiliate link
The code
<section class="tmd-10 w-full min-h-svh bg-tmd10-bg font-tmd10-sans text-tmd10-onsurface px-5 py-9" aria-label="Material Design tooltips and progress demo">
<div class="w-[min(880px,100%)] mx-auto grid gap-6">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Tooltips, progress & badges</h2>
<p class="text-sm text-tmd10-onvariant">Tooltips answer to <code class="font-tmd10-mono text-[12px]">focus-visible</code> as well as hover; the ring animates a registered angle.</p>
</header>
<div class="grid gap-6 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Tooltips — tab to them, don't just hover</span>
<div class="flex flex-wrap justify-center gap-8 py-6">
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-t" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">north</span></button>
<span id="tmd10-tip-t" role="tooltip" class="absolute bottom-full left-1/2 [transform:translateX(-50%)] mb-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:top-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-t-tmd10-inverse">Above the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-b" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">south</span></button>
<span id="tmd10-tip-b" role="tooltip" class="absolute top-full left-1/2 [transform:translateX(-50%)] mt-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:bottom-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-b-tmd10-inverse">Below the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-l" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">west</span></button>
<span id="tmd10-tip-l" role="tooltip" class="absolute right-full top-1/2 [transform:translateY(-50%)] mr-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:left-full after:top-1/2 after:[transform:translateY(-50%)] after:border-4 after:border-transparent after:border-l-tmd10-inverse">To the left</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-r" class="relative overflow-hidden inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-sm font-medium text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[18px] leading-none" aria-hidden="true">info</span>Rich tooltip</button>
<span id="tmd10-tip-r" role="tooltip" class="absolute left-full top-1/2 ml-2 w-56 p-3 rounded-xl bg-tmd10-surface border border-tmd10-outlinevariant shadow-tmd10-2 text-left opacity-0 invisible [transform:translate(4px,-50%)] transition-[opacity,visibility,transform] duration-200 group-hover:opacity-100 group-hover:visible group-hover:[transform:translate(0,-50%)] group-focus-visible:opacity-100 group-focus-visible:visible group-focus-visible:[transform:translate(0,-50%)] motion-reduce:transition-none">
<b class="block mb-1 text-[13px] font-medium">Calibration drift</b>
<span class="block text-xs leading-5 text-tmd10-onvariant">A rich tooltip carries a title, body copy and an action. It is a surface, not an inverse bubble.</span>
</span>
</span>
</div>
</div>
<div class="grid gap-5 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Progress indicators</span>
<div class="grid gap-1.5">
<div class="flex justify-between text-xs text-tmd10-onvariant"><span>Determinate — upload</span><span class="font-tmd10-mono tabular-nums">68%</span></div>
<div role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress" class="h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="h-full w-[68%] rounded-full bg-tmd10-primary transition-[width] duration-300"></div></div>
</div>
<div class="grid gap-1.5">
<span class="text-xs text-tmd10-onvariant">Indeterminate — syncing</span>
<div role="progressbar" aria-label="Syncing" class="relative h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="absolute inset-y-0 w-2/5 rounded-full bg-tmd10-primary animate-tmd10-indeterminate motion-reduce:animate-none motion-reduce:w-full motion-reduce:opacity-40"></div></div>
</div>
<div class="flex flex-wrap items-center gap-8 pt-2">
<div class="grid justify-items-center gap-2">
<div class="relative grid place-items-center w-20 h-20 rounded-full [--tmd10-deg:245deg] [background:conic-gradient(var(--color-tmd10-primary)_var(--tmd10-deg),var(--color-tmd10-track)_0)] transition-[--tmd10-deg] duration-700 hover:[--tmd10-deg:324deg] motion-reduce:transition-none" role="img" aria-label="68 percent complete">
<div class="absolute inset-1.5 rounded-full bg-tmd10-surface"></div>
<span class="relative font-tmd10-mono text-sm font-medium tabular-nums">68%</span>
</div>
<span class="text-[11px] text-tmd10-onvariant">hover to animate</span>
</div>
<div class="grid justify-items-center gap-2">
<div class="w-20 h-20 rounded-full [background:conic-gradient(var(--color-tmd10-primary)_0_90deg,var(--color-tmd10-track)_90deg_360deg)] [mask:radial-gradient(farthest-side,transparent_calc(100%-6px),black_calc(100%-6px))] animate-spin [animation-duration:1.1s] motion-reduce:animate-none" role="progressbar" aria-label="Loading"></div>
<span class="text-[11px] text-tmd10-onvariant">indeterminate ring</span>
</div>
</div>
</div>
<div class="grid gap-4 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Badges</span>
<div class="flex flex-wrap items-center gap-7">
<span class="relative inline-block">
<button type="button" aria-label="Notifications, 3 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">notifications</span></button>
<span class="absolute top-1 right-1 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">3</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Mail, 128 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">mail</span></button>
<span class="absolute top-0.5 right-0 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">128</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Updates available" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">cloud_sync</span></button>
<span class="absolute top-2 right-2 w-2 h-2 rounded-full bg-tmd10-error pointer-events-none" aria-hidden="true"></span>
</span>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-full bg-tmd10-primarycontainer text-tmd10-onprimarycontainer text-xs font-medium"><span class="w-1.5 h-1.5 rounded-full bg-tmd10-primary"></span>Live · 18 stations</span>
</div>
</div>
</div>
</section><section class="tmd-10 w-full min-h-svh bg-tmd10-bg font-tmd10-sans text-tmd10-onsurface px-5 py-9" aria-label="Material Design tooltips and progress demo">
<div class="w-[min(880px,100%)] mx-auto grid gap-6">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Tooltips, progress & badges</h2>
<p class="text-sm text-tmd10-onvariant">Tooltips answer to <code class="font-tmd10-mono text-[12px]">focus-visible</code> as well as hover; the ring animates a registered angle.</p>
</header>
<div class="grid gap-6 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Tooltips — tab to them, don't just hover</span>
<div class="flex flex-wrap justify-center gap-8 py-6">
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-t" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">north</span></button>
<span id="tmd10-tip-t" role="tooltip" class="absolute bottom-full left-1/2 [transform:translateX(-50%)] mb-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:top-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-t-tmd10-inverse">Above the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-b" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">south</span></button>
<span id="tmd10-tip-b" role="tooltip" class="absolute top-full left-1/2 [transform:translateX(-50%)] mt-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:bottom-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-b-tmd10-inverse">Below the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-l" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">west</span></button>
<span id="tmd10-tip-l" role="tooltip" class="absolute right-full top-1/2 [transform:translateY(-50%)] mr-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:left-full after:top-1/2 after:[transform:translateY(-50%)] after:border-4 after:border-transparent after:border-l-tmd10-inverse">To the left</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-r" class="relative overflow-hidden inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-sm font-medium text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[18px] leading-none" aria-hidden="true">info</span>Rich tooltip</button>
<span id="tmd10-tip-r" role="tooltip" class="absolute left-full top-1/2 ml-2 w-56 p-3 rounded-xl bg-tmd10-surface border border-tmd10-outlinevariant shadow-tmd10-2 text-left opacity-0 invisible [transform:translate(4px,-50%)] transition-[opacity,visibility,transform] duration-200 group-hover:opacity-100 group-hover:visible group-hover:[transform:translate(0,-50%)] group-focus-visible:opacity-100 group-focus-visible:visible group-focus-visible:[transform:translate(0,-50%)] motion-reduce:transition-none">
<b class="block mb-1 text-[13px] font-medium">Calibration drift</b>
<span class="block text-xs leading-5 text-tmd10-onvariant">A rich tooltip carries a title, body copy and an action. It is a surface, not an inverse bubble.</span>
</span>
</span>
</div>
</div>
<div class="grid gap-5 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Progress indicators</span>
<div class="grid gap-1.5">
<div class="flex justify-between text-xs text-tmd10-onvariant"><span>Determinate — upload</span><span class="font-tmd10-mono tabular-nums">68%</span></div>
<div role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress" class="h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="h-full w-[68%] rounded-full bg-tmd10-primary transition-[width] duration-300"></div></div>
</div>
<div class="grid gap-1.5">
<span class="text-xs text-tmd10-onvariant">Indeterminate — syncing</span>
<div role="progressbar" aria-label="Syncing" class="relative h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="absolute inset-y-0 w-2/5 rounded-full bg-tmd10-primary animate-tmd10-indeterminate motion-reduce:animate-none motion-reduce:w-full motion-reduce:opacity-40"></div></div>
</div>
<div class="flex flex-wrap items-center gap-8 pt-2">
<div class="grid justify-items-center gap-2">
<div class="relative grid place-items-center w-20 h-20 rounded-full [--tmd10-deg:245deg] [background:conic-gradient(var(--color-tmd10-primary)_var(--tmd10-deg),var(--color-tmd10-track)_0)] transition-[--tmd10-deg] duration-700 hover:[--tmd10-deg:324deg] motion-reduce:transition-none" role="img" aria-label="68 percent complete">
<div class="absolute inset-1.5 rounded-full bg-tmd10-surface"></div>
<span class="relative font-tmd10-mono text-sm font-medium tabular-nums">68%</span>
</div>
<span class="text-[11px] text-tmd10-onvariant">hover to animate</span>
</div>
<div class="grid justify-items-center gap-2">
<div class="w-20 h-20 rounded-full [background:conic-gradient(var(--color-tmd10-primary)_0_90deg,var(--color-tmd10-track)_90deg_360deg)] [mask:radial-gradient(farthest-side,transparent_calc(100%-6px),black_calc(100%-6px))] animate-spin [animation-duration:1.1s] motion-reduce:animate-none" role="progressbar" aria-label="Loading"></div>
<span class="text-[11px] text-tmd10-onvariant">indeterminate ring</span>
</div>
</div>
</div>
<div class="grid gap-4 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Badges</span>
<div class="flex flex-wrap items-center gap-7">
<span class="relative inline-block">
<button type="button" aria-label="Notifications, 3 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">notifications</span></button>
<span class="absolute top-1 right-1 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">3</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Mail, 128 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">mail</span></button>
<span class="absolute top-0.5 right-0 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">128</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Updates available" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">cloud_sync</span></button>
<span class="absolute top-2 right-2 w-2 h-2 rounded-full bg-tmd10-error pointer-events-none" aria-hidden="true"></span>
</span>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-full bg-tmd10-primarycontainer text-tmd10-onprimarycontainer text-xs font-medium"><span class="w-1.5 h-1.5 rounded-full bg-tmd10-primary"></span>Live · 18 stations</span>
</div>
</div>
</div>
</section>@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}Setup — do this once per project
Skip this if your project already runs Tailwind v4.
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>Compiles in the browser — perfect for a CodePen or a quick test, not for production.
npm install tailwindcss @tailwindcss/vite
npm install tailwindcss @tailwindcss/vite// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})// vite.config.js ("type": "module" in package.json)
import { defineConfig } from 'vite'
import tailwindcss from '@tailwindcss/vite'
export default defineConfig({
plugins: [tailwindcss()],
})Any v4 setup works — Vite, PostCSS or the CLI. Nothing in this demo is bundler-specific.
@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}<link rel="preconnect" href="https://fonts.googleapis.com"> <link rel="preconnect" href="https://fonts.gstatic.com" crossorigin> <link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300..700&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0&display=swap">
<link rel="preconnect" href="https://fonts.googleapis.com">
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link rel="stylesheet" href="https://fonts.googleapis.com/css2?family=Roboto+Flex:opsz,wght@8..144,300..700&family=Roboto+Mono:wght@400;500&family=Material+Symbols+Outlined:opsz,wght,FILL,GRAD@24,400,0..1,0&display=swap">What changed in the port
Which parts became utilities, which stayed CSS, and why.
- Every tooltip gained a
group-focus-visible:twin, which is the accessibility bug the CSS original shipped: hover-only tooltips fail WCAG 2.1.1. invisiblealongsideopacity-0is deliberate — a merely transparent tooltip is still hit-testable and can still be announced.- The indeterminate bar's keyframes stay in the stylesheet, but the animation is a theme token, so the element reads
animate-tmd10-indeterminateand the timing lives with the colours. @propertyis the demo's one irreducible CSS block, and it buys a genuinely better circular indicator than the CSS version's rotating mask.- The four tooltip positions are four class lists on one bubble recipe, arrow included — the arrow is a bordered
after:with one side coloured by a token. - Badges are
pointer-events-noneso they never steal a tap from the 44px button underneath.
Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Material Design Tooltips, Progress and Badges
Source: https://codefronts.com/design-styles/tailwind-material-design-components/tailwind-material-design-tooltips-progress-badges/
Plain tooltips in all four positions, a rich tooltip, linear and circular progress (determinate and indeterminate) and the badge family. Two things are better than the CSS original: tooltips appear on focus-visible as well as hover, so keyboard users get them; and the circular indicator animates a real @property angle instead of a rotating mask.
## HTML
```html
<section class="tmd-10 w-full min-h-svh bg-tmd10-bg font-tmd10-sans text-tmd10-onsurface px-5 py-9" aria-label="Material Design tooltips and progress demo">
<div class="w-[min(880px,100%)] mx-auto grid gap-6">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Tooltips, progress & badges</h2>
<p class="text-sm text-tmd10-onvariant">Tooltips answer to <code class="font-tmd10-mono text-[12px]">focus-visible</code> as well as hover; the ring animates a registered angle.</p>
</header>
<div class="grid gap-6 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Tooltips — tab to them, don't just hover</span>
<div class="flex flex-wrap justify-center gap-8 py-6">
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-t" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">north</span></button>
<span id="tmd10-tip-t" role="tooltip" class="absolute bottom-full left-1/2 [transform:translateX(-50%)] mb-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:top-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-t-tmd10-inverse">Above the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-b" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">south</span></button>
<span id="tmd10-tip-b" role="tooltip" class="absolute top-full left-1/2 [transform:translateX(-50%)] mt-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:bottom-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-b-tmd10-inverse">Below the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-l" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">west</span></button>
<span id="tmd10-tip-l" role="tooltip" class="absolute right-full top-1/2 [transform:translateY(-50%)] mr-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:left-full after:top-1/2 after:[transform:translateY(-50%)] after:border-4 after:border-transparent after:border-l-tmd10-inverse">To the left</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-r" class="relative overflow-hidden inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-sm font-medium text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[18px] leading-none" aria-hidden="true">info</span>Rich tooltip</button>
<span id="tmd10-tip-r" role="tooltip" class="absolute left-full top-1/2 ml-2 w-56 p-3 rounded-xl bg-tmd10-surface border border-tmd10-outlinevariant shadow-tmd10-2 text-left opacity-0 invisible [transform:translate(4px,-50%)] transition-[opacity,visibility,transform] duration-200 group-hover:opacity-100 group-hover:visible group-hover:[transform:translate(0,-50%)] group-focus-visible:opacity-100 group-focus-visible:visible group-focus-visible:[transform:translate(0,-50%)] motion-reduce:transition-none">
<b class="block mb-1 text-[13px] font-medium">Calibration drift</b>
<span class="block text-xs leading-5 text-tmd10-onvariant">A rich tooltip carries a title, body copy and an action. It is a surface, not an inverse bubble.</span>
</span>
</span>
</div>
</div>
<div class="grid gap-5 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Progress indicators</span>
<div class="grid gap-1.5">
<div class="flex justify-between text-xs text-tmd10-onvariant"><span>Determinate — upload</span><span class="font-tmd10-mono tabular-nums">68%</span></div>
<div role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress" class="h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="h-full w-[68%] rounded-full bg-tmd10-primary transition-[width] duration-300"></div></div>
</div>
<div class="grid gap-1.5">
<span class="text-xs text-tmd10-onvariant">Indeterminate — syncing</span>
<div role="progressbar" aria-label="Syncing" class="relative h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="absolute inset-y-0 w-2/5 rounded-full bg-tmd10-primary animate-tmd10-indeterminate motion-reduce:animate-none motion-reduce:w-full motion-reduce:opacity-40"></div></div>
</div>
<div class="flex flex-wrap items-center gap-8 pt-2">
<div class="grid justify-items-center gap-2">
<div class="relative grid place-items-center w-20 h-20 rounded-full [--tmd10-deg:245deg] [background:conic-gradient(var(--color-tmd10-primary)_var(--tmd10-deg),var(--color-tmd10-track)_0)] transition-[--tmd10-deg] duration-700 hover:[--tmd10-deg:324deg] motion-reduce:transition-none" role="img" aria-label="68 percent complete">
<div class="absolute inset-1.5 rounded-full bg-tmd10-surface"></div>
<span class="relative font-tmd10-mono text-sm font-medium tabular-nums">68%</span>
</div>
<span class="text-[11px] text-tmd10-onvariant">hover to animate</span>
</div>
<div class="grid justify-items-center gap-2">
<div class="w-20 h-20 rounded-full [background:conic-gradient(var(--color-tmd10-primary)_0_90deg,var(--color-tmd10-track)_90deg_360deg)] [mask:radial-gradient(farthest-side,transparent_calc(100%-6px),black_calc(100%-6px))] animate-spin [animation-duration:1.1s] motion-reduce:animate-none" role="progressbar" aria-label="Loading"></div>
<span class="text-[11px] text-tmd10-onvariant">indeterminate ring</span>
</div>
</div>
</div>
<div class="grid gap-4 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Badges</span>
<div class="flex flex-wrap items-center gap-7">
<span class="relative inline-block">
<button type="button" aria-label="Notifications, 3 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">notifications</span></button>
<span class="absolute top-1 right-1 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">3</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Mail, 128 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">mail</span></button>
<span class="absolute top-0.5 right-0 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">128</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Updates available" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">cloud_sync</span></button>
<span class="absolute top-2 right-2 w-2 h-2 rounded-full bg-tmd10-error pointer-events-none" aria-hidden="true"></span>
</span>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-full bg-tmd10-primarycontainer text-tmd10-onprimarycontainer text-xs font-medium"><span class="w-1.5 h-1.5 rounded-full bg-tmd10-primary"></span>Live · 18 stations</span>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}
```Here's a working CSS demo from CodeFronts. Use it as-is or adapt to your framework. All classes are scoped under a unique prefix so the code won't collide with your existing styles. MIT licensed.
Demo: Tailwind Material Design Tooltips, Progress and Badges
Source: https://codefronts.com/design-styles/tailwind-material-design-components/tailwind-material-design-tooltips-progress-badges/
Plain tooltips in all four positions, a rich tooltip, linear and circular progress (determinate and indeterminate) and the badge family. Two things are better than the CSS original: tooltips appear on focus-visible as well as hover, so keyboard users get them; and the circular indicator animates a real @property angle instead of a rotating mask.
## HTML
```html
<section class="tmd-10 w-full min-h-svh bg-tmd10-bg font-tmd10-sans text-tmd10-onsurface px-5 py-9" aria-label="Material Design tooltips and progress demo">
<div class="w-[min(880px,100%)] mx-auto grid gap-6">
<header class="grid gap-1.5">
<h2 class="text-[26px] font-medium tracking-[-0.01em]">Tooltips, progress & badges</h2>
<p class="text-sm text-tmd10-onvariant">Tooltips answer to <code class="font-tmd10-mono text-[12px]">focus-visible</code> as well as hover; the ring animates a registered angle.</p>
</header>
<div class="grid gap-6 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Tooltips — tab to them, don't just hover</span>
<div class="flex flex-wrap justify-center gap-8 py-6">
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-t" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">north</span></button>
<span id="tmd10-tip-t" role="tooltip" class="absolute bottom-full left-1/2 [transform:translateX(-50%)] mb-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:top-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-t-tmd10-inverse">Above the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-b" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">south</span></button>
<span id="tmd10-tip-b" role="tooltip" class="absolute top-full left-1/2 [transform:translateX(-50%)] mt-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:bottom-full after:left-1/2 after:[transform:translateX(-50%)] after:border-4 after:border-transparent after:border-b-tmd10-inverse">Below the trigger</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-l" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[20px] leading-none" aria-hidden="true">west</span></button>
<span id="tmd10-tip-l" role="tooltip" class="absolute right-full top-1/2 [transform:translateY(-50%)] mr-2 px-2.5 py-1.5 rounded bg-tmd10-inverse text-tmd10-oninverse text-xs whitespace-nowrap opacity-0 invisible transition-[opacity,visibility] duration-150 group-hover:opacity-100 group-hover:visible group-focus-visible:opacity-100 group-focus-visible:visible after:absolute after:left-full after:top-1/2 after:[transform:translateY(-50%)] after:border-4 after:border-transparent after:border-l-tmd10-inverse">To the left</span>
</span>
<span class="group relative inline-block">
<button type="button" aria-describedby="tmd10-tip-r" class="relative overflow-hidden inline-flex items-center gap-2 min-h-11 px-4 rounded-full border border-tmd10-outlinevariant bg-tmd10-surface cursor-pointer text-sm font-medium text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-0 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[18px] leading-none" aria-hidden="true">info</span>Rich tooltip</button>
<span id="tmd10-tip-r" role="tooltip" class="absolute left-full top-1/2 ml-2 w-56 p-3 rounded-xl bg-tmd10-surface border border-tmd10-outlinevariant shadow-tmd10-2 text-left opacity-0 invisible [transform:translate(4px,-50%)] transition-[opacity,visibility,transform] duration-200 group-hover:opacity-100 group-hover:visible group-hover:[transform:translate(0,-50%)] group-focus-visible:opacity-100 group-focus-visible:visible group-focus-visible:[transform:translate(0,-50%)] motion-reduce:transition-none">
<b class="block mb-1 text-[13px] font-medium">Calibration drift</b>
<span class="block text-xs leading-5 text-tmd10-onvariant">A rich tooltip carries a title, body copy and an action. It is a surface, not an inverse bubble.</span>
</span>
</span>
</div>
</div>
<div class="grid gap-5 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Progress indicators</span>
<div class="grid gap-1.5">
<div class="flex justify-between text-xs text-tmd10-onvariant"><span>Determinate — upload</span><span class="font-tmd10-mono tabular-nums">68%</span></div>
<div role="progressbar" aria-valuenow="68" aria-valuemin="0" aria-valuemax="100" aria-label="Upload progress" class="h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="h-full w-[68%] rounded-full bg-tmd10-primary transition-[width] duration-300"></div></div>
</div>
<div class="grid gap-1.5">
<span class="text-xs text-tmd10-onvariant">Indeterminate — syncing</span>
<div role="progressbar" aria-label="Syncing" class="relative h-1 rounded-full bg-tmd10-track overflow-hidden"><div class="absolute inset-y-0 w-2/5 rounded-full bg-tmd10-primary animate-tmd10-indeterminate motion-reduce:animate-none motion-reduce:w-full motion-reduce:opacity-40"></div></div>
</div>
<div class="flex flex-wrap items-center gap-8 pt-2">
<div class="grid justify-items-center gap-2">
<div class="relative grid place-items-center w-20 h-20 rounded-full [--tmd10-deg:245deg] [background:conic-gradient(var(--color-tmd10-primary)_var(--tmd10-deg),var(--color-tmd10-track)_0)] transition-[--tmd10-deg] duration-700 hover:[--tmd10-deg:324deg] motion-reduce:transition-none" role="img" aria-label="68 percent complete">
<div class="absolute inset-1.5 rounded-full bg-tmd10-surface"></div>
<span class="relative font-tmd10-mono text-sm font-medium tabular-nums">68%</span>
</div>
<span class="text-[11px] text-tmd10-onvariant">hover to animate</span>
</div>
<div class="grid justify-items-center gap-2">
<div class="w-20 h-20 rounded-full [background:conic-gradient(var(--color-tmd10-primary)_0_90deg,var(--color-tmd10-track)_90deg_360deg)] [mask:radial-gradient(farthest-side,transparent_calc(100%-6px),black_calc(100%-6px))] animate-spin [animation-duration:1.1s] motion-reduce:animate-none" role="progressbar" aria-label="Loading"></div>
<span class="text-[11px] text-tmd10-onvariant">indeterminate ring</span>
</div>
</div>
</div>
<div class="grid gap-4 p-6 rounded-3xl bg-tmd10-surface shadow-tmd10-1">
<span class="font-tmd10-mono text-[11px] uppercase tracking-[0.1em] text-tmd10-onvariant">Badges</span>
<div class="flex flex-wrap items-center gap-7">
<span class="relative inline-block">
<button type="button" aria-label="Notifications, 3 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">notifications</span></button>
<span class="absolute top-1 right-1 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">3</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Mail, 128 unread" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">mail</span></button>
<span class="absolute top-0.5 right-0 min-w-4 h-4 px-1 grid place-items-center rounded-full bg-tmd10-error text-white font-tmd10-mono text-[10px] tabular-nums pointer-events-none" aria-hidden="true">128</span>
</span>
<span class="relative inline-block">
<button type="button" aria-label="Updates available" class="relative overflow-hidden grid place-items-center min-w-11 min-h-11 rounded-full border-0 bg-transparent cursor-pointer text-tmd10-onsurface focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-tmd10-primary before:absolute before:inset-1 before:rounded-full before:bg-current before:opacity-0 before:transition-opacity hover:before:opacity-[0.08]"><span class="font-tmd10-icon text-[22px] leading-none" aria-hidden="true">cloud_sync</span></button>
<span class="absolute top-2 right-2 w-2 h-2 rounded-full bg-tmd10-error pointer-events-none" aria-hidden="true"></span>
</span>
<span class="inline-flex items-center gap-2 h-8 px-3 rounded-full bg-tmd10-primarycontainer text-tmd10-onprimarycontainer text-xs font-medium"><span class="w-1.5 h-1.5 rounded-full bg-tmd10-primary"></span>Live · 18 stations</span>
</div>
</div>
</div>
</section>
```
## CSS
```css
@import "tailwindcss";
@theme {
--color-tmd10-bg: #e0f2f1;
--color-tmd10-bg: oklch(0.948 0.019 192.8);
--color-tmd10-surface: #ffffff;
--color-tmd10-surface: oklch(1 0 89.9);
--color-tmd10-primary: #00695c;
--color-tmd10-primary: oklch(0.467 0.084 180.4);
--color-tmd10-primarycontainer: #cfe9e5;
--color-tmd10-primarycontainer: oklch(0.914 0.028 185.5);
--color-tmd10-onprimarycontainer: #002019;
--color-tmd10-onprimarycontainer: oklch(0.217 0.041 175.3);
--color-tmd10-track: #cfe0dd;
--color-tmd10-track: oklch(0.893 0.019 184.2);
--color-tmd10-onsurface: #161d1c;
--color-tmd10-onsurface: oklch(0.223 0.01 185.9);
--color-tmd10-onvariant: #4a5b58;
--color-tmd10-onvariant: oklch(0.456 0.022 183.5);
--color-tmd10-outlinevariant: #c4d6d3;
--color-tmd10-outlinevariant: oklch(0.862 0.02 184.8);
--color-tmd10-inverse: #2b3231;
--color-tmd10-inverse: oklch(0.31 0.01 186.3);
--color-tmd10-oninverse: #eff1f0;
--color-tmd10-oninverse: oklch(0.956 0.003 165.1);
--color-tmd10-error: #b3261e;
--color-tmd10-error: oklch(0.501 0.178 28.7);
--font-tmd10-sans: 'Roboto Flex', 'Roboto', system-ui, sans-serif;
--font-tmd10-mono: 'Roboto Mono', ui-monospace, monospace;
--font-tmd10-icon: 'Material Symbols Outlined';
--shadow-tmd10-1: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 1px 3px 1px rgb(0 0 0 / 0.15);
--shadow-tmd10-2: 0 1px 2px 0 rgb(0 0 0 / 0.3), 0 2px 6px 2px rgb(0 0 0 / 0.15);
--animate-tmd10-indeterminate: tmd10-indeterminate 1.6s cubic-bezier(0.4, 0, 0.2, 1) infinite;
}
/* Keyframes cannot be utilities — but the animation that uses them can, because the
@theme --animate-* entry above mints the animate-tmd10-indeterminate utility. */
@keyframes tmd10-indeterminate {
0% {
left: -40%;
}
100% {
left: 100%;
}
}
/* A custom property must be registered to interpolate. Without this the conic ring
jumps between angles instead of sweeping — and there is no utility for @property. */
@property --tmd10-deg {
syntax: '<angle>';
inherits: false;
initial-value: 0deg;
}
.tmd-10 {
width: 100%;
min-height: 100svh;
}
.tmd-10 .font-tmd10-icon {
font-feature-settings: 'liga';
}
```How this works
A tooltip is a group and a sibling.
<span class="group relative inline-block">
<button aria-describedby="tip1">…</button>
<span id="tip1" role="tooltip"
class="absolute bottom-full left-1/2 [transform:translateX(-50%)] mb-2 opacity-0 invisible
transition-[opacity,visibility] group-hover:opacity-100 group-hover:visible
group-focus-visible:opacity-100 group-focus-visible:visible">invisible rather than opacity-0 alone matters: a transparent tooltip is still hit-testable and still read by some screen readers.
Indeterminate progress is a token. The keyframes go in the stylesheet (they must), but the animation becomes a utility because --animate-* in @theme mints one. So the bar reads animate-tmd10-indeterminate, and the timing lives with the rest of the theme.
The circular ring animates because the angle is registered. Custom properties are strings unless declared, and a string cannot interpolate. @property --tmd10-deg { syntax: '<angle>' } makes the conic sweep animatable — which is why a hover on the ring glides instead of snapping.
Make it yours
- Tooltip delay: add
delay-500to the bubble's transition for the M3 long-press feel. - Bubble colour:
--color-tmd10-inverse— M3 tooltips use the inverse surface, and both the bubble and its arrow read the same token. - Ring thickness: the inner mask circle's
inset-1.5; ring size isw-20 h-20. - Progress values are
w-[68%]and[--tmd10-deg:245deg]inline — the two places a real app writes a style attribute from data. - Badge shapes: drop the count for a dot badge by removing the text and using
w-2 h-2.
Gotchas — read before shipping
- A tooltip must never be the only source of information; it is a supplement, and
aria-describedbyis what connects it. titleattributes are not tooltips for keyboard or touch users. If you need one of these bubbles, remove thetitleor you get both.- Hover-only tooltips fail WCAG 2.1.1. The
group-focus-visible:twin is not optional. @propertycannot be expressed as a utility and must live in the stylesheet; without registration the conic angle jumps between values.- An indeterminate bar needs
role="progressbar"withoutaria-valuenow; supplying a value contradicts the visual. - Badges on icon buttons must not cover the glyph's hit target — keep
pointer-events-noneon the badge.
Browser support
| Chrome | Safari | Firefox | Edge |
|---|---|---|---|
| 111+ | 16.4+ | 128+ | 111+ |
@property needs Firefox 128+, which matches Tailwind v4's own floor. Without it the ring still renders at its static angle and simply does not animate.