Mdash

93 tokens
31 components
300+ utility classes
7 kilobytes

A design system that fully embraces web standards 🤗

Thank of it as HTML6.

It's so modern it feels old!

#UseThePlatform

Because UI should be fun 🥳

It's freedom, baby. Yeah!

Code depressed? Call 1-800-MDASH

"Nothing is faster than nothing."
-Me

The AWS bill was $90 last month!!

You can stop reading this.

How many of these are there?

15, including this one.

Does it loop?

No.

zero dependencies

tiny 6kb

responsive

WCAG

CDN hosted

works everywhere
Quick start

This is the web. Just link to Mdash and go!

<link href="https://unpkg.com/m-@4.1.0/dist/m-.woff2" rel="preload" as="font" crossorigin>
<link href="https://unpkg.com/m-@4.1.0/dist/m-.css" rel="stylesheet">

Introduction

What is Mdash?

Mdash is a design system built exclusively with modern web standards. It's impossibly small (just 6kb), works everywhere, and is incredibly easy to work with and customize.

It includes native and custom HTML elements, CSS Custom Properties, and utility classes.

What makes Mdash different?

Quite literally, nothing. Nothing is exactly what makes Mdash different:

It's HTML and CSS combined in a way you've never seen before, yet it's completely familiar.

Take a look around and compare Mdash's size and markup to see how nothing really is better.

Where did Mdash come from?

In 2016 my team at Expedia started building a design system using CSS-only Web Components, which at the time was the first any of us had ever seen that done before. This approach enabled us to support variations in tech stacks and architectures as the company was building more and more products. It was during this time the TAC CSS methodology was developed with Mdash being the first open-source implementation.


Compatible With Everything

Mdash can be used anywhere HTML is used because it is HTML. Here's an example of 14 different technologies all using the same Mdash component:

<!-- Vue -->
<m-alert v-bind:type="alert.type">{{ alert.message }}</m-alert>


<!-- Angular -->
<m-alert [type]="alert.type">{{ alert.message }}</m-alert>


<!-- React*, Preact, Solid -->
<m-alert type={alert.type}>{alert.message}</m-alert>


<!-- Riot -->
<m-alert type="{alert.type}">{alert.message}</m-alert>


<!-- Svelte -->
<m-alert bind:type="{alert.type}">{alert.message}</m-alert>


<!-- Handlebars -->
<m-alert type="{{alert.type}}">{{alert.message}}</m-alert>


<!-- Lit, Hyper -->
html`<m-alert type="${alert.type}">${alert.message}</m-alert>`


<!-- Blade -->
<m-alert type="{{ $alert→type }}">{{ $alert→message }}</m-alert>


<!-- EJS, ERB, Underscore, Lodash -->
<m-alert type="<%= alert.type %>"><%= alert.message %></m-alert>


<!-- Static HTML -->
<m-alert type="success">You're using Mdash!</m-alert>
*Framework compatibility with Custom Elements is tracked on custom-elements-everywhere.com and after eight long years React 19 is finally compatible.

Performance

Mdash is fast! It is by all practical measures instant. Its execution speed comes from leveraging native technology and avoiding abstractions as much as possible in order to minimize code and retain browser optimizations. When it comes to code, nothing is faster than nothing.

In addition to execution speed, pages load faster because Mdash is so much smaller than other UI libraries:

Mdash
🔥
6 kb
Bootstrap 71 kb
Material Web 2 79 kb
Zurb Foundation 88 kb
React Bootstrap 103 kb
MUI 146 kb
Semantic UI 174 kb
Microsoft Fabric 244 kb
Shoelace 294 kb
Material Web 3 353 kb
Note: Sizes are min+gzip for all runtime dependencies except icons and web fonts. In other words, this is the overhead before you write your first line of code.

To help visualize the impact of choosing small packages like Mdash, compare these three tech stacks:

Lit + Vaadin Router + Mdash = 21kb
Solid + Solid Router + Mdash = 23kb
React + React Router + MUI = 250kb

With the right tech stack your app can be painted and interactive before React and MUI have even finished downloading!

Installation

CDN

Mdash is designed for production use with a CDN. Copy and paste these two resources into the <head> section of your document and you're golden.

Icon Preload

<link href="https://unpkg.com/m-@4.1.0/dist/m-.woff2" rel="preload" as="font" crossorigin>

The preload option tells the browser to start downloading the icon font now instead of waiting for the stylesheet to be parsed. This must come before the stylesheet, but if you don't use icons you don't need this.

Stylesheet

<link href="https://unpkg.com/m-@4.1.0/dist/m-.css" rel="stylesheet">

This file includes everything: 70 tokens, 35 components, and 302 utility classes. All in a single 6kb stylesheet!

NPM and Internal Stylesheet

If the CDN is not an option or you want to use Mdash as an internal stylesheet you can install the Mdash package.

npm install m-

Built assets (the stylesheet and icon file above) are located in node_modules/m-/dist. The hyper optimization of critical CSS is possible because Mdash is so small.

<style>
  /* Depending on the server setup, you can do a CSS import */
  @import "node_modules/m-/dist/m-.css";
</style>

Browser support

Mdash works with the latest versions of all major browsers. Please file a bug if you see something not working as expected.

Components

Accordion

List of expandable details elements

Demo

Summary

Details

Summary

Details

Summary

Details

<m-accordion>
  <details>
    <summary>Summary 1</summary>
    <p>Details 1</p>
  </details>
  <details>
    <summary>Summary 2</summary>
    <p>Details 2</p>
  </details>
  <details>
    <summary>Summary 3</summary>
    <p>Details 3</p>
  </details>
</m-accordion>

API

Tag

Name Type Content
m-accordion Custom tag

<details> children only. See details component for information about its API.

Attributes

Name Type Content
icon Any icon Sets the toggle icon for all details in the accordion. See below for more info and recommended icons.

Guidelines

Custom Icon

Any icon is supported, but arrow down variations, expand down variations, and add variations are recommended. When choosing an icon, note that it will rotate when details is toggled.

With custom icon

Details

<m-accordion icon="add">
  <details>
    <summary>With custom icon</summary>
    <p>Details</p>
  </details>
</m-accordion>

Numbered Accordion

An accordion's details can be auto-numbered with CSS counter. Like this:

<style>
  #counterDemo {
    counter-reset: counterdemo;

    summary::before {
      counter-increment: counterdemo;
      content: counter(counterdemo) ". ";
    }
  }
</style>

<m-accordion id="counterDemo">
  <details>
    <summary>Do this first</summary>
    <p>Details</p>
  </details>
  <details>
    <summary>Next do this</summary>
    <p>Details</p>
  </details>
  <details>
    <summary>Finally do this</summary>
    <p>Details</p>
  </details>
</m-accordion>
Do this first

Details

Next do this

Details

Finally do this

Details

Open by default

Add the open attribute to a details to make it open by default. This will also trigger an initial toggle event. Multiple details can be set to open unless they're part of a named group (see Open one at a time below).

Open by default

Details

Closed by default

Details

<m-accordion>
  <details open>
    <summary>Open by default</summary>
    <p>Details</p>
  </details>
  <details>
    <summary>Closed by default</summary>
    <p>Details</p>
  </details>
</m-accordion>

Open one at a time

The default behavior is each details opens and closes independently, but by defining a group of named details you can limit that to one open at a time.

Note that a details group may result in a poor experience if users want to reference information in other open details but are prevented from doing so.

Summary group

Details

Summary group

Details

Summary group

Details

<m-accordion>
  <details name="groupdemo">
    <summary>Summary group</summary>
    <p>Details</p>
  </details>
  <details name="groupdemo">
    <summary>Summary group</summary>
    <p>Details</p>
  </details>
  <details name="groupdemo">
    <summary>Summary group</summary>
    <p>Details</p>
  </details>
</m-accordion>

Toggle Event Capturing

The toggle event does not bubble, so event delegation to the accordion requires the capture option. Like this:

const accordion = document.querySelector('m-accordion');

// Note the capture option
accordion.addEventListener('toggle', e => alert(`Details ${e.target.dataset.id} toggled`), {capture: true});
Toggle A

Details A

Toggle B

Details B

Accessibility

All accessibility recommendations apply to the details children.


Alert

Container for important messaging

Demo

Neutral message With icon and close option Informational message Positive message Warning message Error message
<m-alert>Neutral message</m-alert>
<m-alert icon="info">With icon and close option <button slot="close" title="Dismiss" aria-label="Dismiss"></button></m-alert>
<m-alert type="info">Informational message</m-alert>
<m-alert type="success">Positive message</m-alert>
<m-alert type="warn">Warning message</m-alert>
<m-alert type="error">Error message</m-alert>

API

Tag

Name Type Content
m-alert Custom Element Any

Slot

Name Element Content
close button None. See close slot for details.

Attributes

Name Value Description
type
  • info
  • success
  • warn
  • error
