Hennepin County pattern and component library
Getting started
How we design and develop our digital interfaces reflects a commitment to efficiently connect people with the information and services they need.
Audience and purpose
Frontend developers should use the pattern and component library as a guide. It contains design elements for county websites and applications.
The guide will help developers meet the county's standards and ensure Section 508 compliance.
Developers may need to customize the code examples to fit their technology stack.
Maintenance of pattern and components
The User Experience Community of Practice reviews and gives input to the standards.
Staff can ask for changes or exceptions to the standards:
- Complete a request form request form (must have network access).
- You must attend a meeting of the User Experience Community of Practice to make your case for the change. This creates a democratic and transparent process for input and decision-making.
- The pattern and component library team makes the final decision about requested changes.
Accessibility
Everyone should be able to easily interact with the county online. This includes people with visual, hearing, motor, and cognitive disabilities. These disabilities could be permanent, temporary or situational.
Our digital accessibility standards align with the county's accessibility web standards.
Requirements for all our websites, applications, vendor technology, and third-party tools:
- Follow the Web Content Accessibility Guidelines (WCAG)
- Meet success criteria for level A and AA
- Meet success criteria for level AAA where possible and relevant
- Be validated for accessibility with in-person testing and automated testing with tools like WAVE
To understand and follow web accessibility laws and best practices we use:
Structural markup
Semantic structure is the bedrock of accessible markup. Screen readers rely HTML elements and attributes to convey information to blind users. Semantic markup allows assistive technologies to convey information through the accessibility API.
Interactive components
Design these for touch, mouse, and keyboard users. Use relevant WAI-ARIA to make interactive components understandable and useable by assistive technologies.
You may need to use extra ARIA attributes and JavaScript depending on the component function.
Color contrast
Some color combinations in our default palette may lead to insufficient color contrast. We suggest testing colors. We also suggest you change default colors to ensure adequate color contrast ratios. The WCAG 2.1 text color contrast ratio is 4.5:1. The WCAG 2.1 non-text color contrast ratio is 3:1.
Focus indicator
Any element that receives focus should have a visual focus indicator. The focus indicator must be a solid outline. The default color is inherited from the font color. If that does not meet acceptable contrast ratios, use black. If black does not meet contrast ratios, use another brand color that does. The width should be about 4px, but the width can be adjusted to accommodate large or small components. Set the default outline-offset to zero for padded elements like buttons and inputs. Adjust the offset to allow space around other elements that need it like inline text and image links. For example, tab through the interactive controls below:
*:focus {
outline:4px solid; /* adjust the width depending on the element size */
outline-offset:0; /* adjust the offset depending on the element */
}
Visually hidden content
Sometimes important visual cues are provided to sighted users, like red text. These cues are picked up by assistive technology only by using visually hidden content. For example:
Danger: This action is not reversible!
<p class="text-danger">
<span class="visually-hidden">Danger: </span>
This action is not reversible!
</p>
For visually hidden controls like "skip" links make sure the control becomes visible once focused (for sighted keyboard users).
Reduced motion
You must include support for the prefers-reduced-motion
media feature. CSS
transitions should be disabled. Meaningful animations such as spinners should be slowed down.
@media (prefers-reduced-motion: reduce) {
*, *::before, *::after {
animation: none !important;
transition: none !important;
}
}
document.addEventListener('click', event => {
if ( window.matchMedia('(prefers-reduced-motion: reduce)').matches ) {
executeTaskWithoutMotion();
} else {
executeTaskWithMotion();
}
});
Additional resources
Alerts
Alerts are used to convey special messages distinct from regular content.
<div class="alert" role="alert">
A simple alert—check it out!
<button type="button" class="close">
Close <span aria-hidden="true">×</span>
</button>
</div>
.alert {
position: relative;
padding: 0.5rem 0.5rem;
margin-bottom: 1rem;
display: flex;
justify-content: space-between;
align-items: center;
}
.alert .close {
padding: 0.75rem 1.25rem;
color: inherit;
font-size: 1rem;
display: flex;
align-items: center;
}
.alert .close:hover,
.alert .close:focus {
color: inherit;
}
.alert .close span[aria-hidden] {
font-size: 1.5rem;
margin-top: -0.2rem;
}
Use of color: Using color to add meaning provides only a visual cue. Assistive technologies will not pick up this visual cue. To make the color meaningful to assistive technologies, either use visual or visually hidden text. See the section on visually hidden content.
Accessible Role: The role="alert"
is used for important and usually
time-sensitive messages.
For example:
- An invalid value was entered into a form field
- The user's login session is about to expire
- The connection to the server was lost, local changes will not be saved
Use the alert role sparingly because of its intrusive nature. Less urgent dynamic changes can use
methods like aria-live="polite"
or other live region roles.
Use of dismissible "close" button: Close buttons are optional on the alert
component. If you choose to use one, it must have ample clickable spacing around it (at least 40px
by 40px). The close button must be a button
element, or make use of the button role
(role="button"
). The close button should include the visible word "Close." If that is
not possible, it must be present through other
means, like visually hidden text.
Branding
Refer to the Hennepin County brand guide for more information regarding logos, colors, typography, and more.
Logos
Hennepin County's logo and wordmark communicate who we are immediately. Using each correctly is a key part of maintaining a professional and coherent brand.
Hennepin County "H"
Hennepin County's primary logo is the trusted Hennepin "H." This bold and highly recognizable symbol acts as a seal of approval on all Hennepin County branded materials. It must be used in all publications and promotional materials for county programs and services.
Hennepin County wordmark
The Hennepin County wordmark is a clear and uniform visual expression of the county name. It is designed to augment the Hennepin "H" in specific circumstances, but should never be used in place of it.
Colors
Our color system reflects the diversity and vibrancy of Hennepin County. We're not a one-dimensional region and our organization is not a single shade of blue.
Breadcrumbs
Use breadcrumbs to show the current page's location within a navigational hierarchy.
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="#">Home</a></li>
<li class="breadcrumb-item"><a href="#">Library</a></li>
<li class="breadcrumb-item active" aria-current="page">Data</li>
</ol>
</nav>
.breadcrumb {
display: flex;
flex-wrap: wrap;
padding: 0.75rem 1rem;
margin-bottom: 1rem;
list-style: none;
border-radius: 0;
}
.breadcrumb-item {
display: flex;
}
.breadcrumb-item + .breadcrumb-item {
padding-left: 0.5rem;
}
.breadcrumb-item + .breadcrumb-item::before {
display: inline-block;
padding-right: 0.5rem;
color: #6c757d;
content: ">";
}
.breadcrumb-item + .breadcrumb-item:hover::before {
text-decoration: underline;
}
.breadcrumb-item + .breadcrumb-item:hover::before {
text-decoration: none;
}
.breadcrumb-item.active {
color: #6c757d;
}
Since breadcrumbs provide a navigation, it's a good idea to add a meaningful label such as
aria-label="breadcrumb"
to describe the type of navigation provided in the
<nav>
element, as well as applying an aria-current="page"
to the
last item of the set to indicate that it represents the current page.
For more information, see the WAI-ARIA Authoring Practices for the breadcrumb pattern.
Buttons
Use buttons to let a user take an action. An example is when asking a user to create a new profile, or to register.
Buttons have a primary and secondary action. A primary action is the primary option or task for the user. The secondary action is available, but used less often.
Language used for button labels should reflect what action the user will take. We recommend using verbs or adverbs and limiting words to two or three. If you need more content to describe the button action, consider using a tooltip for clarity.
Hennepin County branded buttons uses the following colors:
-
Default -
- background-color: #113c66
- color: #ffffff
-
:hover -
- background-color: #44c8f5
- color: #000000
-
Default -
- background-color: #ffffff
- color: #113c66
-
:hover -
- background-color: #44c8f5
- color: #000000
-
Primary -
Secondary -
- opacity: 0.65
Button options
Default
<button class="btn btn-primary">Primary</button>
<button class="btn btn-outline-primary">Secondary</button>
Disabled state
<button class="btn btn-primary" disabled>Primary disabled</button>
<button class="btn btn-outline-primary" disabled>Secondary disabled</button>
Button sizing
Large
<button class="btn btn-primary btn-lg">Primary</button>
<button class="btn btn-outline-primary btn-lg">Secondary</button>
Small
<button class="btn btn-primary btn-sm">Primary</button>
<button class="btn btn-outline-primary btn-sm">Secondary</button>
.btn {
display: inline-block;
font-weight: 400;
color: #000;
text-align: center;
vertical-align: middle;
-webkit-user-select: none;
-moz-user-select: none;
-ms-user-select: none;
user-select: none;
background-color: transparent;
border: 1px solid transparent;
padding: 0.375rem 0.75rem;
font-size: 1rem;
line-height: 1.5;
border-radius: 0;
transition: color 0.15s ease-in-out, background-color 0.15s ease-in-out, border-color 0.15s ease-in-out, box-shadow
0.15s ease-in-out;
}
.btn.disabled,
.btn:disabled {
opacity: 0.65;
}
.btn-lg {
padding: 0.5rem 1rem;
font-size: 1.25rem;
line-height: 1.5;
border-radius: 0;
}
.btn-sm {
padding: 0.25rem 0.5rem;
font-size: 0.875rem;
line-height: 1.5;
border-radius: 0;
}
@media (prefers-reduced-motion: reduce) {
.btn {
transition: none;
}
}
.btn:hover {
color: #000;
text-decoration: none;
}
.btn:focus,
.btn.focus {
outline-color: #000;
}
.btn:not(:disabled):not(.disabled) {
cursor: pointer;
}
a.btn.disabled {
pointer-events: none;
}
.btn-primary {
color: #fff;
background-color: #113c66;
border-color: #113c66;
}
.btn-outline-primary {
color: #113c66;
border-color: #113c66;
}
.btn-primary:hover,
.btn-primary:focus,
.btn-primary.focus,
.btn-outline-primary:hover,
.btn-outline-primary:focus,
.btn-outline-primary.focus {
background-color: #44c8f5;
border-color: #000;
color: #000;
}
It is important to use semantic HTML for buttons. Not using semantic HTML for buttons risks making the buttons less accessible.
If not using a <button>
tag consider:
- Having to define event handlers for keydown and click/tap events
- (If needed) adding a class and
aria-label
to signify the button is disabled - Adding in
role="button"
- Ensuring the element is focusable for all assistive technology
- The need for
aria
attributes and other complexities that comes with it, as outlined by the W3C
Buttons vs. links
Assistive technologies read buttons and links differently. Both are focusable when using a keyboard. Buttons trigger with a space or enter key, but a link only triggers with an enter key.
Buttons tell the user they can take an action. Links offer the user a way to get to a new page related to the context of their current page.
digitala11y offered two points that can serve as a reminder of when to use a button over a link:
- Use buttons when the user- action causes a change in either back-end or the front-end of the website. For example, submitting a form, opening a pop-up or a modal or a panel on the same page.
- Use links when the user- action doesn't affect the website at all. In this, the users are readers or spectators of the site. For example, to navigate to the next page or an external source after viewing the content of the page.
Collapse
Collapsible panels are a great way to toggle the visibility of content. Hennepin County often refers to these as Drawers.
Some placeholder content for the first collapse component of this multi-collapse example. This panel is hidden by default but revealed when the user activates the relevant trigger.
- Lorem ipsum dolor sit amet
- Consectetur adipiscing elit
- Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
- Ut enim ad minim veniam
- Quis nostrud exercitation ullamco laboris
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Some placeholder content for the second collapse component of this multi-collapse example. This panel is hidden by default but revealed when the user activates the relevant trigger.
- Lorem ipsum dolor sit amet
- Consectetur adipiscing elit
- Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
- Ut enim ad minim veniam
- Quis nostrud exercitation ullamco laboris
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Some placeholder content for the third collapse component of this multi-collapse example. This panel is hidden by default but revealed when the user activates the relevant trigger.
- Lorem ipsum dolor sit amet
- Consectetur adipiscing elit
- Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
- Ut enim ad minim veniam
- Quis nostrud exercitation ullamco laboris
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Some placeholder content for the fourth collapse component of this multi-collapse example. This panel is hidden by default but revealed when the user activates the relevant trigger.
- Lorem ipsum dolor sit amet
- Consectetur adipiscing elit
- Sed do eiusmod tempor incididunt ut labore et dolore magna aliqua
- Ut enim ad minim veniam
- Quis nostrud exercitation ullamco laboris
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
<button class="collapsed" data-target="#multiCollapseExample1" role="button" aria-expanded="false" aria-controls="multiCollapseExample1">
Toggle first element
</button>
<div class="collapse" id="multiCollapseExample1">
...
</div>
.collapse:not(.show) {
display: none;
}
.collapsing {
position: relative;
height: 0;
overflow: hidden;
transition: height 0.35s ease;
}
@media (prefers-reduced-motion: reduce) {
.collapsing {
transition: none;
}
}
.drawer > .btn,
.drawer > .btn.collapsed:hover,
.drawer > .btn.collapsed:focus {
background: #44c8f5;
}
.drawer > .btn::before {
content: '';
background: url('minus.svg') no-repeat;
height: 15px;
width: 15px;
display: inline-block;
margin-right: 1rem;
}
.drawer > .btn.collapsed {
background: #dee2e6;
}
.drawer > .btn.collapsed::before {
background-image: url('plus.svg');
}
$('.drawer > .btn').on('click', function() {
...
$(this).removeClass('collapsed').attr('aria-expanded', 'true');
...
});
Reduced motion: The animation effect of this component should depend on the
prefers-reduced-motion
media
query. For more information see the accessibility
section.
Accessible Role: If the control element's HTML element is not a
<button>
, you must add the attribute role="button"
to the element.
Accessible state: This attribute conveys the state of the collapsible element to
assistive technologies. Based on the open or closed state, toggle the aria-expanded
attribute (true or false).
Accessible control: Add the aria-controls
attribute to any control that
targets a single collapsible element. The value of that attribute must contain the id
of the collapsible element. Assistive technologies use this attribute to jump to the collapsible
element.
Footers
Use footers to signify the end of a section especially the end of an HTML document.
For the sake of this document, we are only going to focus on footers used to signify the end of an HTML page.
Footers may contain contact information, navigation items, and social media links.
At a minimum, footers must display the Hennepin County "H" logo in the lower right corner. They also must link to the county's privacy policy and display copyright information.
<footer class="footer">
<a href="https://www.hennepin.us/">
<img src="hennepin-county-minnesota-H-logo-blue.svg" alt="Hennepin County, Minnesota">
</a>
<p>
<a href="https://www.hennepin.us/your-government/open-government/website-privacy-security">Privacy</a>
<span>|</span>
© 2022 Hennepin County, Minnesota
</p>
</footer>
Forms
Textual controls
Textual controls include form elements that are textual inputs or textareas. For example:
input[type="text|email|password|search|tel|url"]
and textarea
.
<label for="exampleInput">First name</label>
<input type="text" class="form-control" id="exampleInput">
<label for="exampleTextarea">Comment</label>
<textarea class="form-control" id="exampleTextarea" rows="3"></textarea>
.form-control {
display: block;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0;
}
Checkboxes and radios
Custom checkboxes and radios create a consistent experience across browsers and devices.
Checkboxes
The focus indicator for checkboxes wraps around the entire label
and control element.
<div class="form-group custom-control custom-checkbox">
<div class="custom-focus">
<input type="checkbox" class="custom-control-input" id="customCheck1">
<label class="custom-control-label" for="customCheck1">Check this checkbox</label>
</div>
</div>
<div class="form-group custom-control custom-checkbox">
<div class="custom-focus">
<input type="checkbox" class="custom-control-input" id="customCheck2">
<label class="custom-control-label" for="customCheck2">Check this checkbox too</label>
</div>
</div>
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-label::before {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #adb5bd solid 1px;
}
.custom-control-label::after {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50% / 50% 50%;
}
.custom-checkbox .custom-control-label::before {
border-radius: 0;
}
.custom-checkbox .custom-control-input:checked ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='8' height='8' viewBox='0 0 8 8'%3e%3cpath fill='%23fff' d='M6.564.75l-3.59 3.612-1.538-1.55L0 4.26l2.974 2.99L8 2.193z'/%3e%3c/svg%3e");
}
.custom-focus {
display: inline-block;
padding-left: 1.5rem;
margin-left: -1.5rem;
}
@supports selector(:focus-within) {
.custom-focus:focus-within {
outline:4px solid;
outline-offset:4px;
}
.custom-focus .custom-control-input:focus ~ .custom-control-label::before,
.custom-focus .custom-control-input:focus ~ .custom-control-label::after {
outline:none;
}
}
Radios
The focus indicator for radios wraps around the entire label
and control element.
<div class="form-group custom-control custom-radio">
<div class="custom-focus">
<input type="radio" id="customRadio1" name="customRadio" class="custom-control-input">
<label class="custom-control-label" for="customRadio1">Toggle this custom radio</label>
</div>
</div>
<div class="form-group custom-control custom-radio">
<div class="custom-focus">
<input type="radio" id="customRadio2" name="customRadio" class="custom-control-input">
<label class="custom-control-label" for="customRadio2">Or toggle this other custom radio</label>
</div>
</div>
.custom-control-label {
position: relative;
margin-bottom: 0;
vertical-align: top;
}
.custom-control-label::before {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
pointer-events: none;
content: "";
background-color: #fff;
border: #adb5bd solid 1px;
}
.custom-control-label::after {
position: absolute;
top: 0.25rem;
left: -1.5rem;
display: block;
width: 1rem;
height: 1rem;
content: "";
background: no-repeat 50% / 50% 50%;
}
.custom-radio .custom-control-label::before {
border-radius: 50%;
}
.custom-radio .custom-control-input:checked ~ .custom-control-label::after {
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='-4 -4 8 8'%3e%3ccircle r='3' fill='%23fff'/%3e%3c/svg%3e");
}
.custom-focus {
display: inline-block;
padding-left: 1.5rem;
margin-left: -1.5rem;
}
@supports selector(:focus-within) {
.custom-focus:focus-within {
outline:4px solid;
outline-offset:4px;
}
.custom-focus .custom-control-input:focus ~ .custom-control-label::before,
.custom-focus .custom-control-input:focus ~ .custom-control-label::after {
outline:none;
}
}
Selects
<label for="selectOne">Choose an option</label>
<select id="selectOne" class="custom-select">
<option selected>Open this select menu</option>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
<label for="selectMulti">Choose multiple options</label>
<select id="selectMulti" class="custom-select" multiple>
<option value="1">One</option>
<option value="2">Two</option>
<option value="3">Three</option>
</select>
.custom-select {
display: inline-block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
padding: 0.375rem 1.75rem 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
vertical-align: middle;
background: #fff url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4
5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right 0.75rem center/8px 10px;
border: 1px solid #ced4da;
border-radius: 0;
appearance: none;
}
File browser
<div class="label-secondary" data-for="inputFile">Upload files</div>
<div class="custom-file">
<input type="file" id="inputFile" class="custom-file-input" multiple>
<label for="inputFile" class="custom-file-label">
<span>Choose files...</span>
</label>
</div>
<div class="label-secondary" data-for="inputFileDrop">File drop</div>
<div class="custom-file file-drop">
<input type="file" id="inputFileDrop" class="custom-file-input" multiple>
<label for="inputFileDrop" class="custom-file-label">
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="17" viewBox="0 0 20 17"><path fill="#113c66" d="M10 0l-5.2 4.9h3.3v5.1h3.8v-5.1h3.3l-5.2-4.9zm9.3 11.5l-3.2-2.1h-2l3.4 2.6h-3.5c-.1 0-.2.1-.2.1l-.8 2.3h-6l-.8-2.2c-.1-.1-.1-.2-.2-.2h-3.6l3.4-2.6h-2l-3.2 2.1c-.4.3-.7 1-.6 1.5l.6 3.1c.1.5.7.9 1.2.9h16.3c.6 0 1.1-.4 1.3-.9l.6-3.1c.1-.5-.2-1.2-.7-1.5z"></path></svg>
<span>Click or drop files here</span>
</label>
</div>
.custom-file {
position: relative;
display: inline-block;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
margin-bottom: 0;
}
.custom-file.file-drop {
height: 20vh;
}
.custom-file.file-drop .custom-file-label {
height: 100%;
border-style: dashed;
border-width: 2px;
background-color: #f8f9fa;
display: flex;
align-items: center;
justify-content: center;
}
.custom-file.file-drop .custom-file-label::after {
display: none;
}
.custom-file.file-drop .custom-file-input {
height: 100%;
}
.custom-file.file-drop .custom-file-input:hover + .custom-file-label {
background-color: #fafbfc;
border-color: #b8c1ca;
}
.custom-file-input {
position: relative;
z-index: 2;
width: 100%;
height: calc(1.5em + 0.75rem + 2px);
margin: 0;
opacity: 0;
}
.custom-file-label,
.custom-file-label::after {
position: absolute;
top: 0;
right: 0;
z-index: 1;
height: calc(1.5em + 0.75rem + 2px);
padding: 0.375rem 0.75rem;
font-weight: 400;
line-height: 1.5;
color: #495057;
background-color: #fff;
border-radius: 0;
.custom-file-label {
left: 0;
overflow: hidden;
border: 1px solid #ced4da;
}
.custom-file-label::after {
bottom: 0;
z-index: 3;
display: block;
height: calc(1.5em + 0.75rem);
content: "Browse";
background-color: #e9ecef;
border-left: inherit;
}
.custom-control-label::before,
.custom-file-label,
.custom-select {
transition: background-color 0.15s ease-in-out,
border-color 0.15s ease-in-out,
box-shadow 0.15s ease-in-out;
}
@media (prefers-reduced-motion: reduce) {
.custom-control-label::before,
.custom-file-label {
transition: none;
}
}
$('input[type="file"]').on('change', function () {
var $label = $(this).next('.custom-file-label').find('span');
if (!this.files) return;
var fileNames = '';
if (this.files.length === 0) {
if ($(this).parent('.file-drop').length) {
fileNames = (this.multiple) ? 'Click or drop files here' : 'Click or drop file here';
} else {
fileNames = (this.multiple) ? 'Choose files...' : 'Choose file...';
}
} else if (this.files.length === 1) {
fileNames = this.files[0].name;
} else {
fileNames = this.files.length + ' files';
}
$label.html(fileNames);
});
Form validation
Form validation should take place on the front and back end.
Include these features in form validation:
- Don't display errors until a user has had a chance to complete the input.
- Remove error messages when the field becomes valid on
keypress
. - Customize the error message to help the user fix the issue.
- Scroll the user to the top-most error and give that field focus.
- Show all errors to the user at the same time when the user tries to submit the form.
- Make error message clickable to give focus to its associated form field.
- Inputs with an error must have an attribute
aria-invalid="true"
- Use
aria-describedby
to associate error messages with form fields to relay the error to screen readers. - Ensure error messages are visible and next to the inputs.
<form id="validation" novalidate>
<div class="form-row">
<div class="col-md-6 mb-3">
<label for="name1">First name</label>
<input type="text" class="form-control" id="name1" aria-describedby="name1Feedback" required>
<div data-for="name1" id="name1Feedback" class="label-secondary invalid-feedback">
Please provide a first name.
</div>
</div>
<div class="col-md-6 mb-3">
<label for="name2">Last name</label>
<input type="text" class="form-control" id="name2" aria-describedby="name2Feedback" required>
<div data-for="name2" id="name2Feedback" class="label-secondary invalid-feedback">
Please provide a last name.
</div>
</div>
</div>
<div class="form-row">
<div class="col-md-6 mb-3">
<label for="city">City</label>
<input type="text" class="form-control" id="city" aria-describedby="cityFeedback" required>
<div data-for="city" id="cityFeedback" class="label-secondary invalid-feedback">
Please provide a city.
</div>
</div>
<div class="col-md-3 mb-3">
<label for="state">State</label>
<select class="custom-select" id="state" aria-describedby="stateFeedback" required>
<option selected disabled value="">Choose...</option>
<option value="AL">Alabama</option>
...
<option vlaue="WY">Wyoming</option>
</select>
<div data-for="state" id="stateFeedback" class="label-secondary invalid-feedback">
Please select a state.
</div>
</div>
<div class="col-md-3 mb-3">
<label for="zip">Zip</label>
<input type="text" class="form-control" id="zip" aria-describedby="zipFeedback" required>
<div data-for="zip" id="zipFeedback" class="label-secondary invalid-feedback">
Please provide a 5 digit zip code.
</div>
</div>
</div>
<div class="form-group custom-control custom-checkbox">
<div class="custom-focus">
<input type="checkbox" class="custom-control-input" id="agreement" aria-describedby="invalidCheck3Feedback"
required>
<label class="custom-control-label" for="agreement">Agree to terms and conditions</label>
</div>
<div data-for="agreement" id="invalidCheck3Feedback" class="label-secondary invalid-feedback">
You must agree before submitting.
</div>
</div>
<div class="form-group">
<button class="btn btn-primary" type="submit">Submit form</button>
</div>
<div id="validationSuccess" class="alert alert-dismissible alert-success" role="alert" style="display:none"></div>
</form>
.form-control.is-invalid {
border-color: #ce1432;
padding-right: calc(1.5em + 0.75rem);
background-image: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3e%3cpath style='fill:%23ce1432;stroke:%23ce1432;stroke-width:4;stroke-miterlimit:10;' d='M25.5,26c-0.1,0-0.3,0-0.4-0.1l-23-23C2,2.7,2,2.3,2.1,2.1s0.5-0.2,0.7,0l23,23c0.2,0.2,0.2,0.5,0,0.7C25.8,26,25.6,26,25.5,26z'/%3e%3cpath style='fill:%23ce1432;stroke:%23ce1432;stroke-width:4;stroke-miterlimit:10;' d='M2.5,26c-0.1,0-0.3,0-0.4-0.1c-0.2-0.2-0.2-0.5,0-0.7l23-23c0.2-0.2,0.5-0.2,0.7,0s0.2,0.5,0,0.7l-23,23C2.8,26,2.6,26,2.5,26z'/%3e%3c/svg%3e");
background-repeat: no-repeat;
background-position: right calc(0.375em + 0.1875rem) center;
background-size: calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-control.is-invalid:focus {
border-color: #ce1432;
box-shadow: 0 0 0 0.2rem rgba(206, 20, 50, 0.25);
}
textarea.form-control.is-invalid {
padding-right: calc(1.5em + 0.75rem);
background-position: top calc(0.375em + 0.1875rem) right calc(0.375em + 0.1875rem);
}
.custom-select.is-invalid {
border-color: #ce1432;
padding-right: calc(0.75em + 2.3125rem);
background: url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='4' height='5' viewBox='0 0 4 5'%3e%3cpath fill='%23343a40' d='M2 0L0 2h4zm0 5L0 3h4z'/%3e%3c/svg%3e") no-repeat right .75rem center/8px 10px,url("data:image/svg+xml,%3csvg xmlns='http://www.w3.org/2000/svg' width='28' height='28' viewBox='0 0 28 28'%3e%3cpath style='fill:%23ce1432;stroke:%23ce1432;stroke-width:4;stroke-miterlimit:10;' d='M25.5,26c-0.1,0-0.3,0-0.4-0.1l-23-23C2,2.7,2,2.3,2.1,2.1s0.5-0.2,0.7,0l23,23c0.2,0.2,0.2,0.5,0,0.7C25.8,26,25.6,26,25.5,26z'/%3e%3cpath style='fill:%23ce1432;stroke:%23ce1432;stroke-width:4;stroke-miterlimit:10;' d='M2.5,26c-0.1,0-0.3,0-0.4-0.1c-0.2-0.2-0.2-0.5,0-0.7l23-23c0.2-0.2,0.5-0.2,0.7,0s0.2,0.5,0,0.7l-23,23C2.8,26,2.6,26,2.5,26z'/%3e%3c/svg%3e") #fff no-repeat center right 1.75rem/calc(0.75em + 0.375rem) calc(0.75em + 0.375rem);
}
.form-check-input.is-invalid ~ .form-check-label {
color: #ce1432;
}
.custom-control-input.is-invalid ~ .custom-control-label {
color: #ce1432;
}
.custom-control-input.is-invalid ~ .custom-control-label::before {
border-color: #ce1432;
}
.custom-control-input.is-invalid:checked ~ .custom-control-label::before {
border-color: #ea2b4a;
background-color: #ea2b4a;
}
.custom-control-input.is-invalid:focus ~ .custom-control-label::before {
box-shadow: 0 0 0 0.2rem rgba(206, 20, 50, 0.25);
}
.custom-control-input.is-invalid:focus:not(:checked) ~ .custom-control-label::before {
border-color: #ce1432;
}
custom-file-input.is-invalid ~ .custom-file-label {
border-color: #ce1432;
}
.custom-file-input.is-invalid:focus ~ .custom-file-label {
border-color: #ce1432;
box-shadow: 0 0 0 0.2rem rgba(206, 20, 50, 0.25);
}
.custom-focus.is-invalid .custom-control-label::before {
border-color: #ce1432;
}
var $form = $('#validation'),
$name1 = $('#name1'),
$name2 = $('#name2'),
$state = $('#state'),
$city = $('#city'),
$zip = $('#zip'),
$agreement = $('#agreement'),
$validationSuccess = $('#validationSuccess');
var validate = {
name1: function () {
if ($name1.val().length) {
$name1.removeClass('is-invalid');
$name1.attr('aria-invalid', 'false');
return true;
} else {
$name1.addClass('is-invalid');
$name1.attr('aria-invalid', 'true');
return false;
}
},
name2: function () {
if ($name2.val().length) {
$name2.removeClass('is-invalid');
$name2.attr('aria-invalid', 'false');
return true;
} else {
$name2.addClass('is-invalid');
$name2.attr('aria-invalid', 'true');
return false;
}
},
city: function () {
if ($city.val().length) {
$city.removeClass('is-invalid');
$city.attr('aria-invalid', 'false');
return true;
} else {
$city.addClass('is-invalid');
$city.attr('aria-invalid', 'true');
return false;
}
},
state: function () {
if ($state.val() && $state.val().length === 2) {
$state.removeClass('is-invalid');
$state.attr('aria-invalid', 'false');
return true;
} else {
$state.addClass('is-invalid');
$state.attr('aria-invalid', 'true');
return false;
}
},
zip: function () {
if (!/\D/.test($zip.val()) && $zip.val().length === 5) {
$zip.removeClass('is-invalid');
$zip.attr('aria-invalid', 'false');
return true;
} else {
$zip.addClass('is-invalid');
$zip.attr('aria-invalid', 'true');
return false;
}
},
agreement: function () {
if ($agreement.is(":checked")) {
$agreement.parent().removeClass('is-invalid');
$agreement.parent().attr('aria-invalid', 'false');
return true;
} else {
$agreement.parent().addClass('is-invalid');
$agreement.parent().attr('aria-invalid', 'true');
return false;
}
}
};
$(document).on('keyup', '#name1.is-invalid, #name2.is-invalid, #city.is-invalid, #zip.is-invalid', function () {
if ($(this).is('#name1')) return validate.name1();
if ($(this).is('#name2')) return validate.name2();
if ($(this).is('#city')) return validate.city();
if ($(this).is('#zip')) return validate.zip();
});
$(document).on('change', '#state.is-invalid, .is-invalid #agreement', function () {
if ($(this).is('#state')) return validate.state();
if ($(this).is('#agreement')) return validate.agreement();
});
$form.on('submit', function (e) {
e.preventDefault();
var isInvalid = false;
if (!validate.name1()) isInvalid = true;
if (!validate.name2()) isInvalid = true;
if (!validate.city()) isInvalid = true;
if (!validate.state()) isInvalid = true;
if (!validate.zip()) isInvalid = true;
if (!validate.agreement()) isInvalid = true;
if (isInvalid) {
var $firstError = $form.find('.is-invalid').eq(0);
var errorScroll = $firstError.offset().top - 100;
var scrollSpeed = (window.matchMedia('(prefers-reduced-motion: reduce)')) ? 0 : 500;
$("html, body").animate({
scrollTop: errorScroll
}, scrollSpeed);
$firstError.focus();
} else {
$validationSuccess.append('<div class="alert-content">Form successfully submitted</div><button type="button" class="close" data-dismiss="alert">Close <span aria-hidden="true">×</span></button>').fadeIn();
}
});
Date input
We use the native HTML5 input[type="date"]
for date input types.
This input lets browsers handle the date picker function while following accessibility standards.
<label for="inputDate">Date</label>
<input type="date" id="inputDate" class="form-control">
.form-control {
display: block;
padding: 0.375rem 0.75rem;
font-size: 1rem;
font-weight: 400;
line-height: 1.5;
background-color: #fff;
background-clip: padding-box;
border: 1px solid #ced4da;
border-radius: 0;
}
Labels
Visible form labels are crucial to making forms accessible.
Form controls should always have a visible label. (Emphasis on the word "visible"). It's crucial for users with cognitive disabilities. It's also crucial for those who use speech recognition software. It also increases usability.
<label for="labelExample">Example label</label>
<input type="text" id="labelExample" class="form-control">
label {
display: inline-block;
margin-bottom: 0.5rem;
}
Help text
<label for="inputPassword">Password</label>
<input type="password" id="inputPassword" class="form-control" aria-describedby="passwordHelp">
<div id="passwordHelp" class="form-text">
Your password must be 8-20 characters long, contain letters and numbers, and must not contain spaces, special
characters, or emoji.
</div>
.form-text {
margin-top: 0.25rem;
color: #6c757d;
}
Labels:You must programmatically associate labels with their fields:
- Label text must be meaningful.
- You may use icons as visual labels instead of text. Make sure the icon is visually self-evident. Make sure to also associate it with the field for assistive technologies.
- Don't use placeholder text as the only way to provide a label for a text input.
- Place labels next to their corresponding elements.
- A label should be next to its corresponding element in the DOM.
Form validation: Give focus to inputs that have an error. Give them an attribute of
aria-invalid="true
."
Use aria-describedby
to associate error
messages with form fields to relay the error to screen readers. Make the error messages visible and
next to the inputs.
Help text: Use aria-describedby
to associate help text with the form
control it relates to. Assistive technologies can then announce the help text when the control
receives focus.
Modals
Modals dialogs require the user to take an action. They direct a user’s attention to important information.
Modals should include:
- An explicit close button (either with an "x" in the upper right corner, or a close button in the modal footer, or both)
- The ability to close the modal by clicking the backdrop
- The ability to close the modal by pressing the esc key
- The modal must be given keyboard focus when opened
- The element that opened the modal must be given keyboard focus when the modal is closed
- The modal must create a keyboard trap for when the modal is open
<div class="modal fade" id="exampleModal" tabindex="-1" aria-labelledby="exampleModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<div class="h5 modal-title" id="exampleModalLabel">Modal title</div>
<button type="button" class="close" data-dismiss="modal" aria-label="Close">
<span aria-hidden="true">×</span>
</button>
</div>
<div class="modal-body">
...
</div>
<div class="modal-footer">
<button type="button" class="btn btn-outline-primary" data-dismiss="modal">Close</button>
<button type="button" class="btn btn-primary">Save changes</button>
</div>
</div>
</div>
</div>
.modal {
position: fixed;
top: 0;
left: 0;
z-index: 1050;
display: none;
width: 100%;
height: 100%;
overflow: hidden;
outline: 0;
}
.modal-dialog {
position: relative;
width: auto;
margin: 0.5rem;
pointer-events: none;
}
.modal.fade .modal-dialog {
transition: transform 0.3s ease-out;
transform: translate(0, -50px);
}
@media (prefers-reduced-motion: reduce) {
.modal.fade .modal-dialog {
transition: none;
}
}
.modal.show .modal-dialog {
transform: none;
}
.modal-content {
position: relative;
display: flex;
flex-direction: column;
width: 100%;
pointer-events: auto;
background-color: #fff;
background-clip: padding-box;
border: 1px solid rgba(0, 0, 0, 0.2);
border-radius: 0;
outline: 0;
}
.modal-backdrop {
position: fixed;
top: 0;
left: 0;
z-index: 1040;
width: 100vw;
height: 100vh;
background-color: #000;
}
.modal-backdrop.fade {
opacity: 0;
}
.modal-backdrop.show {
opacity: 0.5;
}
.modal-header {
display: flex;
align-items: flex-start;
justify-content: space-between;
padding: 1rem 1rem;
border-top-left-radius: 0;
border-top-right-radius: 0;
margin: 0;
}
.modal-header .close {
padding: 1rem 1rem;
margin: -1rem -1rem -1rem auto;
}
.modal-title {
margin: 0;
line-height: 1.5;
}
.modal-body {
position: relative;
flex: 1 1 auto;
padding: 1rem;
}
.modal-footer {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: flex-end;
padding: 0.75rem;
border-bottom-right-radius: 0;
border-bottom-left-radius: 0;
}
.modal-footer > * {
margin: 0.25rem;
}
.modal-scrollbar-measure {
position: absolute;
top: -9999px;
width: 50px;
height: 50px;
overflow: scroll;
}
@media (min-width: 576px) {
.modal-dialog {
max-width: 500px;
margin: 1.75rem auto;
}
}
_showElement(relatedTarget) {
...
this._element.style.display = 'block';
this._element.removeAttribute('aria-hidden');
this._element.setAttribute('aria-modal', true);
this._element.setAttribute('role', 'dialog');
...
$(this._element).addClass('show');
...
}
Accessible role: You need to add role="dialog"
to the modal container.
Accessible ARIA attributes: Be sure to add aria-labelledby="..."
,
referencing the modal title, to modal container. Additionally, you may give a description of your
modal dialog with aria-describedby
on the modal container. If the modal is only
visually hidden from the page, you will need to include aria-hidden="true"
as well.
That value must be changed to false, or the attribute must be removed once the modal is opened.
Use of dismissible "close" button: It must have ample clickable spacing around it
(at least 40px by 40px). The close button must be a button
element, or make use of the
button role (role="button"
). If the close button does include the visible word "Close",
then it must be present through alternative means (such as visually hidden text that is accessible
to assistive technology).
Keyboard experience: There are a couple considerations you need to make for keyboard users.
- Keyboard trap: Typically when developing with accessibility in mind, you want to avoid keyboard traps, but modals are an exception to the rule. You must create a keyboard trap when modals are open to force keyboard users to interact with the dialog, and prevent them from getting lost in the content behind the modal.
- Close on esc keypress: It's always good UX practice to allow the esc key to close modal windows, but it's especially critical when we are creating a keyboard trap. This will allow keyboard users to close modals with ease.
Skip links
These let keyboard users skip navigation or other repeated components across many pages.
To see a working example, tab around this site (especially around navigation controls). Skip links should be visually hidden until they receive focus.
Example of a static skip link (its position and display have been overridden):
<body>
<a class="skip-to-link" href="#main">
Skip to main content
</a>
<nav id="nav">
...
</nav>
<main id="main">
...
</main>
</body>
.skip-to-link {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
top: 4px;
left: 4px;
color: #fff;
background: #ce1432;
z-index: 999;
opacity: 0;
pointer-events: none;
font-size: 1rem;
display: inline-block;
}
.skip-to-link:focus,
.skip-to-link:hover,
.skip-to-link:active {
color: #fff;
opacity: 1;
pointer-events: auto;
padding: 8px;
margin: 0;
height: unset;
width: unset;
clip: unset;
overflow: unset;
}
Tables
Tables are meant for tabular data that preserves relationships within the information.
Tabular information displays in columns and rows. It has logical relationships among data like text, numbers and images. Columns and rows must be identified for the logical relationships to be perceived.
Responsive tables
Responsive tables can be set to side scroll by setting overflow-x: auto;
to the parent
element of a
table.
Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | |
---|---|---|---|---|---|
Row A | A1 | A2 | A3 | A4 | A5 |
Row B | B1 | B2 | B3 | B4 | B5 |
Row C | C1 | C2 | C3 | C4 | C5 |
Responsive tables can change their display properties to stack on smaller screens. This second example is more complex and needs to follow all accessibility guidelines.
Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | |
---|---|---|---|---|---|
Row A | A1 | A2 | A3 | A4 | A5 |
Row B | B1 | B2 | B3 | B4 | B5 |
Row C | C1 | C2 | C3 | C4 | C5 |
Table styles
Table stripes
Table stripes should be used to help guide a user's eyes across wide tables with a lot of data.
Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | |
---|---|---|---|---|---|
Row A | A1 | A2 | A3 | A4 | A5 |
Row B | B1 | B2 | B3 | B4 | B5 |
Row C | C1 | C2 | C3 | C4 | C5 |
Table borders
Borders are another acceptable approach to help guide a user's eyes across tables with a lot of data.
This is especially true when using colspan
.
Col 1 | Col 2 | Col 3 | Col 4 | Col 5 | |
---|---|---|---|---|---|
Row A | A1 | A2 | A3 | A4 | A5 |
Row B | B1 | B2 | B3 | B4 | B5 |
Row C | C1 | C2 - C4 | C5 |
<div class="table-responsive">
<table class="table">
<caption class="sr-only">Example responsive table with side scroll</caption>
<thead>
<tr>
<td scope="col"> </td>
<th scope="col">Col 1</th>
<th scope="col">Col 2</th>
<th scope="col">Col 3</th>
<th scope="col">Col 4</th>
<th scope="col">Col 5</th>
</tr>
</thead>
<tbody>
<tr>
<th scope="row">Row A</th>
<td>A1</td>
<td>A2</td>
<td>A3</td>
<td>A4</td>
<td>A5</td>
</tr>
<tr>
<th scope="row">Row B</th>
<td>B1</td>
<td>B2</td>
<td>B3</td>
<td>B4</td>
<td>B5</td>
</tr>
<tr>
<th scope="row">Row C</th>
<td>C1</td>
<td>C2</td>
<td>C3</td>
<td>C4</td>
<td>C5</td>
</tr>
</tbody>
</table>
</div>
.table {
width: 100%;
margin-bottom: 1rem;
color: #000;
}
.table th,
.table td {
padding: 0.75rem;
vertical-align: top;
border-top: 1px solid #dee2e6;
}
.table thead th {
vertical-align: bottom;
border-bottom: 2px solid #dee2e6;
}
.table tbody + tbody {
border-top: 2px solid #dee2e6;
}
.table-bordered {
border: 1px solid #dee2e6;
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
}
.table-bordered thead th,
.table-bordered thead td {
border-bottom-width: 2px;
}
.table-striped tbody tr:nth-of-type(odd) {
background-color: rgba(0, 0, 0, 0.05);
}
@media (max-width: 575.98px) {
.table-responsive-sm {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive-sm > .table-bordered {
border: 0;
}
}
@media (max-width: 767.98px) {
.table-responsive-md {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive-md > .table-bordered {
border: 0;
}
}
@media (max-width: 991.98px) {
.table-responsive-lg {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive-lg > .table-bordered {
border: 0;
}
}
@media (max-width: 1199.98px) {
.table-responsive-xl {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive-xl > .table-bordered {
border: 0;
}
}
.table-responsive {
display: block;
width: 100%;
overflow-x: auto;
-webkit-overflow-scrolling: touch;
}
.table-responsive > .table-bordered {
border: 0;
}
Sortable Tables
Sortable tables should be used to help users sort data by the column headers, in ascending or descending order.
1 | Myk | 33 | Purple |
---|---|---|---|
2 | Hannah | 21 | Blue |
3 | Salim | 18 | Green |
4 | Greg | 45 | Orange |
5 | Caitlin | 58 | Red |
6 | Cyan | 35 | Yellow |
<div class="table-responsive">
<table class="table table-sort ascending" role="grid" aria-readonly="true">
<caption>Example table with sortable column headers</caption>
<thead class="thead-light">
<tr role="row">
<th role="columnheader" scope="col" aria-sort="ascending">
<button class="btn btn-table-sort sorted">
Index
<svg version="1.1" class="sort-arrows" x="0px" y="0px" viewBox="0 0 401.998 401.998" style="enable-background:new 0 0 401.998 401.998;" xml:space="preserve" aria-hidden="true">
<path class="up" d="M73.092,164.452h255.813c4.949,0,9.233-1.807,12.848-5.424c3.613-3.616,5.427-7.898,5.427-12.847 c0-4.949-1.813-9.229-5.427-12.85L213.846,5.424C210.232,1.812,205.951,0,200.999,0s-9.233,1.812-12.85,5.424L60.242,133.331 c-3.617,3.617-5.424,7.901-5.424,12.85c0,4.948,1.807,9.231,5.424,12.847C63.863,162.645,68.144,164.452,73.092,164.452z" />
<path class="down" d="M328.905,237.549H73.092c-4.952,0-9.233,1.808-12.85,5.421c-3.617,3.617-5.424,7.898-5.424,12.847 c0,4.949,1.807,9.233,5.424,12.848L188.149,396.57c3.621,3.617,7.902,5.428,12.85,5.428s9.233-1.811,12.847-5.428l127.907-127.906 c3.613-3.614,5.427-7.898,5.427-12.848c0-4.948-1.813-9.229-5.427-12.847C338.139,239.353,333.854,237.549,328.905,237.549z" />
</svg>
</button>
</th>
<th role="columnheader" scope="col">
<button class="btn btn-table-sort">
Name
...
</button>
</th>
<th role="columnheader" scope="col">
<button class="btn btn-table-sort">
Age
...
</button>
</th>
<th role="columnheader" scope="col">
<button class="btn btn-table-sort">
Favorite Color
...
</button>
</th>
</tr>
</thead>
<tbody>
<tr role="row">
<th scope="row" role="rowheader">1</th>
<td role="gridcell">Myk</td>
<td role="gridcell">33</td>
<td role="gridcell">Purple</td>
</tr>
<tr role="row">
...
</tr>
<tr role="row">
...
</tr>
<tr role="row">
...
</tr>
<tr role="row">
...
</tr>
<tr role="row">
...
</tr>
</tbody>
</table>
<span id="tableSortlive" aria-live="polite" readCaptions="false" class="sr-only"></span>
</div>
.table-sort tr th[role=columnheader] {
padding: 0;
position: relative;
}
.table-sort tr th[role=columnheader]:focus-within {
z-index: 9;
}
.table-sort .btn-table-sort {
padding: .75rem;
width: 100%;
text-align: left;
white-space: nowrap
}
.table-sort .btn-table-sort:hover {
background-color: #44c8f5
}
.table-sort .btn-table-sort .sort-arrows {
display: inline-block;
width: 1em;
height: 1em;
margin-left: .5em;
}
.table-sort .btn-table-sort:hover .sort-arrows .down,
.table-sort .btn-table-sort:hover .sort-arrows .up,
.table-sort .btn-table-sort:focus .sort-arrows .down,
.table-sort .btn-table-sort:focus .sort-arrows .up,
.table-sort.ascending .btn-table-sort.sorted .sort-arrows .up,
.table-sort.descending .btn-table-sort.sorted .sort-arrows .down {
opacity: 1;
}
.table-sort .btn-table-sort .sort-arrows .down,.table-sort .btn-table-sort .sort-arrows .up {
opacity: .5;
}
.table-sort.ascending .btn-table-sort.sorted .sort-arrows .down,
.table-sort.descending .btn-table-sort.sorted .sort-arrows .up {
opacity: 0;
}
var $tableSortlive = $('#tableSortlive');
$('.table-sort .btn-table-sort').on('click', function() {
var $this = $(this);
var $table = $this.closest('.table-sort');
var caption = $table.find('caption') ? $table.find('caption').text() : 'Table';
var i = $this.parent().index();
var $sortCols = $table.find('tbody < tr < *:nth-child(' + (i + 1) + ')');
var heading = $this.text().trim();
var direction;
if ($this.hasClass('sorted') && $table.hasClass('ascending')) {
// sort desc
$sortCols.sort(function (a, b) {
return $(b).text().localeCompare($(a).text());
});
direction = 'descending';
$table.removeClass('ascending').addClass(direction);
} else {
// sort asc
$sortCols.sort(function (a, b) {
return $(a).text().localeCompare($(b).text());
});
direction = 'ascending';
$table.removeClass('descending').addClass(direction);
}
for (var j = 0; j < $sortCols.length; j++) {
$table.find('tbody').append($($sortCols[j]).closest('tr'));
}
$('.btn-table-sort').removeClass('sorted')
.parent().removeAttr('aria-sort');
$this.addClass('sorted')
.parent().attr('aria-sort', direction);
$tableSortlive.text(caption + ' is now sorted by ' + heading + ', ' + direction);
});
A <caption>
functions like a heading for a table. It helps users with screen readers to find a table and understand what it's about and decide if they want to read it.
Table headers must be designated with <th>
. It is also a good idea to make the scope
explicit by adding a scope of one of the following: col
, row
, colgroup
, or
rowgroup
.
Never use tables to lay out webpages. But if HTML is not editable, you must use role="presentation"
to clearly identify its purpose.
Sorted tables should utilize buttons in the column headers to allow :focus
and native keyboard controls to activate the sorting functionality. An aria-live
region should also be used to alert screen readers when a table has been sorted, and how it was sorted.
Tabs
Add quick, dynamic tab function to move through panes of content.
Horizontal tabs
Tab 1
Sample content for tab 1. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 1
- Sample bullet for tab 1
- Sample bullet for tab 1
Tab 2
Sample content for tab 2. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 2
- Sample bullet for tab 2
- Sample bullet for tab 2
Tab 3
Sample content for tab 3. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 3
- Sample bullet for tab 3
- Sample bullet for tab 3
Vertical tabs
Tab 1
Sample content for tab 1. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 1
- Sample bullet for tab 1
- Sample bullet for tab 1
Tab 2
Sample content for tab 2. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 2
- Sample bullet for tab 2
- Sample bullet for tab 2
Tab 3
Sample content for tab 3. Sample link to exemplify how focus management should work with tabs (ie. keyboard navigation.
- Sample bullet for tab 3
- Sample bullet for tab 3
- Sample bullet for tab 3
<nav>
<ul class="nav nav-tabs" id="tabs" role="tablist">
<li class="nav-item" role="presentation">
<a class="nav-link active" id="tab1" data-toggle="tab" href="#tab1-tab" role="tab" aria-controls="tab1" aria-selected="true" tabindex="0">Tab 1</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="tab2" data-toggle="tab" href="#tab2-tab" role="tab" aria-controls="tab2" aria-selected="false" tabindex="-1">Tab 2</a>
</li>
<li class="nav-item" role="presentation">
<a class="nav-link" id="tab3" data-toggle="tab" href="#tab3-tab" role="tab" aria-controls="tab3" aria-selected="false" tabindex="-1">Tab 3</a>
</li>
</ul>
</nav>
<div class="tab-content" id="tabConent">
<div class="tab-pane fade show active" id="tab1-tab" role="tabpanel" aria-labelledby="tab1">
...
</div>
<div class="tab-pane fade" id="tab2-tab" role="tabpanel" aria-labelledby="tab2">
...
</div>
<div class="tab-pane fade" id="tab3-tab" role="tabpanel" aria-labelledby="tab3">
...
</div>
</div>
.nav-tabs .nav-item {
margin-bottom: -1px;
margin-right: 0.33rem;
position: relative;
}
.nav-tabs .nav-item:focus-within {
z-index: 1;
}
.nav-tabs .nav-link {
background: #113c66;
color: #fff;
}
.nav-tabs .nav-link:focus,
.nav-tabs .nav-link:hover {
background-color: #44c8f5;
color: #113c66;
}
.nav-tabs .nav-link:focus {
outline-color: #000;
}
.nav-tabs .nav-link.disabled {
color: #6c757d;
background-color: transparent;
border-color: transparent;
}
.nav-tabs .nav-link.active,
.nav-tabs .nav-item.show .nav-link {
color: #113c66;
background-color: #44c8f5;
border-color: #dee2e6 #dee2e6 #44c8f5;
position: relative;
}
.nav-tabs .nav-link.active::after,
.nav-tabs .nav-link.active::before,
.nav-tabs .nav-item.show .nav-link::after,
.nav-tabs .nav-item.show .nav-link::before {
content: "";
height: 0;
position: absolute;
width: 0;
border: 10px solid transparent;
border-top-color: #44c8f5;
top: 100%;
left: 50%;
transform: translateX(-50%);
}
.nav-tabs .nav-link.active::before,
.nav-tabs .nav-item.show .nav-link::before {
display: none;
}
.nav-tabs .nav-link.active:focus::before,
.nav-tabs .nav-item.show .nav-link:focus::before {
display: block;
border-width: 12px;
border-top-color: #000;
top: calc(100% + 4px);
}
.nav-tabs-column {
flex-direction: column;
}
.nav-tabs-column .nav-link.active::after,
.nav-tabs-column .nav-link.active::before,
.nav-tabs-column .nav-item.show .nav-link::after,
.nav-tabs-column .nav-item.show .nav-link::before {
border: 10px solid transparent;
border-left-color: #44c8f5;
top: 50%;
left: 100%;
transform: translateY(-50%);
}
.nav-tabs-column .nav-link.active:focus::before,
.nav-tabs-column .nav-item.show .nav-link:focus::before {
display: block;
border-width: 12px;
border-top-color: transparent;
border-left-color: #000;
top: 50%;
left: calc(100% + 4px);
}
function s($selector) {
return document.querySelectorAll($selector);
}
var $tabLists = s('[role="tablist"]');
var keys = {
end: 35,
home: 36,
left: 37,
up: 38,
right: 39,
down: 40
};
for (let i = 0; i < $tabLists.length; i++) {
const $tablist = $tabLists[i];
const $tabs = $tabLists[i].querySelectorAll('[role="tab"]');
const $tabLength = $tabs.length;
const isVertical = $tabLists[i].getAttribute('aria-orientation') === 'vertical';
$tablist.addEventListener('keydown', function(e) {
var key = e.which;
var currIndex = Array.from(this.children).indexOf(this.querySelector(':focus').parentNode);
var $newActiveTab = false;
switch (key) {
case keys.end:
e.preventDefault();
$newActiveTab = $tabs[$tabLength - 1];
break;
case keys.home:
e.preventDefault();
$newActiveTab = $tabs[0];
break;
case keys.left:
e.preventDefault();
$newActiveTab = (currIndex === 0) ? $tabs[$tabLength - 1] : $tabs[currIndex - 1];
break;
case keys.right:
e.preventDefault();
$newActiveTab = ((currIndex + 1) < $tabLength) ? $tabs[currIndex + 1] : $tabs[0];
break;
case keys.up:
if (!isVertical) break;
e.preventDefault();
$newActiveTab = (currIndex === 0) ? $tabs[$tabLength - 1] : $tabs[currIndex - 1];
break;
case keys.down:
if (!isVertical) break;
e.preventDefault();
$newActiveTab = ((currIndex + 1) < $tabLength) ? $tabs[currIndex + 1] : $tabs[0];
break;
};
// activate new tab
if ($newActiveTab) $newActiveTab.tab('show');
});
// shift tabindex for newly inactive tabs
for (let j = 0; j < $tabs.length; j++) {
const $tab = $tabs[j];
$tab.addEventListener('hide.bs.tab', function (e) {
e.relatedTarget.focus();
e.relatedTarget.setAttribute('tabindex', 0);
e.target.setAttribute('tabindex', -1);
});
}
}
Accessible roles: Add role="navigation"
to the most logical parent container of the <ul>
Or wrap a <nav>
element around the whole navigation. Do not add the role to the <ul>
itself. This would prevent assistive technologies from announcing it as a list.
The containing tab element should be given an attribute of role="tablist"
. The tab element itself should be given the attribute role="tab"
. The tab serves as a label for the tab panels. The tab content associated with a given tab should be given the attribute role="tabpanel"
.
Vertical tabs should make use of aria-orientation="vertical"
. Horizontal tabs can use aria-orientation="horizontal"
. But it is not required since this is the default value.
Keyboard Interaction:
- Tab: When focus moves into the tab list, places focus on the active tab element. When the tab list contains the focus, moves focus to the next element in the page tab sequence outside the tablist, which is typically either the first focusable element inside the tab panel or the tab panel itself.
- When focus is on a tab in a tablist with either horizontal or vertical orientation:
- Left Arrow: moves focus to the previous tab. If focus is on the first tab, moves focus to the last tab. Optionally, activates the newly focused tab.
- Right Arrow: Moves focus to the next tab. If focus is on the last tab element, moves focus to the first tab. Optionally, activates the newly focused tab.
- Space or Enter: Activates the tab if it was not activated automatically on focus.
- Home (Optional): Moves focus to the first tab. Optionally, activates the newly focused tab.
- End (Optional): Moves focus to the last tab. Optionally, activates the newly focused tab.
- Shift + F10: If the tab has an associated pop-up menu, opens the menu.
- Delete (Optional): If deletion is allowed, deletes (closes) the current tab element and its associated tab panel, sets focus on the tab following the tab that was closed, and optionally activates the newly focused tab. If there is not a tab that followed the tab that was deleted, e.g., the deleted tab was the right-most tab in a left-to-right horizontal tab list, sets focus on and optionally activates the tab that preceded the deleted tab. If the application allows all tabs to be deleted, and the user deletes the last remaining tab in the tab list, the application moves focus to another element that provides a logical work flow. As an alternative to Delete, or in addition to supporting Delete, the delete function is available in a context menu.
- When focus is on a tab element in a vertical tab list:
- Left Arrow: moves focus to the previous tab. If focus is on the first tab, moves focus to the last tab. Optionally, activates the newly focused tab.
- Right Arrow: Moves focus to the next tab. If focus is on the last tab element, moves focus to the first tab. Optionally, activates the newly focused tab.
If a navigation is styled as tabs, but does not function like tabs (eg. navigation that goes to other pages), do not give the tabs role="tablist
,"
role="tab"
or role="tabpanel
."
Only use these for dynamic tabbed interfaces. Find details in the WAI ARIA Authoring Practices.
Tooltips
Use a tooltip to specify extra information about an element. The tooltip appears when the user mouses over an element, or the element receives focus.
Always make tooltips visible within a user's viewport on all screen sizes. The position of the tooltip should adapt to different screen sizes so it's always visible.
<button type="button" class="btn btn-primary" data-toggle="tooltip" data-placement="top" title="Tooltip on top">
Tooltip on top
</button>
.tooltip {
position: absolute;
z-index: 1070;
display: block;
margin: 0;
font-style: normal;
font-weight: 400;
line-height: 1.5;
text-align: left;
text-align: start;
text-decoration: none;
text-shadow: none;
text-transform: none;
letter-spacing: normal;
word-break: normal;
word-spacing: normal;
white-space: normal;
line-break: auto;
font-size: 0.875rem;
word-wrap: break-word;
opacity: 0;
}
.tooltip.show {
opacity: 0.9;
}
.tooltip .arrow {
position: absolute;
display: block;
width: 0.8rem;
height: 0.4rem;
}
.tooltip .arrow::before {
position: absolute;
content: "";
border-color: transparent;
border-style: solid;
}
.tooltip-top {
padding: 0.4rem 0;
}
.tooltip-top .arrow {
bottom: 0;
}
.tooltip-top .arrow::before {
top: 0;
border-width: 0.4rem 0.4rem 0;
border-top-color: #000;
}
.tooltip-inner {
max-width: 200px;
padding: 0.25rem 0.5rem;
color: #fff;
text-align: center;
background-color: #000;
border-radius: 0;
}
setElementContent($element, content) {
if (typeof content === 'object' && (content.nodeType || content.jquery)) {
if (this.config.html) {
if (!$(content).parent().is($element)) {
$element.empty().append(content);
}
} else {
$element.text($(content).text());
}
return;
}
if (this.config.html) {
if (this.config.sanitize) {
content = sanitizeHtml(content, this.config.whiteList, this.config.sanitizeFn);
}
$element.html(content);
} else {
$element.text(content);
}
}
Accessible role: You need to add role="tooltip"
to the tooltip
container.
Accessible ARIA attributes: Be sure to add aria-describedby="..."
,
referencing the
tooltip container. If the tooltip is visually hidden you must include
aria-hidden="true
."
Once the tooltip is visible, change the value to
false, or remove the attribute.
Typography
Font family
Consistent use of our corporate typefaces reinforces Hennepin's brand identity.
Primary font family - Myriad Pro
Use Myriad Pro for all text, including headings and body copy. When using Myriad Pro Light, use larger point sizes to maximize readability. As a general guide, headlines should be twice as large as subheads, which should be twice as large as body copy. Reserve bold and semi-bold should for subheads.
Font family fallbacks
When Myriad Pro does not load, always provide font fallbacks. Target similar system fonts for your fallback. This will give you the best alternative for the user's system.
body {
font-family: "myriad-pro", -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, "Noto Sans", sans-serif,
"Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol", "Noto Color Emoji";
}
Serif fonts
For formal communications use Adobe Garamond (Garamond). Always provide similar font fallbacks for serif text.
body {
font-family: Garamond, serif;
}
Headings
Headings help organize the content on the page.
Headings are hierarchical, with <h1>
representing the overall idea of the page. Each
page should only have one <h1>
. Headings from <h2>
to
<h6>
help provide more organization and subsections to the content.
The rankings or level of the header relate to the number (for example, the number 2 in h2
).
We recommend not skipping headings. For example, from <h2>
to
<h4>
. It is okay to skip headings if going backwards. For example, from
<h4>
to <h3>
.
Appropriate headings help provide assistive technologies a way to provide in-page navigation.
h1 Heading
h2 Heading
h3 Heading
h4 Heading
h5 Heading
h6 Heading
<h1>All about farm animals</h1>
<h2>Food</h2>
<h3>How do they eat?</h3>
<h3>What do they eat?</h3>
<h2>Home</h2>
h1 {
font-size: 2.5rem;
}
h2 {
font-size: 2rem;
}
h3 {
font-size: 1.66rem;
}
h4 {
font-size: 1.25rem;
}
h5 {
font-size: 1rem;
}
h6 {
font-size: 0.85rem;
}
Body copy
Body copy should be 16px with a line-height
of at least 1.5. This makes it easier for users
to read, especially those with cognitive disabilities. Provide a line-height
of 1.5 to 2.
See WCAG specifying
line spacing in CSS.
For example:
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Lorem ipsum dolor sit amet, consectetur adipiscing elit, sed do eiusmod tempor incididunt ut labore et dolore magna aliqua. Ut enim ad minim veniam, quis nostrud exercitation ullamco laboris nisi ut aliquip ex ea commodo consequat. Duis aute irure dolor in reprehenderit in voluptate velit esse cillum dolore eu fugiat nulla pariatur. Excepteur sint occaecat cupidatat non proident, sunt in culpa qui officia deserunt mollit anim id est laborum.
Sed ut perspiciatis unde omnis iste natus error sit voluptatem accusantium doloremque laudantium, totam rem aperiam, eaque ipsa quae ab illo inventore veritatis et quasi architecto beatae vitae dicta sunt explicabo. Nemo enim ipsam voluptatem quia voluptas sit aspernatur aut odit aut fugit, sed quia consequuntur magni dolores eos qui ratione voluptatem sequi nesciunt. Neque porro quisquam est, qui dolorem ipsum quia dolor sit amet, consectetur, adipisci velit.
Sed quia non numquam eius modi tempora incidunt ut labore et dolore magnam aliquam quaerat voluptatem. Ut enim ad minima veniam, quis nostrum exercitationem ullam corporis suscipit laboriosam, nisi ut aliquid ex ea commodi consequatur? Quis autem vel eum iure reprehenderit qui in ea voluptate velit esse quam nihil molestiae consequatur, vel illum qui dolorem eum fugiat quo voluptas nulla pariatur?
<p>...</p>
<p>...</p>
p {
font-size: 1rem;
line-height: 1.5;
margin-top: 0;
margin-bottom: 1rem;
}