Archive

Author Archive

How to Make a “Scroll to Select” Form Control

September 25th, 2024 No comments

The  element is a fairly straightforward concept: focus on it to reveal a set of s that can be selected as the input’s value. That’s a great pattern and I’m not suggesting we change it. That said, I do enjoy poking at things and found an interesting way to turn a  into a dial of sorts — where options are selected by scrolling them into position, not totally unlike a combination lock or iOS date pickers. Anyone who’s expanded a  for selecting a country knows how painfully long lists can be and this could be one way to prevent that.

Here’s what I’m talking about:

CodePen Embed Fallback

It’s fairly common knowledge that styling  in CSS is not the easiest thing in the world. But here’s the trick: we’re not working with  at all. No, we’re not going to do anything like building our own  by jamming a bunch of JavaScript into a 

. We’re still working with semantic form controls, only it’s radio buttons.

<section class=scroll-container>
  <label for="madrid" class="scroll-item">
      Madrid 
      <abbr>MAD</abbr>
      <input id="madrid" type="radio" name="items">
  </label>
  <label for="malta" class="scroll-item">
      Malta 
      <abbr>MLA</abbr>
      <input id="malta" type="radio" name="items">
  </label>
  <!-- etc. -->
</section>

What we need is to style the list of selectable controls where we are capable of managing their sizes and spacing in CSS. I’ve gone with a group of labels with nested radio boxes as far as the markup goes. The exact styling is totally up to you, of course, but you can use these base styles I wrote up if you want a starting point.

.scroll-container {
  /* SIZING & LAYOUT */
  --itemHeight: 60px;
  --itemGap: 10px;
  --containerHeight: calc((var(--itemHeight) * 7) + (var(--itemGap) * 6));

  width: 400px; 
  height: var(--containerHeight);
  align-items: center;
  row-gap: var(--itemGap);
  border-radius: 4px;

  /* PAINT */
  --topBit: calc((var(--containerHeight) - var(--itemHeight))/2);
  --footBit: calc((var(--containerHeight) + var(--itemHeight))/2);

  background: linear-gradient(
    rgb(254 251 240), 
    rgb(254 251 240) var(--topBit), 
    rgb(229 50 34 / .5) var(--topBit), 
    rgb(229 50 34 / .5) var(--footBit), 
    rgb(254 251 240) 
    var(--footBit));
  box-shadow: 0 0 10px #eee;
}

A couple of details on this:

  • --itemHeight is the height of each item in the list.
  • --itemGap is meant to be the space between two items.
  • The --containerHeight variable is the .scroll-container’s height. It’s the sum of the item sizes and the gaps between them, ensuring that we display, at maximum, seven items at once. (An odd number of items gives us a nice balance where the selected item is directly in the vertical center of the list). 
  • The background is a striped gradient that highlights the middle area, i.e., the location of the currently selected item. 
  •  The --topBit and –-footBit variables are color stops that visually paint in the middle area (which is orange in the demo) to represent the currently selected item.

I’ll arrange the controls in a vertical column with flexbox declared on the .scroll-container:

.scroll-container {
  display: flex; 
  flex-direction: column;
  /* rest of styles */
}

With layout work done, we can focus on the scrolling part of this. If you haven’t worked with CSS Scroll Snapping before, it’s a convenient way to direct a container’s scrolling behavior. For example, we can tell the .scroll-container that we want to enable scrolling in the vertical direction. That way, it’s possible to scroll to the rest of the items that are not in view.

.scroll-container {
  overflow-y: scroll;
  /* rest of styles */
}

Next, we reach for the scroll-snap-style property that can be used to tell the .scroll-container that we want scrolling to stop on an item — not near an item, but directly on it.

.scroll-container {
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  /* rest of styles */
}

Now items “snap” onto an item instead of allowing a scroll to end wherever it wants. One more little detail I like to include is overscroll-behavior, specifically along the y-axis as far as this demo goes:

.scroll-container {
  overflow-y: scroll;
  scroll-snap-type: y mandatory;
  overscroll-behavior-y: none;
  /* rest of styles */
}

