Skip to content

Instantly share code, notes, and snippets.

@davatron5000
Last active May 22, 2026 22:14
Show Gist options
  • Select an option

  • Save davatron5000/b6a62cb782f01c69e818cd4585391138 to your computer and use it in GitHub Desktop.

Select an option

Save davatron5000/b6a62cb782f01c69e818cd4585391138 to your computer and use it in GitHub Desktop.
Why we still need reference target

ReferenceTarget vs. HTMLElement Behaviors

A log of all the places where we tried (and failed) to mimic the native HTML Element APIs.

API Goals

Reference: https://storybooks.fluentui.dev/web-components/?path=/docs/concepts-introduction--docs

As a team we have a few culturally-enforced goals for our components.

  • Single element compositions preferred over multi-element compositions (lower chance of mistakes)
  • Low DOM complexity (e.g. no nested elements required, if avoidable) - partner teams are sensitive to node counts
  • Prioritize accessibility and performance over everything else
  • Need bundled template and style delivery
  • Support size/appearance/shape variants on form controls
  • Component names are inherited from React system, align to web whenever possible

Templates and adopted styles are composed into the element and we're supporting server-side rendering options as well. I'm picking out a set of elements where we've had to build workarounds to mimic native APIs. IIRC most of these support aria-* attributes and either delegate those attributes or handle those behaviors.

This often includes a lot of JavaScript (boo! no! angst!) API surface just to duplicate what the platform provides natively.

There are two "painpoints" we hope to solve through additional APIs like HTML*ElementBehaviors and/or referencetarget:

  • Improve accessibility concerns like labelling
  • Reduce script required to achieve native behaviors

Button

For fluent-button we use ElementInternals.role = "button" to set a role and send a concise template.

<fluent-button>
  <template srm=open>
    <slot name="start"></slot>
    <slot></slot>
    <slot name="end"></slot>
  </template>
</fluent-button>

The major problem we encountered is that you can't mimic <button type="submit">

Attributes to mimic native button

In order to support HTMLSubmitButton behaviors we mimic the an API. And even with all these type="submit"-centric attributes we cannot submit a form.

Property Solved by?
disabled: boolean HTMLSubmitButtonBehavior
formaction: string HTMLSubmitButtonBehavior
form: string HTMLSubmitButtonBehavior
formenctype: string HTMLSubmitButtonBehavior
formmethod: string HTMLSubmitButtonBehavior
formnovalidate: boolean HTMLSubmitButtonBehavior
name: string HTMLSubmitButtonBehavior
type: submit | reset | button HTMLSubmitButtonBehavior, HTMLResetButtonBehavior, HTMLButtonBehavior
value: string HTMLSubmitButtonBehavior

Nested button?

<fluent-button>
  <template srm=open>
    <button>
      <slot name="start"></slot>
      <slot></slot>
      <slot name="end"></slot>
    </button>
  </template>
</fluent-button>

We considered adding a nested shadowed <button> element, but that becomes just an extra DOM node since we can't pass/delegate attributes down to the control. If referencetarget existed and we could delegate attributes then we would maybe revist that decision and HTMLSubmitButtonBehaviors might not be necessary.

Other API options considered

Slotted button

<fluent-button>
  <button>Light DOM Button</button>
</fluent-button>

Requiring a Light DOM <button> as a slotted child would be a potential workaround, but wouldn't fully support our slotted icons and Light DOM styles would override shadowed styles. It also raises DOM complexity and increases chances of unaccepted elements (e.g. interactive elements inside button)

Extended elements

  <button is="fluent-button>Extended Button</button>
  • Not supported by Safari
  • Shadow template limitations
  • Binds to single element type

Checkbox

<fluent-checkbox>
  <template srm=open>
    <slot name="checked-indicator"><!-- default svg icon --></slot>
    <slot name="indeterminate-indicator"><!-- default svg icon --></slot>
  </template>
</fluent-checkbox>

Attributes to mimic native checkbox

Property Solved by?
disabled: boolean HTMLCheckboxBehavior?
autofocus: boolean HTMLCheckboxBehavior?
checked: boolean HTMLCheckboxBehavior?
form: string HTMLCheckboxBehavior?
indeterminate: boolean HTMLCheckboxBehavior?
name: string HTMLCheckboxBehavior?
required: boolean HTMLCheckboxBehavior?
value: string HTMLCheckboxBehavior?

Similar to button, nesting a native <input type="checkbox"> would just be an extra element (with styling limitations like height/width limits and no custom icon). Were something like referencetarget to exist, we could potentially revisit, but it wouldn't solve the style limitations (Updates to apperance: base fixes this?)

Label

<fluent-label>
  <template srm=open>
    <slot><slot>
    <span part="asterisk" ?hidden>*</span>
  </template>
</fluent-label>

⚠️ We're unable to mimic a native label because there's no role="label" in ARIA and we cannot set ElementInternals.role = "label". This does not actually label elements, so it ends up being an entirely decorative <span> (and potentially duplicative).

