Web Systems documentation

About §

Overview §


Frequently asked questions §


Documentation §

Custom Elements §


<auto-check> npm


Use <auto-check> to perform server-side validation on an input. Add [required] attribute if this input should only be submittable if the validation passes.

When user types in the input, input value is sent to the endpoint specified by src attribute, with value as the parameter name. The endpoint should respond a 422 responses to signal an invalid state, and 200 for a valid state.

Styling classes for states are globally applied by github/behaviors/autocheck.ts.

<clipboard-copy> npm


Copy element text content or input values to the clipboard.


Temporarily display copy feedback icons.

When a copy event is triggered, this behavior will hide a .js-clipboard-clippy-icon element if it exists, show a .js-clipboard-check-icon, and then revert back after 2000ms. This is useful when you want to provide visual feedback to the user that the data has been copied to their clipboard.


Provide text feedback with the value of [data-copy-feedback] in a tooltip.

<details-menu> npm


<details-menu> is a <details> dependent dropdown menu component with keyboard support. Use it for all dropdown menus and customizable <select>s.

  • details-menu[data-menu-input="targetid"]

    On details-menu-selected, sync event.detail.relatedTarget.value to input#targetid.

  • details-menu[data-menu-max-options="5"]

    On details-menu-selected, check number of selected menu items, toggle a warning element, and disable the inputs if at limit.

<g-emoji> npm


Backports native emoji characters to user agents that don't support them. If a browser does not support emoji, a fallback image tag is created. If a browser supports emoji, nothing happens.

In github/github <g-emoji> tag generation is mostly done with the emoji_filter in HTML pipeline. Outside of HTML pipeline, it is recommended to use our custom Rails helper emoji_tag instead of handcrafting a <g-emoji>.

<image-crop> npm


Use <image-crop> to user to set image cropping dimensions. The actual image crop process should be handled by the server. <image-crop> provides coordinates (x, y) and dimensions (width, height).

image-crop does not come with the default bundles. The bundle can be loaded in as needed.



An extension to <include-fragment>. This element repeatedly fetches from the endpoint as long as a 202 response is returned. The fetch happens on an interval that starts from 1000ms, and increases by 1.5x with each attempt, so that pollers don't hammer the server while it's working on the task.

This is most commonly used to display the status of a background job.

Utility modules §

  • Keyboard interactions
    • captureKeypresses()
    • throttled-input.ts
    • onfocus.ts
      • onKey()
      • onFocus()
      • onInput()
  • eventloop-tasks.ts
    • microtask()
    • animationFrame()
    • idle()
  • parseHTML()
  • debounce()
  • scrollTo()

ARIA live region


announce(), announceFromElement()

These two accessibility helper functions set text content into a global aria-live container so they get read out to screen reader users immediately. This call is essential for interaction feedback like "changes saved" or live updates, because content only gets read when they receive focus, which often isn't the case for dynamic content injections.

import {announceFromElement} from './aria-live'