overscroll-behavior-y: none isn’t required to make this work, but when someone scrolls through the .scroll-container (along the y-axis), scrolling stops once the boundary is reached, and any further continued scrolling action will not trigger scrolling in any nearby scroll containers. Just a form of defensive CSS.

Time to move to the items inside the scroll container. But before we go there, here are some base styles for the items themselves that you can use as a starting point:

.scroll-item {
  /* SIZING & LAYOUT */
  width: 90%;
  box-sizing: border-box;
  padding-inline: 20px;
  border-radius: inherit; 

  /* PAINT & FONT */
  background: linear-gradient(to right, rgb(242 194 66), rgb(235 122 51));
  box-shadow: 0 0 4px rgb(235 122 51);
  font: 16pt/var(--itemHeight) system-ui;
  color: #fff;

  input { appearance: none; } 
  abbr { float: right; } /* The airport code */
}

As I mentioned earlier, the --itemHeight variable is setting as the size of each item and we’re declaring it on the flex property — flex: 0 0 var(--itemHeight). Margin is added before and after the first and last items, respectively, so that every item can reach the middle of the container through scrolling. 

The scroll-snap-align property is there to give the .scroll-container a snap point for the items. A center alignment, for instance, snaps an item’s center (vertical center, in this case) with the .scroll-container‘s center (vertical center as well). Since the items are meant to be selected through scrolling alone pointer-events: none is added to prevent selection from clicks.

One last little styling detail is to set a new background on an item when it is in a :checked state:

.scroll-item {
  /* Same styles as before */

  /* If input="radio" is :checked */
  &:has(:checked) {
    background: rgb(229 50 34);
  }
}

But wait! You’re probably wondering how in the world an item can be :checked when we’re removing pointer-events. Good question! We’re all finished with styling, so let’s move on to figuring some way to “select” an item purely through scrolling. In other words, whatever item scrolls into view and “snaps” into the container’s vertical center needs to behave like a typical form control selection. Yes, we’ll need JavaScript for that. 

let observer = new IntersectionObserver(entries => { 
  entries.forEach(entry => {
    with(entry) if(isIntersecting) target.children[1].checked = true;
  });
}, { 
  root: document.querySelector(`.scroll-container`), rootMargin: `-51% 0px -49% 0px`
});

document.querySelectorAll(`.scroll-item`).forEach(item => observer.observe(item));

The IntersectionObserver object is used to monitor (or “observe”) if and when an element (called a target) crosses through (or “intersects”) another element. That other element could be the viewport itself, but in this case, we’re observing the .scroll-container for when a .scroll-item intersects it. We’ve established the observed boundary with rootMargin:"-51% 0px -49% 0px".  

A callback function is executed when that happens, and we can use that to apply changes to the target element, which is the currently selected .scroll-item. In our case, we want to select a .scroll-item that is at the halfway mark in the .scroll-containertarget.children[1].checked = true.

That completes the code. Now, as we scroll through the items, whichever one snaps into the center position is the selected item. Here’s a look at the final demo again:

CodePen Embed Fallback

Let’s say that, instead of selecting an item that snaps into the .scroll-container‘s vertical center, the selection point we need to watch is the top of the container. No worries! All we do is update the scroll-snap-align property value from center to start in the CSS and remove the :first-of-type‘s top margin. From there, it’s only a matter of updating the scroll container’s background gradient so that the color stops highlight the top instead of the center. Like this:

CodePen Embed Fallback

And if one of the items has to be pre-selected when the page loads, we can get its position in JavaScript (getBoundingClientRect()) and use the scrollTo() method to scroll the container to where that specific item’s position is at the point of selection (which we’ll say is the center in keeping with our original demo). We’ll append a .selected class on that .scroll-item

<section class="scroll-container">
  <!-- more items -->
  <label class="scroll-items selected">
    2024
    <input type=radio name=items />
  </label>

  <!-- more items -->
</section>

Let’s select the .selected class, get its dimensions, and automatically scroll to it on page load:

let selected_item = (document.querySelector(".selected")).getBoundingClientRect();
let scroll_container = document.querySelector(".scroll-container");
scroll_container.scrollTo(0, selected_item.top - scroll_container.offsetHeight - selected_item.height);