Sets the type of the message. Error messages should use error, cautionary messages should use warn, positive messages should use success, and important informational messages should use info.
icon Any icon Adds the specified icon. Suggested icons for each type are info for info, check_circle for success, warning for warn, and error for error.

Guidelines

Dismiss an Alert

Slot the close button and use whatever method is right for your app to bind a click handler that removes the alert:

Dismiss me
<script>
  function dismissAlert(e) {
    e.target.parentElement.remove();
  }
</script>

<m-alert>Dismiss me <button onclick="dismissAlert(event)" slot="close" title="Dismiss" aria-label="Dismiss"></button></m-alert>

It is recommended that error alerts do not use the close slot. The user should instead be guided toward correcting the error if possible.

Alert vs. Dialog

Some information is so important it justifies interrupting the user. In those cases consider displaying it in a dialog instead of an alert.

What about a Toast component?

Mdash does not have a toast component. There are several design options that serve the same purpose in a less distracting and more thoughtful way:

Accessibility

Warning and error alerts should also have role="alert" (see ARIA alert role). Add a title and aria-label to the close slot.


Badge

Display notification counts, values or text

Demo

text
<m-badge count="1"></m-badge>
<m-badge>text</m-badge>

API

Tag

Name Type Content
m-badge Custom tag Text or the count attribute

Attributes

Name Value Description
count Number A number to display. If set to zero or empty the badge will not be shown.

Guidelines

Empty badge hides itself

Badge can be used to display a count or text. If either is omitted the badge will not be shown.

Empty count:
Zero count:
Non-zero count:
Empty text:
Non-empty text: not empty
<m-badge count=""></m-badge>
<m-badge count="0"></m-badge>
<m-badge count="1"></m-badge>
<m-badge></m-badge>
<m-badge>not empty</m-badge>

Localization

If localization is needed, use Intl.NumberFormat.

English:
French:
<m-badge lang="en"></m-badge>
<m-badge lang="fr"></m-badge>

<script>
  document.querySelectorAll('m-badge[lang]').forEach(badge => {
    const count = Intl.NumberFormat(badge.lang).format(1000000);
    badge.setAttribute('count', count);
  })
</script>

Accessibility

Use aria-labelledby or aria-label to provide context for the count.


Card

Static container for elevating content

Demo

Primary content Secondary content
<m-card>Primary content</m-card>
<m-card ord="secondary">Secondary content</m-card>

API

Tag

Name Type Content
m-card Custom tag Any

Attributes

Name Value Description
ord
  • secondary
Ordinal number word for describing the content's importance.

Guidelines

A custom card header and footer is possible using utility classes and/or custom styles. Like this:

Header Title

Body content

Footer with button
<m-card class="pad-0">
  <header class="pad-4 bg-gray-1 brd-b font-med">Header Title</header>
  <p class="pad-4">Body content</p>
  <footer class="flex justify-content-between align-items-center pad-4 bg-gray-1 brd-t">
    <div>Footer with button</div>
    <button ord="primary" controlsize="sm">Save</button>
  </footer>
</m-card>

Custom cards

Card is highly customizable. Leverage HTML, utility classes, and custom styles to create unique and interactive cards. Like this:

Card 1 Card 2 Card 3
<script>
  function selectCard(e) {
    e.target.ariaPressed = e.target.ariaPressed === 'true' ? 'false' : 'true'
  }
</script>

<style>
  m-card[role=button] {
    &:not([aria-pressed=true]):hover {
      box-shadow: var(--m-shadow-1);
    }
    &[aria-pressed=true] {
      border-color: var(--m-color-primary-action);
    }
  }
</style>

<div class="flex gap-2">
  <m-card onclick="selectCard(event)" role="button" class="pointer">Card 1</m-card>
  <m-card onclick="selectCard(event)" role="button" class="pointer">Card 2</m-card>
  <m-card onclick="selectCard(event)" role="button" class="pointer">Card 3</m-card>
</div>

Accessibility

There are no accessibility recommendations for card.



Button

For triggering actions

Demo

Ordinality

Size

Disabled

Link as button

Link
<button ord="primary">Primary</button>
<button ord="secondary">Secondary</button>
<button ord="tertiary">Tertiary</button>

<button ord="primary" controlsize="sm">Small</button>
<button ord="primary" controlsize="md">Medium</button>
<button ord="primary" controlsize="lg">Large</button>

<button ord="primary" disabled>Disabled</button>

<a role="button" ord="primary" href="/">Link</a>

API

Tag

Name Type Display Content
button Native element inline-flex Any

Attributes

Name Value Description
ord required
  • primary
  • secondary
  • tertiary
Ordinal number word for describing the button's precedence.
controlsize
  • sm
  • md default
  • lg

Changes the size of the button.

type
  • submit

Use submit when the button submits a form. Avoid <input type="submit">. See Submit button for more info

disabled Boolean attribute Disables the button. Link buttons also need tabindex="-1" to prevent activation by keyboard or assistive tech.
aria-pressed
  • true
  • false
  • mixed
Defines the button as a toggle button and sets its pressed state. The ariaPressed property works as well. See MDN for details.

See Button Group below to learn how to create a group of toggle buttons.

For <a> only:

role required button Styles the link as a button. Use this instead of <button> when your use case needs a real link.

Guidelines

Link Buttons

If the action navigates to a new url or downloads a file, use a link, and if the link needs to look like a button then add the role="button" attribute. It's important that an actual link is used so the user can cmd + click or copy and share the link. If the action does not navigate to a new URL or download, then use a regular button.

Navigation links

Back Continue
<a href="/back" role="button" ord="secondary">Back</a>
<a href="/continue" role="button" ord="primary">Continue</a>

Download link

<a href="/img/homer.webp" download role="button" ord="primary" title="Download" aria-label="Download">
  <m-icon name="download"></m-icon>
</a>

Regular button

<button ord="primary">Save</button>

Icon Button

Put an icon in a button and add title and aria-label attributes:

<button ord="primary" title="User menu" aria-label="User menu">
  <m-icon name="person"></m-icon>
</button>

Button comes styled with gap so icons and text look good when paired together.

<button ord="primary">
  <m-icon name="person"></m-icon>Left Icon
</button>
<button ord="primary">
  Right Icon<m-icon name="person"></m-icon>
</button>

Custom Button Styles

Mdash leaves the button element untouched so you can freely use them without competing with Mdash styles (Mdash buttons require the ord attribute). Use the all-unset utility class to remove user-agent styles from button and customize as desired. For example:

<button>User Agent Button</button>
<button class="all-unset">Unstyled Button</button>
<button class="all-unset pad-1 bg-gray-7 txt-white txt-center txt-upper txt-space pointer" style="width: 280px">Custom Button</button>

Accessibility

Icon-only buttons should have title and aria-label attributes. Disabled link buttons should have tabindex="-1" to prevent activation.


Button Group

Group of toggle buttons

Demo

<div role="group">
  <button ord="secondary" aria-pressed="true">One</button>
  <button ord="secondary" aria-pressed="mixed">Two</button>
  <button ord="secondary" aria-pressed="false">Three</button>
  <button ord="secondary">Four</button>
</div>

API

Tag

Name Type Display Content
div Native element inline-grid Secondary buttons

Attributes

Name Value Description
role required group Defines the button group
ord required
  • secondary
  • tertiary
Sets the button variation. Primary buttons are not supported.
aria-pressed
  • true
  • false
  • mixed
Defines a toggle button and its pressed state. The ariaPressed property works as well. See MDN for details.

Guidelines

Managing Pressed State

Your app manages the button's aria-pressed value. You can set the attribute or ariaPressed property. One way to do this is with event delegation:

Allow more than one to be pressed

<script>
  function toggleFilter(e) {
    e.target.ariaPressed = e.target.ariaPressed === 'true' ? 'false' : 'true'
  }
</script>

<div onclick="toggleFilter(event)" role="group">
  <button ord="secondary" value="ready">Ready</button>
  <button ord="secondary" value="complete">Complete</button>
  <button ord="secondary" value="overdue">Overdue</button>
</div>

Allow only one to be pressed

<script>
  function switchFilter(e) {
    [...e.currentTarget.children].forEach(button => button.ariaPressed = String(button === e.target))
  }
</script>

<div onclick="switchFilter(event)" role="group">
  <button ord="secondary" value="ready">Ready</button>
  <button ord="secondary" value="complete">Complete</button>
  <button ord="secondary" value="overdue">Overdue</button>
</div>

Getting the Pressed Button Value

Using button value is one way to know which button was toggled and what action to take.

<script>
  function alertFilter(e) {
    e.target.ariaPressed = e.target.ariaPressed === 'true' ? 'false' : 'true';
    alert(`${e.target.value} is ${e.target.ariaPressed}`);
  }
</script>

<div onclick="alertFilter(event)" role="group">
  <button ord="secondary" value="ready">Ready</button>
  <button ord="secondary" value="complete">Complete</button>
  <button ord="secondary" value="overdue">Overdue</button>
</div>

Layout

Button group is a CSS grid. Use grid-template-columns to lay out the buttons as needed.

Content Width (default)

.filters-demo-content-width {
  grid-template-columns: repeat(3, min-content);
}

Equal Width

.filters-demo-equal-width {
  grid-template-columns: repeat(3, 1fr);
}

Equal Full Width

.filters-demo-full-width {
  grid-template-columns: repeat(3, 1fr);
  width: 100%;
}

Group Size

A group can be one or more buttons. Groups of one still require a container with the group role.

<div role="group">
  <button ord="secondary">1</button>
</div>

<div role="group">
  <button ord="secondary">1</button>
  <button ord="secondary">2</button>
  <button ord="secondary">3</button>
  <button ord="secondary">4</button>
  <button ord="secondary">5</button>
  <button ord="secondary">6</button>
  <button ord="secondary">7</button>
</div>

Icons

Like regular buttons, group buttons can contain an icon.

<div role="group">
  <button ord="secondary"><m-icon name="format_bold" title="Bold" aria-label="Bold"></m-icon></button>
  <button ord="secondary"><m-icon name="format_italic" title="Italic" aria-label="Italic"></m-icon></button>
  <button ord="secondary"><m-icon name="format_underlined" title="Underline" aria-label="Underline"></m-icon></button>
</div>

Accessibility

The pressed button(s) must have aria-pressed set to true or mixed. All regular button accessibility also applies to group buttons.


Checkbox

Form element for selecting many options

Demo

Languages
<fieldset>
  <legend>Languages</legend>
  <input id="html" type="checkbox" name="speed" value="html" checked>
  <label for="html">HTML</label>
  <input id="css" type="checkbox" name="speed" value="css">
  <label for="css">CSS</label>
  <input id="js" type="checkbox" name="speed" value="js">
  <label for="js">JavaScript</label>
</fieldset>

API

Tag

Name Type Content
input Native element None

Attributes

Name Value Description
name required String All checkboxes in a group use the same name.

Guidelines

Radio, checkbox, or select?

There's many uses cases where a single checkbox works very well and other use cases where 50 checkboxes makes sense too. If only one of the options can be selected, use radio or select.

Accessibility

Like other input elements, be sure to use for and id.


Container

Primary container element for page layout

Demo

Container is centered in its parent, has responsive padding, and its content will be contained based on its maxwidth.

<m-container>
  <p>Container is centered in its parent, has responsive padding, and its content will be contained based on its <code>maxwidth</code>.</p>
</m-container>

API

Tag

Name Type Content
m-container Custom tag Any

Attributes

Name Value Description
maxwidth
  • lg default
  • md
  • sm
  • none
Sets the max width of the container, which includes some padding. "lg" grows up to 1536px, "md" starts at 800px and shrinks down, and "sm" starts at 576px and shrinks down.
Read more on maxwidth for details.

Guidelines

More on maxwidth

By default Container will grow up to 1,536 pixels wide, which is intended to make full use of a screen up to the equivalent of a 16" MacBook Pro. You can remove this limit with none.

In cases where there isn't a full page of content or maybe a more focused layout is desired, md is often the right width. On-boarding flows or a promotional page are good examples where medium would be useful. In some cases a very narrow and focused layout is needed, like a log in form. Use sm for these.

In all cases Container includes padding and centers itself in the viewport.

Multiple containers

A page can have multiple containers. This is most common when two components should share the same container characteristics, like max width and centered in the viewport, but need to have different backgrounds or other design elements that prevent sharing a single container. A site navigation is a good example:

LOGO

And the rest of the page's content is here in another container.

Note how this section and the site nav's content are aligned.

Other layout elements

In addition to Mdash's container element, HTML offers a number of semantic elements for layout. From W3Schools:

  • <header> Defines a header for a document or a section
  • <nav> Defines a container for navigation links
  • <section> Defines a section in a document
  • <article> Defines an independent self-contained article
  • <aside> Defines content aside from the content (like a sidebar)
  • <footer> Defines a footer for a document or a section

Accessibility

There are no accessibility recommendations for Container.


Details

Expandable container for progressive disclosure

Demo

Click to see details

The deets.

<details>
  <summary>Click to see details</summary>
  <p>The deets.</p>
</details>

API

Details is a native element. More information is available on MDN.

Tags

Name Type Content
details Native element <summary> as first child and any other content after that
summary Native element Any, but should be a “summary, caption, or legend for the rest of the contents"

Attributes

Name Value Description
open Boolean attribute When present, the details are shown (everything after summary). When removed details are hidden. If you want Details open by default, add this attribute.
name Text Defines a group of named details, which limits the group to only one open at a time. See Accordion for an example.

Events

Name Detail Description
toggle None

Fires after details was opened or closed. If e.target.open is true, then it was just opened, otherwise it was closed.

Guidelines

Is the content even necessary?

Although details and accordion are great tools for progressive disclosure, you should consider if the hidden content is even necessary or if it could be removed altogether.

Clickable Elements Inside Summary

Use event.preventDefault() to stop other interactive elements in summary from toggling the details:

Show report history
Delete Export
The report history...

Accessibility

The aria-expanded attribute is managed automatically. Pressing spacebar will open/close details.


Dialog

Modal content container

Demo

Put anything you want in here.

(press Esc to close)

<script>
  function openDialog(e) {
    e.target.nextElementSibling.showModal()
  }
</script>
<button onclick="openDialog(event)" ord="primary">Open Dialog</button>
<dialog>
  <p>Put anything you want in here.</p>
  <p>(press <kbd>Esc</kbd> to close)</p>
</dialog>

API

Tag

Name Type Content
dialog Native element Any. The first element with autofocus will receive focus when the dialog is opened.

Slot

Name Element Content
close button None. See Close Slot for details.

Attributes

Name Value Description
open Boolean attribute Will open the Dialog when added or close when removed. If you want the dialog displayed "modelessly", you have to call the show method.

Events

Name Detail Description
close None MDN says: "Fired when the dialog is closed, whether with the escape key, the HTMLDialogElement.close() method, or via submitting a form within the dialog with method="dialog"."
cancel None MDN says: "Fired when the user dismisses the current open dialog with the escape key."

Methods

