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.
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:
- No new concepts or abstractions
- No setup or configuration
- No dependencies
- No build step
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
🔥
|
|
| Bootstrap |
|
| Material Web 2 |
|
| Zurb Foundation |
|
| React Bootstrap |
|
| MUI |
|
| Semantic UI |
|
| Microsoft Fabric |
|
| Shoelace |
|
| Material Web 3 |
|
To help visualize the impact of choosing small packages like Mdash, compare these three tech stacks:
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.
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 |
|
|||||||
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
<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 |
|
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:
<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:
- Reconsider how necessary the message is and avoid distracting users with a popup that isn't necessary
- Place the message and any related actions in close proximity to the relevant process
- Display this alert component in a prominent location
- For really important messages, use a dialog (see Alert vs. Dialog above)
- Trigger a system notification using the Notifications
API, e.g.
new Notification('Hello').
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
<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.
<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.
<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
<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 |
|
Ordinal number word for describing the content's importance. | |||||||
Guidelines
Card with Header & Footer
A custom card header and footer is possible using utility classes and/or custom styles. Like this:
Body content
<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:
<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.
Checkbox
Form element for selecting many options
Demo
<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 |
|
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:
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 |
|||||||
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
Accessibility
The aria-expanded attribute is managed automatically. Pressing spacebar will open/close details.
Dialog
Modal content container
Demo
<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:
<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
<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 |
|
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:
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
<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 | |||||||
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:
Validation
Use the Constraints API
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.
<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:
<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 |
|
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 |
|
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. | |||||||
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:
- ⌘ Command key
- ⇧ Shift key
- ⌥ Option/Alt key
- ⌃ Control key
- ⇪ Caps Lock key
Accessibility
There are no accessibility recommendations for the keyboard element.
Link
Used for navigating
Demo
Real link Fake link<a href="#link">Real link</a>
<span role="link">Fake link</span>
API
Tag
| Name | Type | Content | |||||||
|---|---|---|---|---|---|---|---|---|---|
a |
Native element | Any | |||||||
Attributes
| Name | Value | Description | |||||||
|---|---|---|---|---|---|---|---|---|---|
href required |
|
Sets the type of link. Be sure to use target fragments or the other URL schemes correctly to ensure optimal user experience. Every phone number, for example, should be wrapped in a link with the |
|||||||
disabled |
Boolean attribute | Disables the link. | |||||||
For span elements only:
role required |
link | Styles the element as a link. Use this instead of <a> when your use case is not a real link. |
|||||||
Guidelines
Security
When linking to websites you don't control, you should add the rel="noopener" attribute.
Open link in new tab
Quite often it is more convenient for users to open links in a new browser tab. This is especially true when a link goes to another website or when the link is on a page (or in a place on a page) that the user would have trouble getting back to. Simply add target="_blank".
Accessibility
Links should behave as links, i.e. they activate when clicked. Links that don't behave this way should not use the anchor tag and should instead use a span with role="link" attribute to avoid confusing screen-readers. Also, groups of links like menus, breadcrumbs, or primary navigation should be contained in a nav 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.
<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:
- Remove
tabindex="0"from<m-popover> - Replace
aria-controlswithpopovertargetand leave the value the same - Replace
slot="popover"with justpopover
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
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. |
|||||||
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
<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
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 |
|
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 |
|
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.
| Header |
|---|
| Cell |
| 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
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:
<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
<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:
<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:
<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.
<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.
<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.
<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. | |||||||
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.
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
- Foo
- Bar
- Baz
- Foo
- Bar
- Baz
- Foo
- Bar
- Baz
- Foo
- Bar
- 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 |
|
Defines a vertical list of content. Used for implementing designs like the master-detail pattern. See guideline below. | |||||||
For ul only:
type |
|
Sets the marker type | |||||||
For ol only:
type |
|
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:
- Prep Cook rice the day before 🍚
- Step 1 Preheat wok on high heat, add 2 tbsp. oil 🔥
- Step 2 Chop veggies and meat 🧄🥩
- Step 3 Add everything to wok and stir fry 🫕
- Step 4 Top with fresh basil and serve 🌿
Unordered, like notification preferences:
- SMS
- Push
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 for small print, side-comments, disclaimers, etc.This is a blockquote.
<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.
Custom Properties
The design tokens used internally and intended for use in custom styles
Demo
<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
<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.