It’s a little tough to demo this in a typical CodePen embed, so here’s a live demo in a GitHub Page (source code). I’ll drop a video in as well:

That’s it! You can build up this control or use it as a starting point to experiment with different layouts, styles, animations, and such. It’s important the UX clearly conveys to the users how the selection is done and which item is currently selected. And if I was doing this in a production environment, I’d want to make sure there’s a good fallback experience for when JavaScript might be unavailable and that my markup performs well on a screen reader.

References and further reading


How to Make a “Scroll to Select” Form Control originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

3 Essential Design Trends, October 2024

September 23rd, 2024 No comments

You have to get creative when you lack a strong visual. This month’s design trends roundup focuses on this concept.

Categories: Designing, Others Tags:

Quick Hit #21

September 20th, 2024 No comments

Seeing a lot more headlines decrying JavaScript and pumping up PHP. Always interesting to see which direction the front-end zeitgeist is leaning.


Quick Hit #21 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

CSSWG Minutes Telecon (2024-09-18)

September 20th, 2024 No comments

For the past two months, all my livelihood has gone towards reading, researching, understanding, writing, and editing about Anchor Positioning, and with many Almanac entries published and a full Guide guide on the way, I thought I was ready to tie a bow on it all and call it done. I know that Anchor Positioning is still new and settling in. The speed at which it’s moved, though, is amazing. And there’s more and more coming from the CSSWG!

That all said, I was perusing the last CSSWG minutes telecon and knew I was in for more Anchor Positioning when I came to the following resolution:

Whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree) (Issue #10526: When does anchor-scope “match” a name?)

Resolutions aren’t part of the specification or anything, but the strongest of indications about where they’re headed. So, I thought this was a good opportunity not only to take a peek at what we might get in anchor-scope and touch on other interesting bits from the telecon.

Remember that you can subscribe and read the full minutes on W3C.org. 🙂

What’s anchor-scope?

To register an anchor, we can give it a distinctive anchor-name and then absolutely positioned elements with a matching position-anchor are attached to it. Even though it may look like it, anchor-name doesn’t have to be unique — we may reuse an anchor element inside a component with the same anchor-name.

<ul>
   <li>
	<div class="anchor">Anchor 1</div>
	<div class="target">Target 1</div>
   </li>
   <li>
	<div class="anchor">Anchor 2</div>
	<div class="target">Target 2</div>
   </li>
   <li>
	<div class="anchor">Anchor 3</div>
	<div class="target">Target 3</div>
   </li>
</ul>

However, if we try to connect them with CSS,

.anchor {
  anchor-name: --my-anchor;
}

.target {
  position: absolute;
  position-anchor: --my-anchor;
  position-area: top right;
}

We get an unpleasant surprise where instead of each .anchor have their .target positioned at its top-right edge, meaning they all pile up on the last .anchor instance. We can see it better by rotating each target a little. You’ll want to check out the next demo in Chrome 125+ to see the behavior:

CodePen Embed Fallback

The anchor-scope property should make an anchor element only discoverable by targets in their individual subtree. So, the prior example would be fixed in the future like this:

.anchor {
  anchor-name: --my-anchor;
  anchor-scope: --my-anchor;
}

This is fairly straightforward — anchor-scope makes the anchor element available only in that specific subtree. But then we have to ask another question: What should the anchor-scope own scope be? We can’t have an anchor-scope-scope property and then an anchor-scope-scope-scope and so on… so which behavior should it be?

This is what started the conversation, initially from a GitHub issue:

When an anchor-scope is specified with a , it scopes the name to that subtree when the anchor name is “matching”. The problem is that this matching can be interpreted in at least three ways: (Assuming that anchor-scope is a tree-scoped reference, which is also not clear in the spec):

  1. It matches by the ident part of the name only, ignoring any tree-scope that would be associated with the name, or
  2. It matches by exact match of the ident part and the associated tree-scope, or
  3. It matches by some mechanism similar to dereferencing of tree-scoped references, where it’s a match when the tree-scope of the anchor-scope-name is an inclusive ancestor of the tree-scope of the anchor query.

And then onto the CSSWG Minutes:

TabAtkins: In anchor positioning, anchor names and references are tree scoped. The anchor-scope property that scopes, does not say whether the names are tree scoped or not. Question to decide: should they be?

TabAtkins: I think the answer should be yes. If you have an anchor in a shadow tree with a part involved, then problems result if anchor scopes are not tree scoped. This is bad, so I think it should be tree scoped sounds pretty reasonable makes sense to me as far as I can understand it 🙂

This solution of the scope of scoping properties expanded towards View Transitions, which also rely on tree scoping to work:

khush: Thinking about this in the context of view transitions: in that API you give names and the tree scope has to be the same for them to match. There is another view transitions feature where I’m not sure if the spec says it’s tree scoped

khush: Want to make sure that feature is covered by the more general resolution

TabAtkins: Proposed more general resolution: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

So the scope of anchor-scope is tree-scoped. Say that five times fast!

RESOLVED: whenever you are comparing names, and at least one is tree scoped, then both are tree scoped, and the scoping has to be exact (not subtree)

The next resolution was pretty straightforward. Besides allowing a , the anchor-scope property can take an all keyword, which means that all anchors are tree-scoped, while the says that specific anchor is three-scoped. So, the question was if all is also a tree-scoped value.

TabAtkins: anchor-scope, in addition to idents, can take the keyword ‘all‘, which scopes all names. Should this be a tree-scoped ‘all‘? (i.e. only applies to the current tree scope)

TabAtkins: Proposed resolution: the ‘all‘ keyword is also tree-scoped in the same way sgtm +1, again same pattern with view-transition-group

RESOLVED: the ‘all‘ keyword is tree-scoped

The conversation switched gears toward new properties coming in the CSS Scroll Snap Module Level 2 draft, which is all about changing the user’s initial scroll with CSS. Taking anexample directly from the spec, say we have an image carousel:

<div class="carousel">
  <img src="img1.jpg">
  <img src="img2.jpg">
  <img src="img3.jpg" class="origin">
  <img src="img4.jpg">
  <img src="img5.jpg">
</div>

We could start our scroll to show another image by setting it’s scroll-start-targe to auto:

.carousel {
  overflow-inline: auto;
}

.carousel .origin {
  scroll-start-target: auto;
}

As of right now, the only way to complete this is using JavaScript to scroll an element into view:

document.querySelector(".origin").scrollIntoView({
  behavior: "auto",
  block: "center",
  inline: "center"
});

The last example is probably a carousel that is only scrollable in the inline direction. Still, there are doubts as far when the container is scrollable in both the inline and block directions. As seen in the initial GitHub issue:

The scroll snap 2 spec says that when there are multiple elements that could be scroll-start-targets for a scroll container “user-agents should select the one which comes first in tree order“.

Selecting the first element in tree-order seems like a natural way to resolve competition between multiple targets which would be scrolled to in one particular axis but is perhaps not as flexible as might be needed for the 2d case where an author wants to scroll to one item in one axis and another item in the other axis.

And back to the CSSWG minutes:

DavidA: We have a property we’re adding called scroll-start-target that indicates if an element within a scroll container, then the scroll should start with that element onscreen. Question is what happens if there are multiple targets?
DavidA: Propose to do it in reverse-DOM order, this would result in the first one applied last and then be on screen. Also, should only change the scroll position if you have to.

After discussing why we have to define scroll-start-target when we have scroll-snap-align, the discussion went on discuss the reverse-DOM order:

fantasai: There was a bunch of discussion about regular vs reverse-DOM order. Where did we end up and why?
flackr: Currently, we expect that it scrolls to the first item in DOM order. We probably want that to still happen. That is why the proposal is to scroll to each item in sequence in reverse-DOM order.

So we are coming in reverse to scroll the element, but only as required so the following elements are showing as much as possible:

flackr: There is also the issue of nearest…
fantasai: Can you explain nearest?
flackr: Same as scroll into view
fantasai: ?
flackr: This is needed with you scroll multiple things into view and want to find a good position (?)
fantasai: You scroll in reverse-DOM order…when you add the spec can you make it really clear that this is the end result of the algorithm?
flackr: Yes absolutely
fantasai: Otherwise it seems to make sense

And so it was resolved:

Proposed resolution 2: When scroll-start-target targets multiple elements, scroll to each in reverse DOM order with text to specify priority is the first item

Lastly, there was the debate about the text-underline-position, that when set to auto says, “The user agent may use any algorithm to determine the underline’s position; however it must be placed at or under the alphabetic baseline.” The discussion was about whether the auto value should automatically adjust the underlined position to match specific language rules, for example, at the right of the text for vertical writing modes, like Japanese and Mongolian.

fantasai: The initial value of text-underline-position is auto, which is defined as “find a good place to put the underline”.
Three options there: (1) under alphabetical baseline, (2) fully below text (good for lots-of-descenders cases), (3) for vertical text on the RHS
fantasai: auto value is defined in the spec about ‘how far down below the text’, but doesn’t say things about flipping. The current spec says “at or below”. In order to handle language-specific aspects, there is a default UA style sheet that for Chinese and Japanese and Korean there are differences for those languages. A couple of implementations do this
fantasai: Should we change the spec to mention these things?
fantasai: Or should we stick with the UA stylesheet approach?

The thing is that Chrome and Firefox already place the underline on the right in vertical Japanese when text-underline-position is auto.

CodePen Embed Fallback

The group was left wiuth three options:

A) Keep spec as-is, update Gecko + Blink to match (using UA stylesheet for language switch)
B) Introduce auto to text-emphasis-position and use it in both text-emphasis-position and text-underline-position to effect language switches
C) Adopt inconsistent behavior: text-underline-position uses ‘auto‘ and text-emphasis-position uses UA stylesheet