Signature Description
close([returnValue]) MDN says: "Closes the dialog. An optional DOMString may be passed as an argument, updating the returnValue of the the dialog." Also note that the Dialog and its contents are still present in the DOM (e.g. forms still have user-entered values, so reset it if that's what your use case requires).
show() MDN says: "Displays the dialog modelessly, i.e. still allowing interaction with content outside of the dialog."
showModal() MDN says: "Displays the dialog as a modal, over the top of any other dialogs that might be present. Interaction outside the dialog is blocked."

Guidelines

Close Slot

Dialog elements do not come with a button to close themselves, so Mdash defines a close slot. Add <button slot="close" type="remove"> as the first child and Mdash will style it for you. Your application implements the click handler (e.g. calling the dialog's close method or removing its open attribute). Note that this is required only if a dialog doesn't implement any other buttons for closing. Here's a basic example:

The button[slot=close] element is styled and positioned for you. Your app handles the click event and closes the dialog.

<script>
  function openDialogWithCloseSlot(e) {
    const dialog = e.target.nextElementSibling;
    dialog.showModal();
  }

  function closeDialogWithCloseSlot(e) {
    const dialog = e.target.parent;
    dialog.close();
  }
</script>
<button onclick="openDialogWithCloseSlot(event)" ord="primary">Open Dialog</button>
<dialog>
  <button onclick="closeDialogWithCloseSlot(event)" slot="close" type="remove" title="Close" aria-label="Close"></button>
  <p>The <code>button[slot=close]</code> element is styled and positioned for you. Your app handles the click event and closes the dialog.</p>
</dialog>

Forms and DOM State

The state of the content is controlled by your application. Dialog does not change the state of its children other than moving them into a containing div on init. Forms and all other elements will initialize the way they are provided by the application and will continue to remain untouched even when the Dialog is closed. For example, if a Dialog is used to present a login form the application should remove the Dialog completely or reset the form after successful authentication. If left alone the Dialog and the login form inside it will contain the user's credentials. It's your content; you have to manage it.

Accessibility

The necessary ARIA attributes are added automatically; however, if your Dialog "contains an alert message" you should set role="alertdialog".


Dot

Status indicator

Demo

Unknown Information Success Warning Error
<m-dot>Unknown</m-dot>
<m-dot type="info">Information</m-dot>
<m-dot type="success">Success</m-dot>
<m-dot type="warn">Warning</m-dot>
<m-dot type="error">Error</m-dot>

API

Tag

Name Type Content
m-dot Custom tag Text (optional)

Attributes

Name Value Description
type
  • info
  • success
  • warn
  • error
Sets the type of the indicator and has the same meaning as Alert's type.

Guidelines

Dot text

In most cases Dot should include text, but sometimes it's okay to have a Dot without text (see Accessibility). An Accordion of system summaries works as a good example:

Databases

All good!

API

All good!

Notification service

Increase in avgerage response time.

Unknown type

A Dot with an unknown type is useful when a generic dot is needed. Another use case is when your app wants to display a dot, but its type is still being determined. A progress can help communicate in these situations:

Marcus is offline
Marcus is joining
Marcus is online

Accessibility

When there is no text use aria-label, e.g. <m-dot type="success" aria-label="Systems okay">.


Form elements

Basic elements used when creating forms

Demo

Must be at least 8 characters
<form>
  <fieldset>
    <label>Email</label>
    <input type="email">
  </fieldset>
  <fieldset>
    <label>Password</label>
    <input type="password">
    <small>Must be at least 8 characters</small>
  </fieldset>
  <fieldset>
    <label>Address</label>
    <input placeholder="Street">
    <input placeholder="Zip" autocomplete="postal-code">
  </fieldset>
  <button type="submit" ord="primary">Save</button>
</form>

API

Tags

Name Type Content
form Native element Any
fieldset Native element Any
label Native element Text
small Native element Text for "side-comments" associated with an input. Read more
Note: Form controls are documented separately. See Input, Checkbox, Radio, Select, and Textarea.

Attributes

Nothing special to call out. See MDN: form attributes for the full list.

Events

Name Detail Description
submit None Fires when the form is submitted, which happens when the user clicks the submit button or hits enter.
input None Fires when the value of a text-like input or text area changes.
change None Fires when a value is committed, like selecting from a list, picking a file or date, checking a box, or blurring away from a text input.

Guidelines

Submit button vs. input button

Mdash recommends forms use <button type="submit"> instead of <input type="submit">. This is more semantic and provides greater control of the button style and content. For example, you can't put an icon or a progress inside a submit input.

Why <fieldset>?

Rather than inventing new tags or using boilerplate divs and classes, Mdash leverages the native fieldset tag. Although it's historically uncommon to put just one input inside a fieldset, it is perfectly valid markup. Single or multiple input combinations of various types work perfectly well and results in the most clean and uniform form code possible:

<form>
  <fieldset>
    <label>Name</label>
    <input type="text">
  </fieldset>
  <fieldset>
    <label>Phone</label>
    <input type="tel">
  </fieldset>
  <fieldset>
    <label>Address</label>
    <input type="text" placeholder="Street">
    <input type="text" placeholder="City">
    <select>
      <option>State...</option>
      <option value="AL">Alabama</option>
      <option value="WY">Wyoming</option>
    </select>
  </fieldset>
</form>

Why <small>?

Understand that small's semantics have to do with its type of content and not font size or other visual characteristic. The HTML spec uses examples such as "disclaimers, caveats, legal restrictions, or copyrights", which is inline with this use case. Some examples:

183/200 characters left
Must include a number, special character, and be at least 8 characters long

Validation

Use the Constraints API

Must be a valid email address

Accessibility

See Input, Checkbox, Radio, Select, and Textarea for their respective accessibility recommendations.


Icons

Symbols used to enhance communication

Demo

<m-icon name="flag"></m-icon>
<m-icon name="flag" fill></m-icon>

API

Tag

Name Type Content
m-icon Custom tag None

Attributes

Name Value Description
name required See available icons below Sets the icon's symbol
fill Boolean attribute Uses filled version

Guidelines

Available Icons

Mdash uses Material Symbols Outlined. On that page, getting an icon name is easier if you click the icon, then select the Android tab, then copy the name.

Pair With Text

Icons should be used to enhance content not replace it. Users can misinterpret icons, so strive to accompany an icon with a label or other relevant content in close proximity.

John Doe
Profile Settings
Sign Out
<m-menu>
  <span slot="trigger" role="link"><m-icon name="person"></m-icon> John Doe</span>
  <div slot="items">
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
    <hr class="mar-t-2 mar-b-3">
    <a href="/logout">Log out</a>
  </div>
</m-menu>

If an icon is on its own, use the title attribute to explain what it symbolizes, e.g. "Current location", or its action, e.g. "Reload this page". Hover over the user icon for a clue:

Profile Settings
Log out
<m-menu>
  <span slot="trigger" role="link"><m-icon name="person" title="Open user menu"></m-icon></span>
  <div slot="items">
    <a href="/profile">Profile</a>
    <a href="/settings">Settings</a>
    <hr class="mar-t-2 mar-b-3">
    <a href="/logout">Log out</a>
  </div>
</m-menu>

Why not SVG?

Implementing SVG icons requires a relatively expensive abstraction and because there is no visual or accessibility difference between SVG and glyph icons, that abstraction would be all cost and no benefit. Mdash icons require no JavaScript and so they save kilobytes and have maximum compatibility.

Accessibility

Because icons should be paired with some text (see above) they should reference that text with aria-labelledby attribute. In cases where there is no label, use aria-label and title.


Input

Form element for receiving user input

Demo

API

Tag

Name Type Content
input Native element None

Attributes

Name Value Description
type
  • text default
  • email
  • password
  • tel
  • number
  • file
  • more...
Sets the expected type of value. Take care to set email, tel, password, and number inputs based on use case to ensure optimal user experience.

See checkbox, radio, and range for details on those specific types.

disabled Boolean attribute Disables the input so it can't be changed or interacted with. It will be skipped when tabbing and its value will not be submitted with the form.
readonly Boolean attribute Makes the input read-only, which means the user can't change its value, but can still tab to it and copy the text. Its value will be submitted with the form.
invalid Boolean attribute Highlights input as having an invalid value. When the input is invalid it should have a small element explaining how to correct it. Validation is owned by your application not Mdash.
placeholder String Displays a message inside the input. Ideal for showing an expected format or sample value.
autofocus Boolean attribute If present, the browser will bring focus to this element on page load. Excellent for log in or search pages or whenever the first interaction is likely to type something.
autocomplete
Credit Card
cc-name
cc-number
cc-csc
cc-exp-month
cc-exp-year
Name
name
given-name
additional-name
family-name
Address
street-address
address-level1 (state or province)
address-level2 (city)
postal-code
country
Phone & Email
tel
email
These are required in order for the browser to autofill the form. Not all values are shown here! See the complete list and learn more at MDN: HTML autocomplete attribute.
Note: Not all input attributes are listed here. See MDN: Input element - Attributes for the full list.

Guidelines

Use the right type

It's very important to take the time to understand what values a user can enter in a given input and to use the right type for those values. If, for example, the input is for a membership number then using type="number" will present an easier to use numbers-only keyboard on most touch devices. The same goes for email and phone numbers. These UX optimizations are important to users and are virtually free to implement, so take a moment to ensure you're using the right input type.

Accessibility

Labels should use the for attribute to reference the id of its corresponding input. Inputs should use the right type (see above), autofocus, and autocomplete to improve their usability.


Keyboard

Represents text from an input device

Demo

Press ⇧⌘T to close window

Ctrl + N
<p>Press <kbd>⇧⌘T</kbd> to close window</p>
<kbd>
  <kbd>Ctrl</kbd> + <kbd>N</kbd>
</kbd>

API

Tag

Name Type Content
kbd Native element (documentation) Text or other kbd elements

Guidelines

Formatting

Mdash doesn't define a format for keyboard shortcuts because there are many popular ways to do it. Shortcuts are also platform-specific and so it is recommended to follow the format used by the user's OS. There are several ways to detect OS using the Navigator object.

Windows seems to use a format that includes + characters to indicate a combination of key presses, like Ctrl + C.

macOS seems to use a sequence of characters to indicate a combination of key presses, like ⌘C. Symbols are used for modifier keys (see below).

Be careful how you show the + character. Users can misinterpret it as representing a required key.

macOS Modifier Key Symbols

To create macOS keyboard shortcuts, copy these symbols:

Accessibility

There are no accessibility recommendations for the keyboard element.



Progress

Indicates a processing state

Demo

<progress title="Downloading" aria-label="Downloading"></progress>
<progress value="0.25"></progress>

API

Tag

Name Type Content
progress Native element None

Attributes

Name Value Description
value Number Sets the completed progress

Guidelines

Loader text

In most cases the loader should have its own text or be in close proximity to a related message, like a button label. In some cases it's appropriate to have a loader on it's own without a message (in those cases see Accessibility).

Progress size

The default size works for most use cases, but you can enlarge or shrink progress with the text size utility classes or set its own font size:

Nested progress styles

Progress will inherit font properties so that when it's nested in other elements it matches the expected styles.

Inside a tag Inside a success alert
<m-tag>
  <progress></progress>
  <span>Inside a tag</span>
</m-tag>
<button ord="primary" disabled>
  <progress></progress>
  <span>Inside a disabled button</span>
</button>
<m-alert type="success" class="gap-2">
  <progress></progress>
  <span>Inside a success alert</span>
</m-alert>

Accessibility



Popover

Small overlay containers toggled by the user

Demo

Build anything in the popover.

<m-popover tabindex="0">
  <button ord="primary" aria-controls="my-popover">Open</button>
  <div id="my-popover" slot="popover">
    <p class="pad-2 width-max">Build anything in the popover.</p>
  </div>
</m-popover>

API

Tag

Name Type Content
m-popover Custom tag A button element and the popover slot
button is recommended Native element Any

Slots

Name Element Content
popover div Any

Guidelines

Popover API

The popover attributes and API are going to be awesome. They are still just a bit too new to adopt, but Mdash will once browser usage is at an acceptable level. When that time comes the migration will be simple:

Inspecting & Debugging

Popover will close after losing focus, which makes inspecting its content in developer tools annoying. Select the <m-popover> element in the DOM, then go to the styles tools and check the :focus-within pseudo class. This will keep the popover open.

Content

Mdash only shows, hides, and minimally styles the popover. The target and popover content can anything:

<m-popover tabindex="0">
  <button ord="primary" aria-controls="filters-popover">Pick Foods</button>
  <div id="filters-popover" slot="popover" style="width: 200px">
    <form id="popover-form" class="pad-2">
      <fieldset>
        <label for="banana"><input id="banana" type="checkbox" name="food" value="banana"> Banana</label>
        <label for="carrot"><input id="carrot" type="checkbox" name="food" value="carrot"> Carrot</label>
        <label for="grapes"><input id="grapes" type="checkbox" name="food" value="grapes"> Grapes</label>
        <label for="orange"><input id="orange" type="checkbox" name="food" value="orange"> Orange</label>
      </fieldset>
    </form>
    <footer class="flex justify-content-between">
      <button ord="tertiary" scale="sm" form="popover-form" type="reset">Reset</button>
      <button ord="tertiary" scale="sm" form="popover-form" type="submit">Apply</button>
    </footer>
  </div>
</m-popover>

Accessibility

Popover requires aria-controls on the button. Focus will open the popover and focusable content will receive focus as expected. Blurring popover will close it.


Radio input

Form element for picking one of many choices

Demo

Speed

See more examples

API

Tag

Name Type Content
input Native element None

Attributes

Name Value Description
name required String All radios in a group use the same name.

Guidelines

Radio, checkbox, or select?

If only one of the options can be selected, don't use checkbox. There are no hard rules, but generally radio is best for 2-4 choices and select (or Autocomplete) is better for more.

Accessibility

Like other input elements, be sure to use for and id.


Range Slider

Form element for fine-grained values

Demo

API

Tags

Name Type Content
input[type="range"] Native element None
output Native element The value of range

Attributes

Name Value Description
min Number Minimum value, default is 0
max Number Maximum value, default is 100
step Number Sets the granularity of values, default is 1
orient coming soon vertical Displays the range vertically

Events

Name Detail Description
change None Fires after the value is committed.
input None Fires as the value changes.

Guidelines

Displaying range values

Mdash leaves the design for displaying range values open for customization, but with the one requirement to use the standard <output> element to contain the value.

Here's two common patterns to help get you started:

<fieldset>
  <label>Price</label>
  <input name="rangeDemo1" type="range" min="0" max="1000" value="308">
  <output name="outputDemo1"></output>
</fieldset>

<script>
  const range1  = document.querySelector('[name=rangeDemo1]');
  const output1 = document.querySelector('[name=outputDemo1]');

  function updateOutput2() {
    output1.textContent = range1.value;
  }

  updateOutput2();
  range1.addEventListener('input', updateOutput2);
</script>
<fieldset>
  <label>Price</label>
  <input name="rangeDemo2" type="range" min="0" max="1000" value="700">
  <output name="outputDemo2" class="pos-absolute bg-white shadow pos-b-0 mar-b-8 pad-1 txt-xs brd-radius-3"></output>
</fieldset>

<script>
  const range2  = document.querySelector('[name=rangeDemo2]');
  const output2 = document.querySelector('[name=outputDemo2]');

  function updateOutput2() {
    const val = Number((range2.value - range2.min) * 100 / (range2.max - range2.min));
    const pos = 9 - (val * 0.2); // 9 is half the thumb width
    output2.style.left = `calc(${val}% + ${pos}px)`;
    output2.textContent = range2.value;
  }

  updateOutput2();
  range2.addEventListener('input', updateOutput2);
</script>

<style>
  input[type=range][name=rangeDemo2] + output {
      transform: translate(-50%);
  }
</style>

Accessibility

Use for and id as usual, but also use for on output elements. MDN has more details ARIA: Using the slider role.


Select

Drop-down list of choices

Demo

API

Tag

Name Type Content
select Native element <option> or <optgroup> children
optgroup Native element <option> children
option Native element Text

Attributes

Name Value Description
label String Displayed as the <optgroup> heading.
value String Used as the value of the element.
Note: Not all input attributes are listed here. See MDN: Select element - Attributes for the full list.

Events

Name Detail Description
change None Fired after an option was selected.

Guidelines

Accessibility

Label should use the for attribute to reference the id of its corresponding select.


Separator

An element that divides sections of content

Demo

Content

Content
Content

Content

Content
<div class="flex flex-col gap-2">
  <div>Content</div>
  <hr>
  <div>Content</div>
</div>
<div class="flex gap-2">
  <div>Content</div>
  <hr aria-orientation="vertical">
  <div>Content</div>
  <hr aria-orientation="vertical">
  <div>Content</div>
</div>

API

Tags

Name Type Content
hr Native element None

Attributes

Name Value Description
aria-orientation vertical

Displays the separator vertically. Vertical separators must have a Flexbox or Grid parent (use the flex or grid utility class).

Guidelines

Where to use

Separators are useful in menus, toolbars, and written content.

Avoid using too many separators. Font size, font weight, whitespace, color contrast and other common visual characteristics are often sufficient to create the necessary visual distinction between elements.

Accessibility

MDN says, "The first rule of ARIA is if a native HTML element or attribute has the semantics and behavior you require, use it instead of re-purposing an element and adding ARIA." The native hr element has an implicit role="separator", so there is no need for a proprietary implementation. There is also no need to set aria-orientation="horizontal" since it's the default.


Switch

A control for toggling binary values like on/off

Demo

<input type="checkbox" role="switch" checked>

API

Tag

Name Type Content
input[type=checkbox] Native element None

Attributes

Name Value Description
role required switch Defines the checkbox as a switch
checked Boolean attribute Standard checkbox attribute
disabled Boolean attribute Standard checkbox attribute

Events

Name Detail Description
change None Switch is a checkbox, so you can listen for the standard change event.

Guidelines

Standalone or form

A switch can be used on its own or more commonly as a list of options in a form, like this:

Network settings

Currently unavailable

Accessibility

MDN says, "The first rule of ARIA is if a native HTML element or attribute has the semantics and behavior you require, use it instead of re-purposing an element and adding ARIA." The native checkbox is quite suitable for a custom switch component, so that in conjunction with the switch role should be used instead of a proprietary implementation.


Table

Used for tabular data

Demo

Product
Socks $9.99
Shorts $19.99
Sweater $29.99
Shoes $49.99
<table striped>
  <thead>
  <tr>
    <th>Product</th>
    <th aria-sort="ascending">
      <button>Price</button>
    </th>
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>Socks</td>
    <td>$9.99</td>
  </tr>
  <tr>
    <td>Shorts</td>
    <td>$19.99</td>
  </tr>
  <tr>
    <td>Sweater</td>
    <td>$29.99</td>
  </tr>
  <tr>
    <td>Shoes</td>
    <td>$49.99</td>
  </tr>
  </tbody>
</table>

API

Tags

Name Type Content
table Native element <thead> (optional) and <tbody>
thead Native element <tr> with <th> children
tbody Native element <tr> with <td> children, first child can be <th> for row headings
tfoot Native element <tr> with <td> children
caption Native element If used, must be first child

Attributes

For <table> only:

Name Value Description
layout fixed Table cells are sized automatically by the browser. This doesn't always produce the desired layout, so use this option with colspan to control the layout. Note this option does not mean the table size won't change at all - it'll still respond nicely to screen size.
striped Boolean attribute Creates a visual distinction between alternate rows.

For <td> and <th> only:

colspan Number Defines how many columns the cell should span.

For <th> only:

aria-sort
  • ascending
  • descending
  • other
  • none
Sets that column as sorted. Your application is responsible for the sorting logic. The ariaSort property is also available. See example below.

For <caption> only:

side
  • start default
  • end
Positions the caption at the start or end of the table by setting the caption-side property. The values start and end were chosen instead of top and bottom to map to future support for logical values. Regardless of side, captions must be the first child of the table.

Guidelines

Sorting Rows

Applying a sort to table data and updating rows is handled by your app. Even without a framework this is very straight forward.

<table id="products" layout="fixed">
  <thead>
    <tr>
      <th>
        <button onclick="sortProducts(event)" value="name">Product</button>
      </th>
      <th>
        <button onclick="sortProducts(event)" value="price">Price</button>
      </th>
    </tr>
  </thead>
  <tbody></tbody>
</table>

<script>
  const table = document.all.products;
  const sort = { order: 'ascending', value: 'price' };
  const products = [
    { name: 'Socks', price: 9.99 },
    { name: 'Shorts', price: 19.99 },
    { name: 'Sweater', price: 29.99 },
    { name: 'Shoes', price: 49.99 },
  ];

  function sortProducts(e) {
    sort.order = e.target.parentElement.ariaSort === 'ascending' ? 'descending' : 'ascending'
    sort.value = e.target.value;

    if (sort.value === 'name') {
      products.sort((a, b) => {
        const x = sort.order === 'ascending' ? a : b;
        const y = sort.order !== 'ascending' ? a : b;
        return x.name.localeCompare(y.name, undefined, { sensitivity: 'base' })
      });
    }

    if (sort.value === 'price') {
      products.sort((a, b) => {
        const x = sort.order === 'ascending' ? a : b;
        const y = sort.order !== 'ascending' ? a : b;
        return x.price - y.price
      });
    }

    render(e.target.parentElement);
  }

  function render(column) {
    // Update sorted column and rows.
    [...table.tHead.rows[0].cells].forEach((col) => col.ariaSort = 'none');
    column.ariaSort = sort.order;
    table.tBodies[0].innerHTML = products.reduce((rows, p) => `${rows}<tr><td>${p.name}</td><td>$${p.price}</td></tr>`, '');
  }

  render(table.tHead.rows[0].cells[1]);
</script>

Caption side

Captions must be the first child but can still be positioned on either end of the table. Refer to MDN: caption for more info.

Caption start (default)
Header
Cell

Caption end
Header
Cell
<table>
  <caption side="start">Caption start (default)</caption>
  <thead>
  <tr>
    <th>Header</th>
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>Cell</td>
  </tr>
  </tbody>
</table>

<table>
  <caption side="end">Caption end</caption>
  <thead>
  <tr>
    <th>Header</th>
  </tr>
  </thead>
  <tbody>
  <tr>
    <td>Cell</td>
  </tr>
  </tbody>
</table>

Accessibility

The sorted column must have the appropriate aria-sort value.


Tabs

Master-detail pattern for navigating content

Demo

Link

See selecting tabs to learn how to select a tab

API

Tags

Name Type Content
m-tabs Custom tag button or a children

Attributes

Name Value Description

For m-tabs only:

scrollable Boolean attribute Makes the tab list horizontally scrollable. In some cases, like flex grow, overflow doesn't happen and so the parent element will need an explicit width (try the width-full utility class).

For tab only:

aria-selected true or false Set the selected tab to true. See Selecting tabs for examples.
disabled Boolean attribute Disables the tab.

Guidelines

Selecting tabs

Select the tab

To select a tab, set aria-selected="true". This is handled by your app and there are several approaches you could take to manage this, but event delegation comparing the tabs to the clicked tab is probably the simplest without a framework.

<script>
  function selectTab(e) {
    const tabs = e.currentTarget.querySelectorAll('[role=tab]');
    tabs.forEach(tab => tab.ariaSelected = tab === e.target);
  }
</script>

<m-tabs onclick="selectTab(event)" role="tablist">
  <button role="tab">A</button>
  <button role="tab">B</button>
</m-tabs>

This only selects the tab. Selecting the corresponding panel is explained next.

Show the panel

Mdash assumes nothing about what happens when a tab is selected. This allows for many different solutions, like toggling the panels' hidden attribute, or directly changing the DOM, dynamically rendering a component, navigating to a new page, or something else. Here's a simple using the hidden attribute:

Tab A panel
<script>
  function switchTab(e) {
    const selectedTab = e.target.closest('[role=tab]');
    const tabs = e.currentTarget.querySelectorAll('[role=tab]');
    const panels = document.querySelectorAll('[role=tabpanel]');

    // Select the tab and show  its panel.
    tabs.forEach(tab => tab.ariaSelected = tab.id === selectedTab.id);
    panels.forEach(panel => panel.hidden = panel.dataset.tabId !== selectedTab.id);
  }
</script>

<m-tabs onclick="switchTab(event)" role="tablist">
  <button id="tabA" role="tab" aria-selected="true">A</button>
  <button id="tabB" role="tab">B</button>
</m-tabs>
<div role="tabpanel" data-tab-id="tabA">Tab A panel</div>
<div role="tabpanel" data-tab-id="tabB" hidden>Tab B panel</div>

Here's the same thing with a more declarative approach using Riot.js (or other framework):

<app>
  <m-tabs onclick="{switchTab}" role="tablist">
    <button id="tabA" aria-selected="{state.tab === 'tabA'}">A</button>
    <button id="tabB" aria-selected="{state.tab === 'tabB'}">B</button>
  </m-tabs>
  <div if="{state.tab === 'tabA'}" role="tabpanel">Tab A panel</div>
  <div if="{state.tab === 'tabB'}" role="tabpanel">Tab B panel</div>

  <script>
    export default {
      state: {
        tab: 'tabA'
      }

      switchTab(e) {
        this.update({tab: e.target.id});
      }
    }
  </script>
</app>

Accessibility

All of the necessary ARIA attributes are here in the example below:

<m-tabs role="tablist">
  <button id="tabA" role="tab" aria-controls="panelA">A</button>
  <button id="tabB" role="tab" aria-controls="panelB">B</button>
</m-tabs>
<div id="panelA" role="tabpanel" aria-labelledby="tabA">Tab A panel</div>
<div id="panelB" role="tabpanel" aria-labelledby="tabB">Tab B panel</div>

Tag

Used for keywords, labeling, and filters

Demo

non-smoking pool breakfast available
<m-tag>non-smoking</m-tag>
<m-tag><m-icon name="pool"></m-icon>pool</m-tag>
<m-tag>breakfast available<button slot="close" title="Remove" aria-label="Remove"></button></m-tag>

API

Tag

Name Type Content
m-tag Custom tag Text, icon, or link and optional close slot for removable tags

Slot

Name Element Content
close button None. See removable tags below.

Custom Properties

Name Description
--color Sets the base color of the tag. The text, background, and optional button colors are automatically derived from this value. See Color below.

Guidelines

Removable Tags

To remove a tag, slot the close button and use whatever method is right for your app to bind a click handler that removes the tag. For example:

Text
<script>
  function removeTag(e) {
    e.target.parentElement.remove();
  }
</script>

<m-tag>Text<button onclick="removeTag(event)" slot="close" title="Remove" aria-label="Remove"></button></m-tag>

It can be helpful to add an id or data-* attribute with some identifying data to know which tag was removed. Like this:

Bill Jill Phil
<script>
  function removeUser(e) {
    const tag = e.currentTarget.parentElement;

    // Remove the user and tag.
    alert(`Remove user ${tag.dataset.userId}...`);
    tag.remove();
  }
</script>

<m-tag data-user-id="1">Bill<button onclick="removeUser(event)" slot="close"></button></m-tag>
<m-tag data-user-id="2">Jill<button onclick="removeUser(event)" slot="close"></button></m-tag>
<m-tag data-user-id="3">Phil<button onclick="removeUser(event)" slot="close"></button></m-tag>

Icons

Simply include an icon on the left and/or right side of the label, or include just an icon.

hot shower hot shower
<m-tag><m-icon name="shower"></m-icon>hot shower</m-tag>
<m-tag>hot shower<m-icon name="shower"></m-icon></m-tag>
<m-tag><m-icon name="shower" title="Hot shower" aria-label="Hot shower"></m-icon></m-tag>

Color

Tag's color theme can be changed with its --color custom property.

Blue 3 tag Red 3 tag Green 3 tag Orange 3 tag Custom color tag
<m-tag style="--color: var(--m-color-blue-3)">Blue 3 tag<button slot="close"></button></m-tag>
<m-tag style="--color: var(--m-color-red-3)">Red 3 tag<button slot="close"></button></m-tag>
<m-tag style="--color: var(--m-color-green-3)">Green 3 tag<button slot="close"></button></m-tag>
<m-tag style="--color: var(--m-color-orange-3)">Orange 3 tag<button slot="close"></button></m-tag>
<m-tag style="--color: #984E28">Custom color tag<button slot="close"></button></m-tag>

Accessibility

Use title and aria-label when the tag only contains an icon and on the close button.

Text
<m-tag title="Breakfast available" aria-label="Breakfast available"><m-icon name="local_cafe"></m-icon></m-tag>
<m-tag>Text<button slot="close" title="Remove" aria-label="Remove"></button></m-tag>

Text area

Multi-line input

Demo

<fieldset>
  <label>Label</label>
  <textarea></textarea>
</fieldset>

API

Tag

Name Type Content
textarea Native element Text

Attributes

Name Value Description
rows Number Sets a starting height that will accommodate the given number of rows of text. This does not prevent additional lines from being typed and the user can also resize it.
maxlength Number Limits the number of characters allowed in the text area.
minlength Number Sets a minimum required number of characters in the text area.
Note: Not all text area attributes are listed here. See MDN: Text area element - Attributes for the full list.

Guidelines

Sensible row size

The default is two rows, i.e. two lines of text, but you should set a number that makes the most sense for your use case. For example, a chat app makes sense to take the default of two, but a customer review form should likely have 5-10. Note that text areas are resizable so the user can extend it further if needed, just take care to set a sensible default.

Accessibility

There are no accessibility recommendations for text area.

Typography

Code

For displaying text as code

Demo

This is inline code.

// Multi-line code
const user = {
  name: 'Stan'
};

console.log(user.name);
<p>This is <code>inline code</code>.</p>

<pre><code>// Multi-line code
const user = {
  name: 'Stan'
};

console.log(user.name);</code></pre>

API

Tags

Name Type Content
code Native element Text
pre Native element One <code> child with text

Guidelines

Security

Because these elements' content is code, take extra care to ensure you're preventing cross-site scripting. There's nothing that makes these elements more or less secure than any other, but in this case you know for sure you're dumping code into the DOM, so be careful!

Accessibility

You should use aria-label and/or aria-description to explain the code.

await fetch(url, init);
<code aria-description="JavaScript code that demonstrates the fetch API.">await fetch(url, init);</code>

Headings

Headings elements

Demo

Heading 1

Heading 2

Heading 3

Heading 4

Heading 5
Heading 6
<h1>Heading 1</h1>
<h2>Heading 2</h2>
<h3>Heading 3</h3>
<h4>Heading 4</h4>
<h5>Heading 5</h5>
<h6>Heading 6</h6>

API

Tags

Name Type Content
h1-h6 Native element Any
hgroup Native element One heading and one or more paragraphs

Guidelines

SEO

Using heading elements improves SEO and accessibility.

Accessibility

Strive to use the heading elements. If your situation doesn't allow for that, there is role="heading" and aria-level for defining the same.


Lists

Lists of data or content

Demo

  1. Foo
  2. Bar
  3. Baz
  1. Foo
  2. Bar
  3. Baz
<ul>
  <li>Foo</li>
  <li>Bar</li>
  <li>Baz</li>
</ul>
<ol>
  <li>Foo</li>
  <li>Bar</li>
  <li>Baz</li>
</ol>
<ul type="none">
  <li>Foo</li>
  <li>Bar</li>
  <li>Baz</li>
</ul>
<ol type="content">
  <li class="pad-2">Foo</li>
  <li class="pad-2">Bar</li>
  <li class="pad-2">Baz</li>
</ol>

API

Tags

Name Type Content
ul Native element <li> children
ol Native element <li> children
dl Native element <dt> and <dd> children

Attributes

Name Value Description
type
  • content
Defines a vertical list of content. Used for implementing designs like the master-detail pattern. See guideline below.

For ul only:

type
  • disc default
  • square
  • circle
  • none
Sets the marker type

For ol only:

type
  • 1 default
  • A
  • a
  • I
  • i
Sets the marker type
reversed Boolean attribute Reverses the order of items
start Number Sets the starting number

Guidelines

Lists are for lists

List elements should be used for real lists. It's tempting to overuse them for anything that repeats, so take time to think about some better alternatives to list elements that should be used instead. For example, one alternative is the nav element when building a navigation bar:

Content list type

The content list type is meant for building a vertical list of content.

Semantics and accessibility

To retain list semantics and accessibility, Mdash leverages the standard tags for content lists. Use the correct tag for the content, i.e. if order matters use ol, otherwise ul.

Ordered, like steps in a recipe:

  1. Prep Cook rice the day before 🍚
  2. Step 1 Preheat wok on high heat, add 2 tbsp. oil 🔥
  3. Step 2 Chop veggies and meat 🧄🥩
  4. Step 3 Add everything to wok and stir fry 🫕
  5. Step 4 Top with fresh basil and serve 🌿

Unordered, like notification preferences:

Nesting

Content list is designed to work wherever you need it. It can be used on its own or, for example, nested in a dialog, menu, or a card like this:

  • one
  • two
  • three
<m-card class="pad-0">
  <ul type="content">
    <li class="pad-2">one</li>
    <li class="pad-2">two</li>
    <li class="pad-2">three</li>
  </ul>
</m-card>

Accessibility

Use the correct list parent elements for your use case, i.e. ol when the list order is meaningful and ul when it isn't.


Text

Paragraph and other text-based elements

Demo

This is a paragraph with some important text and some emphasized text. Text styling should be done using the text utility classes.

This is a blockquote.

This is for small print, side-comments, disclaimers, etc.
<p>This is a paragraph with some <strong>important text</strong> and some <em>emphasized text</em>. Text styling should be done using the <a href="#utility-classes-text">text utility classes</a>.</p>
<blockquote>
  <p>This is a blockquote.</p>
</blockquote>
<small>This is for small print, side-comments, disclaimers, etc.</small>

API

Tags

Name Type Content
p Native element Any
strong Native element Any, but indicates that its contents have strong importance, seriousness, or urgency.
em Native element Any, but meant to indicate stress emphasis
blockquote Native element p element for the actual quotation
small Native element Any, but intended to represent "side-comments...small print, like copyright and legal text, independent of its styled presentation."

Guidelines

Characters per line

It is recommend that lines of text be no more than 65-75 characters wide, including spaces (Designing With Type, James Craig). This makes for a more comfortable reading experience. Usability studies dating back to the 1970's show readers experience fatigue with text running longer than this.

To demonstrate, the paragraph below has no width limit while that same paragraph above maxes out at 75ch using the txt-maxlength utility class:

It is recommended that lines of text be no more than 65-75 characters, including spaces (Designing With Type, James Craig). This makes for a more comfortable reading experience. Usability studies dating back to the 1970's show readers experience fatigue with text running longer than this.

SEO

Using these elements correctly will improve SEO and accessibility.

Accessibility

The smallest font size that Mdash has is 13px, which is the smallest recommended by WCAG 2.1. Any smaller can be illegible for some users.

Styles

Custom Properties

The design tokens used internally and intended for use in custom styles

Demo

These background and text colors use custom properties
<style>
  #customPropertiesExample {
    color: var(--m-color-gray-1);
    background-color: var(--m-color-gray-7);
  }
</style>

<div id="customPropertiesExample" class="pad-8">These background and text colors use custom properties</div>

API

Colors

All color values are in the perceptually uniform color space to provide visually appealing colors while still achieving accessible contrast.

Name Example Description
--m-color-red-[1|2|3]
Used to denote something has or will go wrong.
--m-color-orange-[1|2|3]
Used for bringing attention to something that may not be desirable.
--m-color-blue-[1|2|3]
Used to highlight primary actions.
--m-color-green-[1|2|3]
Used to communicate something desirable has or will happen.
--m-color-gray-[7|8|9]
Used for text and high-contrast backgrounds.
--m-color-gray-[1|2|3|4|5|6]
Grayscale used for backgrounds, borders, and disabled state.
--m-color-primary-action
Used for links and other primary action elements.
--m-color-disabled-[bg|fg]
Disabled background and foreground.
--m-color-border
Default border color.

Spacing

Used for padding, margin, position, and gap.

Name Value Description
--m-space-[1-14] 4px - 56px in increments of 4px The padding and margin utility classes are available and map 1:1 with these values. There is no --m-space-0, just use the value 0.

Element styles

All elements use these values for their relevant styles.

Name Value Description
--m-radius-[1-5] 2px - 10px in increments of 2px Used for border radius.
--m-radius-full Creates a fully round radius, e.g. a circle Used for border radius.

Breakpoints

Although custom properties are unfortunately not usable in media queries, these breakpoint values are useful elsewhere.

The large breakpoint and above are considered desktop-sized

Name Value Description
--m-breakpoint-sm 576px The portrait width of an iPhone X.
--m-breakpoint-md 768px The portrait width of an iPad.
--m-breakpoint-lg 992px The landscape width of an iPad.

Other

Name Value Description
--m-max-content-width 1320px The max width used by Container. Wrapping your content in a m-container is more likely what you need, but you can use this if necessary.
--m-min-input-height 34px The min height used for form elements. Reference this if you'd like your element to match the height of inputs.

Guidelines

Do not hard code values

Facebook famously had 548 unique colors hard-coded 6,498 times across all their stylesheets. Custom properties are a modern CSS feature that helps you avoid such a mess by defining reusable values. Use custom properties instead of hard coding their values. Like this:

/* Do */
#example {
  background-color: var(--m-color-gray-2);
}