Actions like clicking <fluent-label> to enter the element don't work, so we had to change the API of Input and Textarea to include a native <label> element in the shadowroot.

  • HTMLLabelBehaviors would help to get some of the actual label click/labelling behaviors
  • referencetarget would also be possible and we could use a native <label> but we have a couple "form-ish" attributes like required for showing/hiding the asterisk and disabled for a disabled appearance appearance (this could be redesigned).

Link

<fluent-link>
  <template srm=open>
    <slot></slot>
    <a inert><!-- Light DOM injected link --></a>
  </template>
</fluent-link>

To achieve Ctrl|Shift|Meta + Click behaviors we had to injet a native anchor into the Light DOM to intercept the behavior.

Attributes to mimic native a anchor elements

Property Solved By?
href: string ?
hreflang: string ?
reffererpolicy: string ?
rel: string ?
type: string ?
target: _self | _blank | _parent | _top ?

Progress Bar

Mimics native elements

<fluent-progress-bar>
  <template srm=open>
     <div part="indicator"></div>
  </template>
</fluent-progress-bar>

Attributes to mimic native progress/meter elements

Property Solved By?
max: number ?
min: number ?
value: number ?

Radio

<fluent-radio tabindex="0">
  <template srm=open>
    <slot name="checked-indicator">
      <!-- default slotted indicator -->
      <span part="checked-indicator" role="presentation"></span>
    </slot>
  </template>
</fluent-radio>

tabindex="0" is injected by the client

Attributes to mimic native input[type="radio"]

Property Solved By?
checked: boolean ?
disabled: boolean ?

RadioGroup

Fieldset, basically

<fluent-radio-group>
  <template srm=open>
   <slot><!--fluent-field with label + radio go here --></slot>
  </template>
</fluent-radio-group>

Attributes to mimic native fieldset

Property Solved By?
disabled: boolean ?

Slider

<fluent-slider tabindex="0">
  <template srm=open>
    <div part="track-container">
      ::before <!-- filled line -->
    </div>
    <div part="thumb-container">
      <slot name="thumb">
        <!-- default thumb -->
        <div class="thumb"></div>
      </slot>
    </div>
  </template>
</fluent-slider>

tabindex="0" is injected

Attributes to mimic native input[type="range"]

Property Solved By?
disabled: boolean ?
min: number ?
value: number ?
step: number ?
orientation: horizontal | vertical ?

Switch

<fluent-switch tabindex="0">
  <template srm=open>
    <slot name="switch">
      <!-- default switch indicator -->
     <span part="checked-indicator"></span>
   </slot>
  </template>
</fluent-switch>

tabindex="0" is injected

Attributes to mimic "native" input[switch]

Property Solved By?
checked: boolean ?
disabled: boolean ?
required: boolean ?

TextArea

The design of this uses a native textarea in the shadowroot because it's was too risky to build a proper textarea with contenteditable. We had to include a label for the shadow textarea because we couldn't label from outside. referencetarget would clean that up and allow for an actual label.

<fluent-textarea>
  <template srm=open>
    <label for="control" part="label" ?hidden>
      <slot="label"><!-- label text goes here --><slot>
    </label>
    <div part="root">
      <textarea part="control"></textarea>
    </div>
    <div ?hidden>
      <slot></slot>
    </div>
  </template>
</fluent-textarea>

Attributes to mimic native textarea

Property Solved By?
autocomplete: string ?
autofocus: boolean ?
dirname: string ?
disabled: boolean ?
form: string ?
maxlength: number ?
minlength: number ?
name: string ?
placeholder: string ?
readOnly: boolean ?
required: boolean ?
resize: none | both | horizontal | vertical ?
spellcheck: boolean ?

TextInput

The design of this uses a native input because it's was too risky to build a proper text input with contenteditable. We also feel the native input API is a bit overloaded and so we scope our "TextInput" to textual inputs (email, password, text, tel, url, etc) only.

<fluent-input>
  <template srm=open>
      <label part="label" for="control">
        <slot><!-- text goes here --></slot>
      </label>
      <div class="root" part="root">
        <slot name="start"></slot> 
        <input part="control" id="control" type="text">
        <slot name="end"></slot> 
      </div> 
  </template>
</fluent-input>

We had to include a label for the shadow input because we couldn't label from outside. referencetarget would clean that up and allow for an actual label.

Attributes to mimic native input[type="text|email|

This only covers a small subset of input attributes because we scope this to only textual inputs.

  • Supported: email | password | text | tel | url
  • NOT supported: button | checkbox | color | date | datetime-local | file | hidden | image | month | number | radio | range | time | week | ...etc

Additionally we ignore attributes like autocapitalize, pattern, and others...

Property Specific input type? Solved By?
autocomplete: string all ?
autofocus: boolean all ?
dirname: string text, search, url, tel, email ?
disabled: boolean all ?
form: string all ?
list: string all, !password ?
maxlength: number all ?
minlength: number all ?
multiple: boolean email ?
name: string all ?
pattern: string all ?
placeholder: string all ?
readOnly: boolean all ?
required: boolean all ?
type: email | password | tel | text | url all ?
value: string all ?
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment