14 Material Design components in Tailwind CSS for admin dashboards, checkout flows, settings panels, and mobile app shells — Material 3 buttons, FABs, elevated cards, navigation bars, drawers, floating-label fields, dialogs, bottom sheets, data tables, tabs, chips, steppers, and expansion panels. Scoped under .tmd-NN for no-collision pasting, prefers-reduced-motion guarded, framework-agnostic.
Building a whole Tailwind app, not just this component?
You just browsed 14 Tailwind material designs. Untitled UI Pro unlocks 5,000+ components and sections and 250+ page examples for React, styled with Tailwind CSS v4. One licence is a one-time $349 with lifetime updates; teams start at $999.
Every Material 3 button in one stage — filled, tonal, elevated, outlined, text, four FAB sizes, four icon-button flavours and a segmented group — and the port replaces Material's twenty .btn--x blocks with one class list per variant. The M3 <b>state layer</b> is the interesting part: it is a before: pseudo-element at bg-current, and hover/focus/active are just three opacity variants on the same element.
The three M3 card containers — elevated, filled and outlined — plus a horizontal media card and a stat tile, laid out on a grid that reflows with no media query. The port's real gain is group-hover:: the whole card is the hover target, so the title colour, the elevation and the media zoom all respond from one hovered ancestor instead of three descendant rules.
The three M3 navigation containers side by side — bottom navigation bar, rail and standard drawer — all driven by radios and peer-checked:, so the active tab, its pill indicator and its filled icon all change with no JavaScript. The port's win is the indicator: an M3 active pill is one peer-checked: background on the icon box, not a ::before rule per item.
Filled and outlined text fields with true floating labels, plus select, textarea, checkbox, radio, switch and slider — and every one of them is peer-driven, so the label, the underline and the helper text all read the input's own state. The port also upgrades validation: user-invalid: shows errors only after the user has actually interacted, which the CSS version's :invalid could not do.
Three overlay patterns on the real <dialog> element — an alert dialog, a full dialog and a bottom sheet — plus a snackbar. The port replaces the CSS collection's checkbox-hack with showModal(), which is the correct upgrade: focus trapping, Esc, inertness and the top layer come from the platform, and Tailwind styles it with open:, backdrop: and starting:.
A real data table: sticky header, sortable columns keyed off aria-sort, row selection that tints the row through has-checked:, status badges, inline progress and a pagination footer. The port's win is that selection and sort state live on the elements the platform already uses — a checkbox and aria-sort — so no row ever carries an .is-selected class.
Newsletter
Enjoying these demos? Get new ones by email.
A short email when a new collection ships. Unsubscribe any time.
Primary tabs with a <b>single sliding indicator</b>, scrollable tabs with snap, and icon-and-label tabs — all zero JavaScript. The indicator is the interesting port: instead of four ::after rules, one absolutely-positioned bar reads an index custom property set by has-checked:, so it glides between tabs and any number of tabs needs no new rule.
All four M3 chip types — assist, filter, input and suggestion — and then the part the CSS version could only fake: the filter chips actually filter. Checking a chip reveals its matching cards through a has-[#id:checked]: rule on the results grid, so a live filter runs with no JavaScript at all.
A horizontal stepper, a vertical stepper and a three-step checkout — all radio-driven, and all three know which steps are done. That is the port's trick: a step's completed look comes from has-[~ …:checked]:, the subsequent-sibling form of :has(), so “a later step is selected, therefore I am complete” is expressible without a single line of script.
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.
Expansion panels on <details>, with the one thing the checkbox-hack version could never have: name="…" makes the accordion <b>exclusive</b> natively, so opening one panel closes the others with no script and no radio hack. Tailwind styles the whole thing through open: and a direct-child arbitrary variant.
The full M3 list vocabulary — one, two and three-line items, leading avatars and icons, trailing metadata, checkboxes and switches, inset dividers and a sticky subheader. The port's structural idea: every item is the same grid-cols-[auto_1fr_auto], so a one-line row and a three-line row differ only in how many children the middle track holds.
The M3 colour system laid out as role pairs — primary / on-primary, container / on-container, surface tiers, error — with four themes switchable by radio and no JavaScript. It is also the collection's one honest lesson about v4: @theme tokens are build-time, so a runtime theme swap uses plain custom properties consumed as bg-[var(--p)], and knowing which to reach for is the whole skill.
The whole thing assembled: a sticky top app bar that gains elevation on scroll, a navigation rail that becomes a modal drawer under md, and a dashboard grid of the earlier demos' components. Every M3 layout breakpoint is a Tailwind responsive modifier, and the only JavaScript is the drawer's focus management and one scroll listener.
FAQ
Frequently asked questions
How do I make a true Material floating label in Tailwind that moves up on focus?
The label sits inside the field as a placeholder, then moves up and shrinks once the field has focus or a value. In these demos that motion is pure CSS: the label is a real <label> positioned over the input, reacting to the input's :focus and :not(:placeholder-shown) states through Tailwind's peer utilities. Give the input a peer class and an empty placeholder=" " attribute so :placeholder-shown is meaningful, then style the label with peer-focus: and peer-[:not(:placeholder-shown)]: variants to translate and scale it. The bug everyone hits: the label never drops back down when the field is empty because the input has no placeholder at all, so :placeholder-shown is never true. Set placeholder=" " — a single space — and never omit it. That one space is what makes the whole thing work.
How does the Material tabs sliding indicator work without JavaScript?
Instead of giving every tab its own bottom border, there is one absolutely-positioned indicator bar that slides horizontally under the active tab. The tabs are hidden radio inputs with <label> triggers; checking a radio drives that bar's transform: translateX() and width through a CSS transition, so the underline appears to travel between tabs. Because it is one element moving rather than a border toggling on and off, you get the smooth Material motion for free. The usual mistake is animating left or margin-left, which is not compositor-friendly and stutters — animate transform instead. If your tabs are not equal width you must set the indicator's width and offset per tab in CSS, since pure CSS cannot measure them. The scrollable variant also needs scroll-snap-align on each tab so the active one snaps into view when the row overflows.
What is the accessible way to build a Material dialog and bottom sheet?
All three overlays — the alert dialog, the full dialog and the bottom sheet — are built on the native <dialog> element, which is what makes them accessible with almost no extra work. Opening with dialog.showModal() gives you a real top layer, a ::backdrop you can style with Tailwind's backdrop: variant, automatic focus trapping and Esc-to-close, and it marks the rest of the page inert. You do not hand-roll a focus loop or a role attribute. Add aria-labelledby pointing at the title so screen readers announce it. The bottom sheet is the same element slid up from the bottom with a transform. The snackbar is not a dialog — it is a transient status message that should be a live region so it is announced without stealing focus. The common trap: calling .show() or setting the open attribute instead of .showModal(), which skips the top layer and the inert behaviour.
How does the sortable Material data table track sort state accessibly?
Sorting state lives on the header cells as aria-sort, which is both the accessibility signal and the styling hook. Each sortable column header is a <th> with aria-sort set to ascending, descending or none, and the sort arrow is drawn with a Tailwind attribute selector keyed off that value — so the visible indicator can never drift from what a screen reader announces. The sticky header is sticky top-0 on the header row with a background so rows scroll underneath it. Row selection uses a checkbox per row that tints the row through the has-[:checked]: variant, and a header checkbox toggles all. Two things to get right: only ever put aria-sort on the one column currently sorted, because the spec allows a single sorted column at a time; and give the sticky header an opaque background, or the body rows will bleed through it as they scroll behind.
Do these Material components need JavaScript or a component runtime?
Most are pure CSS. The tabs, chips, stepper and checkout run on hidden radio and checkbox inputs; the expansion panels use <details>; the text-field labels use :placeholder-shown; row selection and chip filtering lean on the has-[] and peer variants. Where you do need script it is small and honest: .showModal() to open the dialogs, a scroll listener to add elevation to the app bar in the shell layout, and the theme switcher writing a class or attribute so the palette swaps. Nothing depends on a framework runtime and there is no hydration step. The one place to resist a pure-CSS solution is the dialog — the checkbox-hack modal cannot trap focus or mark the background inert, so use the real <dialog> even though it costs two lines of script. Copy the markup and the utility classes straight in; there is no build config beyond Tailwind itself.
How do I implement Material Design 3 color roles and a theme switcher with Tailwind?
Material 3 does not think in primary and secondary hex values; it thinks in role pairs — primary / on-primary, primary-container / on-container, and surface with its tint levels. The palette demo lays these out as pairs so you can see which foreground is guaranteed legible on which background. Wire them as CSS custom properties such as --md-primary and expose them to Tailwind through the theme config, then reference bg-primary text-on-primary rather than raw colours. Dark mode and the switcher then become one change: redefine the custom properties under a .dark class or a data-theme attribute and every component re-tints, because they all read the roles, not fixed values. The mistake is hardcoding text-white on a coloured button — it breaks the instant the theme changes. Always pair a background role with its matching on- role.
When should I use MUI or the official Material Web components instead of these?
Reach for the official Material Web components or a kit like MUI when your app is a large, stateful React or Angular product that already lives in that ecosystem — you want their virtualised tables, form-state integration and long-term spec updates, not hand-copied markup. These demos are for the opposite case: a static site, a landing page, a Rails or Django template, or any project where pulling in a component runtime is overkill. They are also a poor fit when you need behaviour the pattern genuinely lacks — the pure-CSS tabs and stepper hold state in radio inputs, so you cannot read or set that state from application code without adding your own script. And do not lean on them to claim full Material Design compliance in an audit; they cover the common components faithfully but are a curated set of 14, not the whole specification. For copy-paste UI in a framework-light stack, though, they drop in with just Tailwind.
Why build the expansion panels on <details>, and how do I make a single-open accordion?
They are built on the native <details> and <summary> elements, which gives you keyboard support, correct focus and the open/closed state announced to screen readers with no ARIA of your own. The thing the old checkbox-hack accordion could never do is behave as a true single-open accordion: give every <details> in the group the same name attribute and the browser closes the others automatically when one opens — like radio buttons, but semantic. Style the marker by hiding the default triangle and rotating your own chevron off the [open] state. Animating the height is the classic gotcha: you cannot transition from height: auto, so the demos animate a wrapper's grid-template-rows from 0fr to 1fr, which does interpolate smoothly. And do not nest another interactive control inside the <summary> that also toggles the panel, or the two click targets will fight each other.