/* Don't */
#example {
  background-color: #dedede;
}

See MDN to learn more about custom properties and how to use them.

Accessibility

The colors and spacing meet accessibility requirements.


Utility Classes

300+ CSS property shortcuts for building custom layouts and designs

Demo

Homer
Homer J. Simpson
Safety Inspector
Chunkylover53@aol.com
555-7334
<div class="flex align-items-center gap-4 width-fit bg-white brd-radius-3 brd pad-4">
  <img src="img/homer.webp" alt="Homer" class="brd-radius-full obj-fit-cover" width="91" height="91">
  <div>
    <div class="txt-md txt-nowrap">Homer J. Simpson</div>
    <div class="txt-sm txt-gray-6 mar-b-2">Safety Inspector</div>
    <a href="mailto:Chunkylover53@aol.com" class="font-light txt-xs">Chunkylover53@aol.com</a>
    <br>
    <a href="tel:+15555556543" class="font-light txt-xs">555-7334</a>
  </div>
</div>

API

Utility class names generally match the naming of their property with most property names abbreviated to three letters, e.g. padding is abbreviated as pad, position as pos. Some are two letters, like background shortened to just bg, and some are not abbreviated at all.

Display

Name Description
grid Shortcut for display: grid
inline-grid Shortcut for display: inline-grid
flex Shortcut for display: flex
inline-flex Shortcut for display: inline-flex
block Shortcut for display: block
inline-block Shortcut for display: inline-block
inline Shortcut for display: inline
hidden Shortcut for display: none. Consider using the HTML hidden attribute instead.