Many CSSWG members like Emilio Cobos, TabAtkins, Miriam Suzanne, Rachel Andrew and fantasai casted their votes, resulting in the following resolution:

RESOLVED: add auto value for text-emphasis-position, and change the meaning of text-underline-position: auto to care about left vs right in vertical text

I definitely encourage you to read at the full minutes! Or if you don’t have the time, you can there’s a list just of resolutions.


CSSWG Minutes Telecon (2024-09-18) originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Quick Hit #20

September 19th, 2024 No comments

Having fun with Bramus’ new Caniuse CLI tool. This’ll save lots of trips to the Caniuse site!


Quick Hit #20 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Quick Hit #19

September 19th, 2024 No comments

Two possible syntaxes for CSS masonry, one draft specification, and you get to share your opinions.


Quick Hit #19 originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

A Beginner’s Guide to Using BlueSky for Business Success

September 19th, 2024 No comments

In today’s fast-paced digital world, businesses are always on the lookout for new ways to connect with their audience. BlueSky, a decentralized social media platform, is quickly gaining attention as a fresh alternative to traditional platforms like Twitter and Instagram. While it’s still early days for BlueSky, its unique structure offers a big opportunity for businesses to build communities and engage with users in a more transparent, user-focused way.

Categories: Designing, Others Tags:

Clever Polypane Debugging Features I’m Loving

September 17th, 2024 No comments
Polypane browser displaying ryantrimble.com in three different viewport sizes simultaneously

I’m working on a refresh of my personal website, what I’m calling the HD remaster. Well, I wouldn’t call it a “full” redesign. I’m just cleaning things up, and Polypane is coming in clutch. I wrote about how much I enjoy developing with Polypane on my personal blog back in March 2023. In there, I say that I discover new things every time I open the browser up and I’m here to say that is still happening as of August 2024.

Polypane, in case you’re unfamiliar with it, is a web browser specifically created to help developers in all sorts of different ways. The most obvious feature is the multiple panes displaying your project in various viewport sizes:

I’m not about to try to list every feature available in Polypane; I’ll leave that to friend and creator, Kilian Valkhof. Instead, I want to talk about a neat feature that I discovered recently.

Outline tab

Inside Polypane’s sidebar, you will find various tabs that provide different bits of information about your site. For example, if you are wondering how your social media previews will look for your latest blog post, Polypane has you covered in the Meta tab.

Preview card displaying how open graph information will display on Mastodon.

The tab I want to focus on though, is the Outline tab. On the surface, it seems rather straightforward, Polypane scans the page and provides you outlines for headings, landmarks, links, images, focus order, and even the full page accessibility tree.

