Comments
The ability to engage with content must be inclusive. Everyone should be able to have their say
- Version:
- 0.1.0
- Status:
- Published
Introduction
The principle GEL Comments guide goes into some detail about how BBC comments should be presented and behave. The purpose of this guide is to describe the expected technical implementation of a comments section's key features.
Rather than providing a full reference implementation of a working comments section (which is best achieved using a suitable framework such as React or Vue), we will focus on the individual behaviour of the key subcomponents that contribute together to the pattern.
Recommended markup
The comments section
The larger comments section comprises:
- The comment form (or sign in functionality where the user is not authenticated)
- Meta information and sorting control
- The comments stream
Since comments, and the ability to leave a comment, are tangential to the article itself these are grouped into an <aside>
element placed at the end of the article body. The <aside>
element represents a complementary landmark[1] to assistive technology users. An aria-labelledby
attribute is used to label the <aside>
by association to the heading's id
. The heading should comprise both the term "Comments" and the question or call-to-action intended to spark participation.
<aside class="gel-comments" aria-labelledby="comments-label">
<h2 class="gel-comments__label" id="comments-label" >
<small>Comments:</small>
<span>Does the England team have any chance of qualifying for the World Cup?</span>
</h2>
<p>Join in to comment, reply, and rate.</p>
<!-- comments form and comments stream here -->
</aside>
The label is announced along with the complementary role when the user traverses into the <aside>
element. It is also included as the label in screen readers' aggregated landmark menus. The <h2>
heading level marks the comments section as a subsection of the parent article, with its <h1>
heading.
- All About The World Cup Qualifiers (
<h1>
)- Comments: Does the England team have any chance of qualifying for the World Cup? (
<h2>
)
- Comments: Does the England team have any chance of qualifying for the World Cup? (
Screen reader users can navigate directly between headings (and the sections / landmarks they denote), typically using the h shortcut key.
Inside the comments section, the comments form comes first, followed by the meta information and sorting, then the comments stream. A link at the bottom of the section labelled "Leave your comment" points to the comment form's <textarea>
by id
(#your-comment
), so readers can easily leave a comment after reaching the end of the stream.
<aside class="gel-comments" aria-labelledby="comments-label">
<h2 class="gel-comments__label" id="comments-label" >
<small>Comments:</small>
<span>Does the England team have any chance of qualifying for the World Cup?</span>
</h2>
<p>Join in to comment, reply, and rate.</p>
<!-- comments form and comments stream here -->
<div class="gel-comments__footer">
<a href="#your-comment">Leave your comment</a>
</div>
</aside>
The comment form
Authentication is out of scope for this article, so you are asked to imagine the user is already signed in—and the sign in call-to-action replaced by the comment form—in the following example.
<form class="gel-comments__form">
<p>You're signed in as <a href="path/to/user/comment/history">Ben</a></p>
<div class="gel-form__divider">
<div class="gel-comments__success" role="status" aria-live="polite"></div>
</div>
<div class="gel-form__divider">
<label for="your-comment">Add your comment</label>
<div class="gel-comments__field">
<textarea name="your-comment" id="your-comment" maxlength="200" aria-describedby="chars-left"></textarea>
<span class="gel-comments__chars" id="chars-left">
<span class="gel-comments__chars-left">200</span>
<span class="gel-comments__chars-of" aria-hidden="true">/ 200</span>
<span class="gel-sr">characters remaining</span>
</span>
</div>
<div class="gel-form__field-error" id="your-comment-error"></div>
</div>
<div class="gel-form__divider">
<button type="submit" aria-describedby="your-comment-rules" hidden>Post</button>
</div>
<p id="your-comment-rules"><small>Comments must follow the <a href="path/to/rules">house rules</a></small>.</p>
</form>
for="your-comment"
: The textarea must be labelled programmatically, by matching itsid
with a<label>
'sfor
value. As recommended in Form fields the label should appear persistently above the input/textarea, and not be supplanted by aplaceholder
attribute, which presents various accessibility and usability issuesclass="gel-comments__field"
: This groups the<textarea>
with the character count element for CSS positioning. The count is associated with the<textarea>
usingaria-describedby
.class="gel-comment__chars"
: A combination ofaria-hidden="true"
andclass="gel-sr"
ensures the accessible wording for this element follows the pattern "[number] characters remaining". The completearia-describedby
value is a space separated combination of theyour-comment-error
andchars-left
id
s. The text of eachid
's element is read in turn whenever the textarea is focused.class="gel-form__field-error"
: The form should use the standard and accessible error messaging mechanism described in Form fields. This error element is associated with the<textarea>
and populated via the Form fields implementation's script (with addedaria-describedby
, andaria-invalid
where applicable).id="your-comment-rules"
: All users should be made aware of the moderation rules before submitting their comment. To make this information available to screen reader users in a timely fashion, it is associated with the submit button usingaria-describedby
[2]. That is: it will be read out as part of the button's semantic information while the user is focused on it.hidden
: As set out in the principle GEL Comments guide, the submit button is not revealed until the user has entered some text into the<textarea>
class="gel-comments__success"
: This live region[3] is populated with the message "Your comment was posted successfully". Live regions are announced in screen reader software whenever content is appended to them, meaning screen reader users can be kept abreast of changes to state.
Meta information and sort control
Sandwiched between the form and the comment stream is a subcomponent that tells the user the comment count, and allows them to sort the comments based on a handful of different criteria.
How many sorting criteria are present depends on the complexity of any comment for the specific implementation. At the very least, users should be able to reorder comments by submission time: "Latest first" versus "Oldest first". Some comments (see the ensuing comment stream section) include reaction controls. If so, you might implement "Most popular" and "Least popular".
The GEL Comments guide illustrates the sorting control as a kind of dropdown or <select>
. It's recommended this is either implemented using the ARIA menu button pattern[4] or uses a native <select>
element with some special styling. It's possible to achieve the aesthetic illustrated in the GEL Comments guide with CSS—at least for the closed state[5], if not for the <option>
s. The advantage of using the native control is that semantic HTML is robust and interoperable with minimal code.
In the following code sample, the comment count is used as a description for the list (<ul>
) that encloses the comment stream.
<div class="gel-comments__meta">
<span id="comments-count">186 comments</span>
<label class="gel-comments__sort" for="comments-sort">
Show:
<select id="comments-sort">
<option selected>Latest first</option>
<option>Oldest first</option>
</select>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/path/to/gel-icons-all.svg#gel-icon-down"></use>
</svg>
</label>
</div>
<ul class="gel-comments__stream" aria-describedby="comments-count">
<!-- stream of comment list items -->
</ul>
As standard, the SVG takes focusable="false"
and aria-hidden="true"
take it out of focus order and ensure it is not announced in screen reader output.
The comments stream
As already explained in the previous section, the comments themselves ought to be marked up as a list. Lists are identified and their items enumerated in screen reader output. Screen readers also provide list navigation shortcuts, such as the i key for navigating to the next list item in NVDA[6].
Where the facility to reply to comments is implemented, replies should be marked up as nested lists appended to the original comment's content. This describes the relationship of belonging non-visually and to assistive technology users.
<ul class="gel-comments__stream" aria-describedby="comments-count">
<li>
<!-- comment content -->
<ul>
<li>
<!-- reply comment content -->
</li>
</ul>
</li>
</ul>
Each comment, regardless of its nesting level in the list structure, should be introduced by an <h3>
level heading: comments are subcomponents of the comments stream.
- All About The World Cup Qualifiers (
<h1>
)- Comments: Does the England team have any chance of qualifying for the World Cup? (
<h2>
)- Ben (12:00) (
<h3>
) - Clive (13:27) (
<h3>
) - Judith (14:22) (
<h3>
) - Alex (15:08) (
<h3>
)
- Ben (12:00) (
- Comments: Does the England team have any chance of qualifying for the World Cup? (
While the nested list structure describes the nested relationship of replies to original comments, each comment should be considered on the same plane as a contribution to the comment stream / discourse. This way, screen reader users are made aware of the nesting structure, but know that a comment of any type (reply or otherwise) can be navigated to using the <h3>
shortcut (3 in NVDA or JAWS on Windows).
A single comment
Every comment on a BBC site must include:
- An
<h3>
heading containing the commenter's name and the date of submission - The text/body of the comment
Each comment belonging to the same comment stream may also include:
- A reply button
- A sharing button
- Reaction functionality
- An overflow menu containing supplementary functionality such as reporting or sharing the comment
Where possible, it is advised you do not resort to using the overflow menu (as illustrated in the main GEL documentation). If there is room, make all functionality visible by default.
Where the overflow menu is implemented, it should always be as the last control inside the comment, on the right (when rendered in a left-to-right language). It should expand and collapse a horizontal menu of controls according to its aria-expanded
state (true
for expanded; false
for collapsed). Its (visually hidden) text label should read "More".
In the following sample, the overflow menu is in the collapsed state, and the menu itself is, accordingly, hidden
.
<button class="gel-comment__overflow-button" aria-expanded="false">
<span class="gel-sr">More</span>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/path/to/overflow/icon"></use>
</svg>
</button>
<div class="gel-comment__overflow-menu" hidden></div>
The heading
The comment's <h3>
heading (see the previous comments stream section) must include both the commenter's name (as derived from the authenticated user) and the time/date the comment was posted. Only by including the time/date does the <h3>
become unique, and therefore discernible by screen reader users navigating an aggregated headings list.
✕ Name-only headings list
- Clive
- Clive
- Clive
- Judith
✓ Headings include timings
- Clive (12:00)
- Clive (12:01)
- Clive (12:04)
- Judith (12:17)
Linking
The heading content should also be wrapped in a link. This enables readers to link directly to, and share, the comment in contention. In the below example, clicking on the link places the comment's unique id
in the query string, and focuses the comment itself. The tabindex="-1"
attribution ensures keyboard focus, along with scroll position, is moved to the comment.
<li class="gel-comment" id="comment-1562746727083" tabindex="-1">
<h3>
<a href="#comment-1562746727083">
Clive
<time datetime="2019-7-10T12:04">12:04</time>
</a>
</h3>
</li>
The <time>
element's text node can display a time, a date, or a colloquialism like "Just now" depending on the circumstances. However, the datetime
value should consistently include the machine-readable format of the previous example. Here is how you would convert the current time to this format:
var now = Date.now();
var timeParsed = new Date(timestamp);
var datetime = timeParsed.getFullYear() + "-" + (timeParsed.getMonth() + 1) + "-" + timeParsed.getDate() + 'T' + timeParsed.getHours() + ":" + timeParsed.getMinutes();
Reply comments should contain a link to both the current comment and the comment replied to, using the construction "Person 1 replied to Person 2":
<h3>
<a href="#comment-1562762936794">Steve</a>
replied to
<a href="#comment-1562746727083">Clive</a>
<time datetime="2019-08-10T13:48">13:48</time>
</h3>
Theoretically, reply nesting could recurse indefinitely. However, to prevent the comment stream from becoming unwieldy, it is recommended an arbitrary limit is set. Comments, say, 3 replies deep would no longer offer a reply button.
Reaction functionality
The type of 'reaction' functionality available will vary depending on the specific BBC product. But, as illustrated in the GEL Comments guide, it is likely to be a voting or like/dislike mechanism.
Where the like/dislike controls are implemented as illustrated, they should take the form of a toggle button pair:
<div class="gel-comment__vote" role="group" aria-label="vote">
<button aria-pressed="false" aria-descibedby="total-likes-1562746727083">
<span class="gel-sr">Like</span>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/gel/static/images/gel-icons-all.svg#gel-icon-like"></use>
</svg>
<span id="total-likes-1562746727083" aria-hidden="true">216 <span class="gel-sr">total</span></span>
</button>
<button aria-pressed="false" aria-descibedby="total-dislikes-1562746727083">
<span class="gel-sr">Dislike</span>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/gel/static/images/gel-icons-all.svg#gel-icon-dislike"></use>
</svg>
<span id="total-dislikes-1562746727083" aria-hidden="true">34 <span class="gel-sr">total</span></span>
</button>
</div>
role="group"
: The group ARIA role[7] is a generic role to indicate the contents are related/equivalentaria-label
: This gives a non-visual role to the subcomponent, supplementing visual significationaria-describedby
andaria-hidden="true"
:aria-describedby
appends the total vote count to the<button>
screen reader announcement for context. Since the<span>
is inside the button, it would also be announced on focus. This is prevented witharia-hidden="true"
gel-sr
: Visually hides elements only needed for non-visual clarification
See the Recommended behaviour section for notes on how state is managed for this functionality.
Recommended layout
The sorting control
The biggest challenge in terms of styling is the custom aesthetic for the sorting <select>
element. The wtfforms.com site includes a solution using appearance: none
[8] and the positioning of a custom downwards-pointing arrow. The <option>
elements cannot take many custom styles (only color
and background-color
are applicable) but this is considered a fair compromise given the robustness of the underlying native control.
.gel-comments__sort {
position: relative;
}
.gel-comments__sort select {
-webkit-appearance: none;
appearance: none;
border: 0;
background: none;
padding-right: 1em;
}
.gel-comments__sort svg {
width: 0.75em;
position: absolute;
right: 0;
top: 0.25em;
}
Reply styling
As set forth in the GEL Comments guide, reply comments should be demarcated using a vertical line down the left-hand side of the comment. It is recommended this is achieved using a border
and not an image or gradient. CSS border
s are honoured and visible in Windows High Contrast Mode.
.gel-comment--reply {
padding-left: 1rem;
border-left: 2px solid $gel-color--blue;
}
State indication
The pressed (aria-pressed="true"
) state of controls must not be indicated only through a change in colour[9]. In the case of the like and dislike voting controls, the reference implementation uses a blue fill
for the button's SVG icon.
.gel-comment__vote button[aria-pressed="true"] path {
fill: $gel-color--star-command-blue;
}
Recommended behaviour
Comments, and the ability to write them, should be considered a progressive enhancement and the functionality provided via JavaScript. In the Reference implementations to follow, some plain ES5 JavaScript is used to handle rendering to the DOM. In practice, you are more likely to use a library like React or Vue, and fetch comment data over XHR.
Where there is latency, a loading spinner may need to be incorporated similar to that implemented in the Load more component. The Reference implementations use dummy content and data and just exemplify the expected layout and interaction behaviour. Rendering is instantaneous.
Posting a comment
Where JavaScript is available, comments section is rendered and form validation initialised, as implemented in Form fields. In this case, the only validation rule is that the id="your-comment"
<textarea>
is required
. With the following rules object supplied, the aria-required="true"
attribution is automatically added to the <textarea>
element.
var rules = [
{
name: 'your-comment',
required: true,
}
];
The hidden
submit button is visible/available whenever the <textarea>
's value in non-empty. Note that this does not do away with form validation, since we still need to provide associated state information (aria-invalid="true"
) to the user while they are typing, and where they might have deleted all their text. It is better to be explicit.
Submission is suppressed where the required field is empty, and the field's associated error element is populated. The field is marked as invalid with aria-invalid="true"
.
<div class="gel-form__divider">
<label for="your-comment">Add your comment:</label>
<textarea name="your-comment" id="your-comment" name="comment" rows="5" aria-describedby="your-comment-error" aria-required="true" aria-invalid="true"></textarea>
<div class="gel-form__field-error" id="your-comment-error"><strong>Error:</strong> This field is required</div>
</div>
When a comment is successfully submitted, the comment stream should be immediately re-rendered to include the new comment. "Thanks! We will check this as quickly as possible before posting" is appended to the class="gel-comment__success"
live region, and the comment box value is emptied. The custom gel-submitted
event from the Form fields validation script can be the 'hook' for this state management.
form.addEventListener('gel-submitted', function () {
successMsg.textContent = 'Thanks! We will check this as quickly as possible before posting.';
textarea.value = '';
});
This message persists until the user unfocuses the submit button (a blur
event is emitted). This is recommended over a timed removal, which would mean a change in UI unexpectedly not coinciding with a user action. The submit button itself is also hidden at this juncture (the blur
event suggests a keyboard user will have already safely moved to another focusable element).
submitButton.addEventListener('blur', function () {
successMsg.innerHTML = '';
submitButton.hidden = true;
});
Character count
The character count is revealed only when the <textarea>
is in focus.
.gel-comments__chars {
display: none;
}
.gel-comments__form textarea:focus + .gel-comments__chars {
display: block;
}
The accessible text for the character count ("[number] characters remaining") is announced as part of the <textarea>
's accessible description, thanks to the element's id
being included in the <textarea>
's aria-describedby
value (see Recommended markup).
Decrementing the remaining characters is taken care of with an ad hoc script in the reference implementation:
textarea.addEventListener('input', function () {
charsLeft.textContent = 200 - textarea.value.length;
});
Replying
Writing a reply to a comment is first invoked by pressing the original comment's reply button. This should reveal an inline comment form, similar to the main comment form, directly under the reply button, and the reply <textarea>
should be focused. Reply forms are not implemented in the reference implementation but they should behave similarly to the main comment form, with character count and validation error messages included.
Sorting
Selecting a sorting option should immediately re-render the comments stream in the new order. The newly selected option doubles as the control's new label ("Oldest first" in the following example). Since a native <select>
control is used, all of the expected behaviours for such a control are present.
<label class="gel-comments__sort" for="comments-sort">
<span class="gel-sr">Sort</span>
<select id="comments-sort">
<option>Latest first</option>
<option selected>Oldest first</option>
</select>
</label>
Voting
The example voting functionality illustrated in the Reaction functionality section can exist in states. At the outset, no voting has taken place. Each <button>
takes aria-pressed="false"
.
<div class="gel-comment__vote" role="group" aria-label="vote">
<button aria-pressed="false" aria-descibedby="total-likes-1562746727083">
<span class="gel-sr">Like</span>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/gel/static/images/gel-icons-all.svg#gel-icon-like"></use>
</svg>
<span id="total-likes-1562746727083" aria-hidden="true">216 <span class="gel-sr">total</span></span>
</button>
<button aria-pressed="false" aria-descibedby="total-dislikes-1562746727083">
<span class="gel-sr">Dislike</span>
<svg class="gel-icon gel-icon--text" focusable="false" aria-hidden="true">
<use xlink:href="/gel/static/images/gel-icons-all.svg#gel-icon-dislike"></use>
</svg>
<span id="total-dislikes-1562746727083" aria-hidden="true">34 <span class="gel-sr">total</span></span>
</button>
</div>
Where either of the buttons are pressed, that button's aria-pressed
state is switched to true
.
<button aria-pressed="true" aria-descibedby="total-likes-1562746727083">Like</button>
Press the same "Like" button again and its state should switch back to aria-pressed="false"
. Accordingly, the total for likes should be decremented. If the "Dislike" button is pressed while the "Like" button is in the aria-pressed="true"
state, both button states change: "Like" to false
and "Dislike" to true
. Likes decrement by 1
and dislikes increment by 1
. Users cannot both like and dislike something simultaneously.
Reference implementation
Copyright © 2021 BBC. This content is published under the Open Government Licence, unless otherwise noted.