Flexbox

Name Description
flex Shortcut for display: flex
inline-flex Shortcut for display: inline-flex
flex-grow-0 Shortcut for flex-grow: 0
flex-grow-1 Shortcut for flex-grow: 1
flex-shrink-0 Shortcut for flex-shrink: 0
flex-shrink-1 Shortcut for flex-shrink: 1
flex-wrap Shortcut for flex-wrap: wrap
flex-col Shortcut for flex-direction: column
flex-row Shortcut for flex-direction: row
flex-basis-content Shortcut for flex-basis: content
justify-content-start Shortcut for justify-content: flex-start
justify-content-center Shortcut for justify-content: center
justify-content-between Shortcut for justify-content: between
justify-content-evenly Shortcut for justify-content: evenly
justify-content-around Shortcut for justify-content: around
justify-content-end Shortcut for justify-content: flex-end
align-items-center Shortcut for align-items: center
align-items-start Shortcut for align-items: flex-start
align-items-end Shortcut for align-items: flex-end
align-self-stretch Shortcut for align-self: stretch
align-self-center Shortcut for align-self: center
align-self-start Shortcut for align-self: flex-start
align-self-end Shortcut for align-self: flex-end
place-content-center Shortcut for place-content: center

Grid

Name Description
grid Shortcut for display: grid
inline-grid Shortcut for display: inline-grid
grid-flow-col Shortcut for grid-auto-flow: column
grid-flow-row Shortcut for grid-auto-flow: row
grid-flow-dense Shortcut for grid-auto-flow: dense
grid-auto-cols-min Shortcut for grid-auto-columns: min-content
grid-auto-cols-max Shortcut for grid-auto-columns: max-content
grid-template-cols-fit Shortcut for grid-template-columns: repeat(auto-fit, minmax(min(375px, 100%), 1fr))
grid-template-cols-fill Shortcut for grid-template-columns: repeat(auto-fill, minmax(min(400px, 100%), 1fr))
place-content-center Shortcut for place-content: center
place-items-center Shortcut for place-items: center