Polypane sidebar, outline tab, the view select options display the different views available: Headings, Landmarks, Links, Images, Focus order, and Accessibility Tree

Seeing your page this way helps you spot some pretty obvious mistakes, but Polypane doesn’t stop there. Checking the Show issues option will point out some of the not-so-obvious problems.

Polypane sidebar, outline tab, Landsmarks view, the "Show issues" option is enabled. In the outline, red text describes an error: Multiple banner elements at the same level found.

In the Landmarks view, there is an option to Show potentials as well, which displays elements that could potentially be page landmarks.

Polypane sidebar, Outline tab, Landmark view - the outline displays a red symbol indicating a potential landmark element

In these outline views, you also can show an overlay on the page and highlight where things are located.

Polypane laptop viewport view of ryantrimble.com, the landmark overlay feature is enabled, which outlines landmark elements in blue while also displaying their value

Now, the reason I even stumbled upon these features within the Outline tab is due to a bug I was tracking down, one specifically related to focus order. So, I swapped over to the “Focus order” outline to inspect things further.

Polypane sidebar, outline tab, Focus order view, displays an outline of the focusable elements on the page

That’s when I noticed the option to see an overlay for the focus order.

Polypane laptop view with focus order overlay enabled, displays guide lines marking the location of each focusable items

This provides a literal map of the focus order of your page. I found this to be incredibly useful while troubleshooting the bug, as well as a great way to visualize how someone might navigate your website using a keyboard.

These types of seemingly small, but useful features are abundant throughout Polypane.

Amazing tool

When I reached out to Kilian, mentioning my discovery, his response was “Everything’s there when you need it!”

I can vouch for that.


Clever Polypane Debugging Features I’m Loving originally published on CSS-Tricks, which is part of the DigitalOcean family. You should get the newsletter.

Categories: Designing, Others Tags:

Multiple Anchors

September 16th, 2024 No comments

Only Chris, right? You’ll want to view this in a Chromium browser:

CodePen Embed Fallback

This is exactly the sort of thing I love, not for its practicality (cuz it ain’t), but for how it illustrates a concept. Generally, tutorials and demos try to follow the “rules” — whatever those may be — yet breaking them helps you understand how a certain thing works. This is one of those.

The concept is pretty straightforward: one target element can be attached to multiple anchors on the page.

<div class="anchor-1"></div>
<div class="anchor-2"></div>
<div class="target"></div>

We’ve gotta register the anchors and attach the .target to them:

.anchor-1 {
  anchor-name: --anchor-1;
}

.anchor-2 {
  anchor-name: --anchor-2;
}

.target {
  
}

Wait, wait! I didn’t attach the .target to the anchors. That’s because we have two ways to do it. One is using the position-anchor property.

.target {
  position-anchor: --anchor-1;
}

That establishes a target-anchor relationship between the two elements. But it only accepts a single anchor value. Hmm. We need more than that. That’s what the anchor() function can do. Well, it doesn’t take multiple values, but we can declare it multiple times on different inset properties, each referencing a different anchor.

.target {
  top: anchor(--anchor-1, bottom);
}

The second piece of anchor()‘s function is the anchor edge we’re positioned to and it’s gotta be some sort of physical or logical insettop, bottom, start, end, inside, outside, etc. — or percentage. We’re bascially saying, “Take that .target and slap it’s top edge against --anchor-1‘s bottom edge.

That also works for other inset properties:

.target {
  top: anchor(--anchor-1 bottom);
  left: anchor(--anchor-1 right);
  bottom: anchor(--anchor-2 top);
  right: anchor(--anchor-2 left);
}

Notice how both anchors are declared on different properties by way of anchor(). That’s rad. But we aren’t actually anchored yet because the .target is just like any other element that participates in the normal document flow. We have to yank it out with absolute positioning for the inset properties to take hold.

.target {
  position: absolute;

  top: anchor(--anchor-1 bottom);
  left: anchor(--anchor-1 right);
  bottom: anchor(--anchor-2 top);
  right: anchor(--anchor-2 left);
}

In his demo, Chris cleverly attaches the .target to two