on('submit', '.js-auto-save-changes', async function(event) {
  const form = event.currentTarget as HTMLFormElement
  const response = await fetch(form.action, {
    method: form.method,
    body: new FormData(form)
  if (response && response.ok) {
    const message = query(document, '.js-success-message')
    message.hidden = false
  } else {
    const message = query(document, '.js-error-message')
    message.hidden = false



Create a <details-dialog>. This is useful when:

  • The dialog can't/doesn't have a in-page trigger. For example, the keyboard shortcut dialog is only reachable via [data-hotkey="?"].
  • The dialog invokes a JavaScript-dependent feature.
  • A fresh copy of a dialog is needed for every interaction.
  • An inline <details-dialog> would be cutoff by overflow: hidden parents.
import {dialog} from './details-dialog'
import {on} from 'delegated-events'

on('click', '.js-dialog-trigger', async function() {
  const template = document.getElementById('dialog-template')
  const dialogEl = await dialog({
    content: template.content.cloneNode(true) // or Promise<DocumentFragment>

  // dialog is closed via Escape, clicks on the overlay, or any other JS triggers
  dialog.addEventListener('dialog:remove', () => {})

Form helpers


changeValue(input, value)

  1. Validates the value is the correct type for input
  2. Updates an input value
  3. Fires a change event

Having the change event fire is important for various behaviors to react, such as form validation.

import {changeValue} from './form'
import {on} from 'delegated-events'

on('click', '.js-change-color-button', function() {
  const input = query(document, '.js-color-input', HTMLInputElement)
  changeValue(input, generateRandomColor())

requestSubmit(form, ?submitter)

  1. Preserves the submitter value for form
  2. Fires a submit event
  3. Check if event was canceled, if so, return
  4. Submits the form

Calling native form.submit() method immediately submits the form without triggering the submit event. As a result, code that wants to hook into form submits would never execute.

This helper will one day be deprecated in favor of the native form.requestSubmit(submitter) once browsers we support have all implemented them.

import {submit} from './form'
import {on} from 'delegated-events'

on('click', '.js-should-submit-form', () =>
  const form = closest(document, '.js-form')
  if(confirm('really?')) requestSubmit(form)

fillFormValues(form, fields): void

Fill multiple form fields by item name.

isFormField(element): boolean

Check if an element is a form participant.

serialize(form): string

Serialize form data into URLSearchParams.



Variant on the native hashchange event with extra features:

  • Guarantees that handlers run on DOMContentLoaded, or immediately if page has already loaded;
  • If the anchor references an element by id, the data.target property of the argument passed to handlers will be a reference to that DOM element.

Each handler will called with a data object containing properties:

  • oldURL - String URL before the hash change, or null on initial page load;
  • newURL - String URL after hash change, or current page URL;
  • target - DOM element whose id matches the anchor value, or null.
// Redirect old anchor issue urls
hashChange(data => {
  const match = data.newURL.match(/\/issues#issue\/(\d+)$/)
  if (match) {
    window.location.href = data.newURL.replace(/\/?#issue\/.+/, "/#{m[1]}")

// Ensure that the referenced comment's parent container is visible
hashChange(data => {
  const container = data.target && data.target.closest('.js-comments-container')
  if (container) {
    container.hidden = false



Store the results of expensive function calls and return the cached result when the same inputs occur again.

const cachedFetch = memoize(fetch)

// Will take as long it takes for the server to respond.
await cachedFetch(url)

// Will return practically instantly with the results from the cache.
await cachedFetch(url)

Pjax helpers


pjax({url, container, replace?})

  1. Makes a pjax request
  2. Replace container content with pjax response
  3. Optionally replaceState (instead of adding a browser history entry)
import pjax from './pjax'
import {on} from 'delegated-events'

on('click', '.js-trigger-pjax', function(event) {
  if (event.shiftKey) {
      url: window.location.href + "?from=0",
      element: query(document, '#js-repo-pjax-container'),
      replace: true



Run time assertion functions to query the DOM. Unlike the query functions built into browsers, these are guaranteed to return elements or throw an error. This is useful when working in type systems such as TypeScript or Flow. Since the query functions built in the browser have a nullable return value, you need to check to make sure that there actually is an element with a if statement.

Using built-in querySelector:

function getForm() {
  const element = document.querySelector('.js-submit-comment')
  if (element instanceof HTMLButtonElement) {
    return element.form

Using TypeScript:

function getForm() {
  const element = document.querySelector<HTMLButtonElement>('.js-submit-comment')!
  return element.form

Using @github/query-selector:

import {query} from '@github/query-selector'

function getForm() {
  const element = query(document, '.js-submit-comment', HTMLButtonElement)
  return element.form
Return value when document.querySelector(...) TypeScript query(...)
element is null undefined errors errors
element is HTMLElement undefined undefined errors

The @github/query-selector package has query, querySelectorAll, closest, namedItem, getAttribute functions and you can read more about them on the repo page.



Submit a form with JavaScript. See also: [data-replace-remote-form], github/remote-form.

import {remoteForm} from './remote-form'
remoteForm('.js-remote-form', async function (form, wants) => {
  // form submits
  let response
  try {
    // request kick-off
    // response = await wants.text()
    // response = await wants.json()
    response = await wants.html()
  } catch {
    // error state
  // form submission ends

Sortable button



@github/sortablejs only provides a sorting behavior through drag and drop, which isn't accessible to keyboard users. This function is used to manually bind sort on clicks as an accessible alternative.

import {on} from 'delegated-events'
import {moveWithButton} from 'sortable-button'

on('click', '.js-sortable-button', function(event) {
  const button = event.currentTarget
  moveWithButton(button, button.closest('.js-sortable-item'), submitSortORderUpdateForm)
<ul class="js-sortable-container">
  <li class="js-sortable-item">
    <button type="button" class="js-sortable-button" data-direction="up">Move up</button>
    <button type="button" class="js-sortable-button" data-direction="down">Move down</button>

Updatable content


This helper is used to update an element's content by fetching from an endpoint specified via data-url. This is like using fetchSafeDocumentFragment + innerHTML, but on top of that, this also does the following:

  1. Restore focus if current focus is inside the element
  2. Restore <details id> open states after replacement if a match is found
  3. Preserve scroll position
  4. Prevent replacing content if user is interacting with the element content via hasInteractions() check
import {updateContent} from './updatable-content'

on('click', '.js-refresh', function(event) {
  const list = closest(event.currentTarget, '.js-list')
<button type="button" class="js-refresh">Refresh</button>
<ul class="js-list" data-url="/list"></ul>

Rails helpers and partials §


Web Systems maintain a collection of Rails helpers and partials for frontend development.

Partial: site/details_dialog


Use this shared partial for a generic <details-dialog> markup including the trigger and close button.

<%= render layout: "shared/details_dialog", locals: {
  title: "Sponsor monalisaoctocat",
  button_text: "Sponsor"
} do %>
  <!-- dialog content goes here -->
<% end %>

Helper: ensure_local_vars


Annotate required, optional, and default variables for a partial. In development, this helper will throw exceptions if bad local variables are passed in.

Helper: include_cached_fragment


This caches the response from an <include-fragment> or <poll-include-fragment> request, and serves the cached content to subsequent requests.

<%= include_cached_fragment src: top_languages_path, cache_key: top_languages_cache_key do %>
<% end %>

In the template for top_languages_path, do:

<%= cache top_languages_cache_key do %>
  <% languages.each do |name, percentage| %><%= name %>: <%= perceentage %>%<% end %>
<% end %>

Helper: markdown_toolbar


Use this shared helper to create markdown buttons for a textarea with standard styles and keyboard shortcuts. Please refer to the helper code for optional product feature arguments.

<%= markdown_toolbar("edit-comment-#{comment.id}") %>
<textarea id="edit-comment-#{comment.id}"></textarea>

Helper: noscript


Use noscript helper (as opposed to <noscript>) to render alternative code paths. This will cover not only JavaScript-disabled cases, but also unsupported browsers (to which we do not send JavaScript).

<%= noscript do %>
  <button type="submit">Submit</button>
<% end %>

Helper: safe_data_attributes


Convert a hash into data attributes. See background in github/github#116367.

def hydro_payload
    hydro_click_hmac: "123"
  <%= safe_data_attributes(view.hydro_payload) %>>



Shared behaviors §


Bulk actions


.js-bulk-actions-container[data-bulk-actions-url], .js-bulk-actions, .js-bulk-actions-toggle

Provides a way to update bulk actions buttons/menus from a fetched url when check boxes are updated. This is often used along with check-all. Flow as follows:

  1. User checks a .js-bulk-actions-toggle checkbox
  2. A fetch request gets sent to bulkActionsURL?bulkActionsParameter=checkboxValues
  3. The response HTML is set to .js-bulk-actions element

This is particularly useful when there are multiple actions available for a collections of objects, and each action contains HTML content that changes according to the checkboxes selected; for example, label menu in issue triage.


  • button[data-toggle-for="targetid"]

    On click, toggle details#targetid open.

  • details[data-deferred-details-content-url=""], details[data-details-no-preload-on-hover]

    On toggle and hover, load content from URL by setting url to <include-fragment src>. If data-details-no-preload-on-hover is present, hover is disabled.

Details container


.js-details-container, .js-details-target

Toggle content on a page. This behavior is an alternative to <details>.

Apply .js-details-target to a <button> that will trigger the toggle on click.

Apply .js-details-container and .Details to the element wrapping all the elements to be toggled. This behavior will toggle .Details--on on container. and CSS is used to show and hide the elements.

Elements to be toggled: Add .Details-content--shown to elements shown on initial state. Add .Details-content--hidden elements hidden on initial state.

Client side filtering


Filter a list of items based on user input.


  1. input.js-filterable-field with an ID attribute.
  2. A container containing direct children to be filtered, with data-filterable-for=${input.id}, data-filterable-type=${filterableType}; and only if filterableType is substring-memory, a required data-filterable-src=${path}.

Available filtering types:

  • prefix(default)
  • substring
  • fuzzy
  • substring-memory: like substring, but instead of filtering down direct children, it obtains data from a JSON endpoint specified on the container by the data-filterable-src attribute. Use this when the list might be very long; for example, menus with 1000+ users.

Form interaction

  • input[data-autoselect]

    On focus, auto select the full range of text in the input.

  • button[data-disable-with]

    On click, mark button as disabled to prevent double submission.

  • button[data-confirm], input[data-confirm], a[data-confirm]

    On click, confirm the activation with a system dialog.

  • input[data-indeterminate]

    On load, set input.indeterminate = true. This exists because there is no HTML attribute for the indeterminate state.

Form submission

  • form[data-remote]

    Submit form with JavaScript.

  • input[data-autosubmit]

    Submit input.form on input change.

  • form[data-replace-remote-form], [data-replace-remote-form-target]

    Submit form with JavaScript and replace [data-replace-remote-form-target] with the form response.

  • form[data-warn-unsaved-changes]

    Prevent user from navigating away from a page without saving their changes with an alert dialog.

Removed contents



When .has-removed-contents is added to an element, its contents are detached from the document and stored in memory. Once the class is removed, the original contents are automatically reinserted into the document.

This behavior is useful when working with <form> with multiple states. Using display: none is only presentational and doesn't exclude the form fields from being submitted or participating in validation requirements.



This behavior provides keyboard navigation and activation support, as a power user feature only.

  • Navigating up with k or ctrl+n
  • Navigating down with j or ctrl+p
  • Activating focused item with Enter or o
  • Page up with alt+v
  • Page down with ctrl+v


  • A .js-naigation-container element containing all the .js-navigation-item.
  • A List of .js-navigation-item elements to be highlighted on navigation.
  • (optional) Adding .js-active-navigation-container on .js-naigation-container will mark it as active and receiving keystrokes. This can be done programmatically via activate() exported by navigation.ts.
  • (optional) Adding a.js-navigation-open on or within .js-navigation-item will make it so that the activation behavior triggers a click on the link, on top of the default navigation:keyopen custom event dispatch.



Navigate or submit with pushstate + ajax. This creates a navigation-like experience without refreshing the page.


  1. An element with data-pjax-container attribute and an ID attribute. (In github/github there is a default pjax container in application layout.)
  2. An <a>, <form>, or <select> with data-pjax attribute. data-pjax attribute can be empty, or optionally be used to specify a pjax container selector. This custom container does not need to have data-pjax-container attribute, but is still required to have a unique ID.
  3. (optional) Adding data-pjax-preserve-scroll on the data-pjax element will preserve the current scroll position after pjax request.

Pjax originally come from https://github.com/defunkt/jquery-pjax. The current behavior in GitHub has diverged from the original implementation.

Session resume



Restore field values when the user revisits the page in their current browser session (excludes separate tabs). Backed by @github/session-resume.

Field values are persisted based on page path; therefore, when using URL params to autofill input values, those values might be overridden if there are stored values for the path. In those cases, you can:

  1. Disable session resume behavior when the param is present
  2. Set a unique ID for the state via <meta name="session-resume-id content="[id]">

Socket channel



Establish a web socket connection and subscribes elements to socket channel updates.

A custom event socket:message is dispatched when a message is received; event.detail contains name as the channel name, and data as a JSON payload sent by the server.

To send messages to the client from Rails in github/github, check out GitHub::WebSocket model.


.js-updatable-content listens to socket:message event that's emitted on .js-socket-channel, and calls updateContent(element). The updatable element can be specified with data.gid.



form[data-sudo-required], button[data-sudo-required], summary[data-sudo-required]

Ensure a user is in a SudoSession before taking an action. If the user is not currently in one, a dialog or flow will be triggered. A list of different sudo levels supported can be found in sudo.js. Remember, users can always inspect and edit the markup to remove this attribute. Always make sure the action is protected server-side.

SudoSession is the actual model responsible for checking if the current session has been granted access to perform an action at a given risk level. The backend pieces are owned by @github/appsec.



Suggestion menus are based on the <text-expander> custom element. We decorate expanders with data attributes containing the URL of the suggestions to fetch from the server.

Menu data sources can be applied individually or in combination depending on the context of the textarea it's decorating. Some inputs might suggest only emoji while others might suggest emoji, mentions, and issue references.

There are three suggestion types available:

  • <text-expander keys=":" data-emoji-url="…">

    Suggest emoji on :.

  • <text-expander keys="@" data-mention-url="…">

    Suggest user and teams on @.

  • <text-expander keys="#" data-issue-url="…">

    Suggest issues and pull request references on #.

Combine activation keys with a space.

<text-expander keys=": @ #" data-emoji-url="…" data-mention-url="…" data-issue-url="…">

View components §


View components are backed back github/view_component as shareable pieces of markup with a finite set of predefind behaivors. The components listed here are for encapsulating JavaScript behaviors and the markup necessary to ensure the behaviors are accessible.



This component renders a <details-dialog> styled as Primer: Box Overlay. This ensures that dialog and triggers are properly labeled, has an overlay and a close button.

Render a basic dialog:

<%= render(GitHub::DialogComponent.new(title: "Verify")) do %>
  Dialog content!
<% end %>

Render a dialog with a Catalyst event listener for the close event. The following example outputs a data-action="details-dialog-close:get-repo#refreshList" attribute on the <details-dialog> element:

<%= render(GitHub::DialogComponent.new(title: "Verify", onclose: "get-repo#refreshList")) do %>
  Dialog content!
<% end %>

Render a dialog with a custom trigger and structured content:

<%= render(GitHub::DialogComponent.new(title: "Verify")) do |dialog| %>
  <% dialog.with(:summary) do %>
    <summary class="text-red">Don't click on me <img src="/warning.png" alt="danger"></summary>
  <% end %>
  <% dialog.with(:body) do %>
    Dialog content!
  <% end %>
  <% dialog.with(:footer) do %>
    <button type="button" class="btn btn-block" data-close-dialog>Close</button>
  <% end %>
<% end %>

Render a dialog with Primer variants for Box Overlay:

<%= render(GitHub::DialogComponent.new(title: "Verify", variant: :wide)) do |dialog| %>
  Dialog content!
<% end %>

Render a dialog with defer loaded content, where src will be copied onto <include-fragment> on open:

<%= render(GitHub::DialogComponent.new(title: "Verify", src: "/modal")) do |dialog| %>
  <% dialog.with(:body) do %>
  <% end %>
  <% dialog.with(:footer) do %>
    <button type="button" class="btn btn-block" data-close-dialog>Close</button>
  <% end %>
<% end %>

Render a form within a dialog—To ensure that both body and footer are contained in a form, use the default content block rather than passing in content areas :body and :footer separately.

<%= render GitHub::DialogComponent.new(...) do %>
  <%= form_tag form_path do %>
    <div class="Box-body">
    <div class="Box-footer">
      <button type="submit" class="btn btn-primary">Save group</button>
  <% end %>
<% end %>



This component renders a <details-menu> styled as Primer: SelectMenu. Feature supported:

The menu component takes an array of items what should be one of the following:

  • GitHub::Menu::ButtonComponent
  • GitHub::Menu::LinkComponent
  • GitHub::Menu::RadioComponent

More types of items and multi-select support are coming soon.

Best practices §


Linting and tests §


Frontend test suite


Command: bin/npm test [--debug]

When live editing test cases, --debug allows you to quickly re-run tests by going to the "Debug" view and refreshing the page. Make sure to actually open up the tests in your browser as the test suite doesn't open up your browser for you.

Code health


Command: bin/testrb <test_file_path.rb>

We have a number of tests in place to guard against tech debt, and keep the code base heathy:

  • bundle_requires_test.rb ensures that all JS files are used. If the last instance of a module import is removed, the test prompt users to remove the module.

  • js_classes_get_used_test.rb ensures that all js- prefixed classes used in JS are either referenced in markup or Rails helpers, and vice versa.

  • data_attributes_test.rb ensures that all data attributes used in JS are either referenced in markup or Rails helpers, and vice versa.

There is also script/check-js-bundle-size, which fails the build if there is a drastic change in bundle sizes. It prompts for a review, and would require a manual size limit bump to pass.



Command: bin/bundle exec erblint app/views

ERBLint is used to lint HTML. Custom ERBLint rules can be found at ./erb-linters. Basic conditional in the template is parsed.

Avoid postfix conditions in HTML attributes § so that the attribute will be correctly syntax highlighted, and ERBLint will be able to parse required in the example correctly as one of tag.attributes.

<!-- Bad -->
<input type="text" <%= "required" if required %>>
<!-- Good -->
<input type="text" <% if required %>required<% end %>>

Avoid creating data attributes with data: {} § so that attributes are searchable and lintable.

<!-- Bad -->
<%= form_tag path, data: {warn_unsaved_changes: ""} %>
<!-- Good -->
<%= form_tag path, "data-warn-unsaved-changes": "" %>

Avoid HTML generation Rails helpers § like link_to and content_tag.

Rendering static markup strings is simpler than evaluating helper methods. Inspecting HTML in the browser and searching for it in the source code is easier with static markup in the templates. Raw HTML allows us to develop better static analysis tools more easily than dynamic Rails helpers, and helpers do not always scale to meet requirements - making them a leaky abstraction.

Use safe_data_attributes to generate data attributes from Hash if necessary.

<!-- Bad -->
<%= content_tag :button, text, data: hydro.merge(disable_with: ""), type: "button" %>
<!-- Good -->
<button type="button" data-disable-with <%= safe_data_attributes(hydro) %>><%= text %></button>

To disable a ERBLint rule, add: <%# erblint:disable DisabledAttribute %>


Command: bin/rubocop app/views

RuboCop is used to lint Rails view helper usages in views. Rules can be found in rubocop.yml.

To disable a rule, add: <%# rubocop:disable Rails:ButtonType %>



Command: bin/npm run lint

We use ESLint to enforce patterns and common pitfalls in JavaScript. You can check our custom config out in github/eslint-plugin-github. The config includes TypeScript, as well as code formatting via Prettier.

The Web Systems team has written some custom rules for our specific needs. Find a complete list of custom ESLint rules and documentation in the docs folder.

Setting up your editor to warn you about these violations is highly recommended since this will save you time and CI failure cycles. A lot of rules and violations can be autofixed, and your editor can probably be set up to autofix them for you.

VS Code


If you are using the Asyncronous Linting Engine you might want to add the following snippet to your config so that ALE picks is aware of the tools we use when writing javascript at GitHub.

let g:ale_linters = {
\   'javascript': ['eslint', 'prettier', 'prettier-eslint'],
let g:ale_fixers = {
\   'javascript': ['eslint', 'prettier', 'prettier-eslint'],

Since prettier and eslint both have concepts of fixing code when it's in a violation of a rule, it's also a good idea to turn "fix on save" on so that instead of telling you about a problem ALE will just apply the fix for you:

let g:ale_fix_on_save = 1