Spacing

Name Description
pad-[0-14] Sets padding on all sides to the specified size. Sizes map to the space custom props.
pad-[t|r|b|l]-[0-14] Sets padding on the specified side (top, right, bottom, or left) in the specified size. Sizes map to the space custom props.
mar-auto Sets margin on all sides to auto.
mar-[t|r|b|l]-auto Sets margin on the specified side (top, right, bottom, or left) to auto.
mar-[0-14] Sets margin on all sides to the specified size. Sizes map to the space custom props.
mar-[t|r|b|l]-[0-14] Sets margin on the specified side (top, right, bottom, or left) in the specified size. Sizes map to the space custom props.
gap-[0-14] Sets gap to the specified size. Sizes map to the space custom props.

Position

Name Description
pos-absolute Shortcut for position: absolute
pos-fixed Shortcut for position: fixed
pos-relative Shortcut for position: relative
pos-static Shortcut for position: static
pos-sticky Shortcut for position: sticky
pos-[t|r|b|l]-0 Shortcut for setting top, right, bottom, or left to zero

Font

Name Description
font-bold Shortcut for font-weight: 700
font-med Shortcut for font-weight: 500
font-reg Shortcut for font-weight: 400
font-light Shortcut for font-weight: 300
font-italic Shortcut for font-style: italic
font-mono Shortcut for font-family: monospace
font-normal Shortcut for font-style: normal

Text

Name Description
txt-left Shortcut for text-align: left
txt-right Shortcut for text-align: right
txt-center Shortcut for text-align: center
txt-justify Shortcut for text-align: justify
txt-lower Shortcut for text-transform: lowercase
txt-upper Shortcut for text-transform: uppercase
txt-caps Shortcut for text-transform: capitalize
txt-space Shortcut for letter-spacing: 2px
txt-truncate Truncates overflowing text and shows an ellipsis.
txt-nowrap Shortcut for white-space: nowrap. Good for forcing inline elements to stretch wide enough to accommodate the text.
txt-break-all Shortcut for word-break: break-all. Good for permitting characters to wrap when there's not enough room.
txt-break-word Shortcut for word-break: break-word. Good for permitting words to wrap when there's not enough room.
txt-maxlength Shortcut for max-width: 75ch. Good for limiting text line length to improve readability. See Readability: The Optimal Line Length
txt-noselect Prevents the user from selecting the text of this element.
txt-[xs|sm|md|lg|xl|xxl] Sets font-size.
txt-[color-name] Sets text color. The class names map to the color custom props, e.g. txt-red-dark => --m-color-red-dark.
txt-[info|success|warn|error] Sets text color to the alert type.

Border

Name Description
brd Adds a border.
brd-[t|r|b|l] Adds a border to the specified side.
brd-none Removes border from all sides.
brd-radius-[1-5|full] Sets border radius. Sizes map to the radius custom props. Use full to create a fully round radius, e.g. a circle.
brd-[sm|md] Sets border width.
brd-dashed Sets border style to dashed. Good for placeholders and drop targets.

Background

Name Description
bg-clip-text Clips background to match foreground text.
bg-cover Background will be scaled to fill entire area of element. The aspect ratio is preserved, but the image may be cropped.
bg-contain Background will be scaled to fit size of element. The aspect ratio is preserved and the image will not be cropped.
bg-[color-name] Sets background color. The class names map to the color custom props, e.g. bg-red-dark => --m-color-red-dark.

Other

Name Description
pointer Shortcut for cursor: pointer. Useful when you want an element to seem clickable. Buttons and links already have pointer set.
height-full Shortcut for height: 100%
height-half Shortcut for height: 50%
height-min-0 Shortcut for min-height: 0
width-full Shortcut for width: 100%
width-half Shortcut for width: 50%
width-fit Shortcut for width: fit-content
width-min-0 Shortcut for min-width: 0
box-sizing-border Shortcut for box-sizing: border-box
box-sizing-content Shortcut for box-sizing: content-box
overflow-auto Shortcut for overflow: auto
overflow-hidden Shortcut for overflow: hidden
overflow-clip Shortcut for overflow: clip (good for rounded corners)
vis-hidden Shortcut for visibility: hidden
shadow Adds default shadow
content-vis-auto Shortcut for content-visibility: auto. Use on large sections of content not visible to the user after page load. The browser skips rendering and layout until needed thereby reducing the initial page rendering time.
left Shortcut for float: left
right Shortcut for float: right
clear Shortcut for clear: both
all-unset Shortcut for all: unset

Guidelines

Reduce CSS maintenance

You can drastically cut down and even eliminate your CSS maintenance by leveraging these classes as much as possible.

Create and use templates

The demo above with all those classes is fine on its own or even 2-3 copies sitting next to each other, but beyond that you'll want a way to reuse chunks of HTML.

Reuse is possible with the <template> element or template literals (see below). If standard tools aren't enough, templating partials or static components are another low-code option. Avoid the temptation to turn every chunk of HTML into a framework-dependent component.

export function UserCardTemplate(imgUrl, name, title, email, phone) {
  return `
    <div class="inline-flex align-items-center bg-gray-1 brd-radius-3 brd pad-4">
    <img src="${imgUrl}" height="78" class="brd-radius-full mar-r-4">
    <div>
      <div class="txt-md txt-nowrap">${name}</div>
      <div class="txt-sm txt-gray-6 mar-b-2">${title}</div>
      <a href="mailto:${email}" class="font-light txt-xs">${email}</a>
      <br>
      <a href="tel:+1${phone.replaceAll('-', '')}" class="font-light txt-xs">${phone}</a>
    </div>
    </div>
  `
}

const homerCard = UserCardTemplate('/img/homer.webp', 'Homer J. Simpson', 'Safety Inspector', 'Chunkylover53@aol.com', '555-7334');

Accessibility

There are no accessibility recommendations for these classes.