Connect Draggable Circles with CSS

Build draggable circles joined by an animated curved CSS connector.

Introduction

30 Second Summary

Flow charts feel clear because every connection moves with the ideas it joins. A detached or awkward line quickly breaks that illusion.

In this project, you will build two draggable circles connected by a curved CSS line. The connector stays attached while you drag each circle across the page.

What You'll Build

You will drag two circles around your browser and watch their connector bend, straighten, animate, and keep its endpoints locked in place.

By the end of this project, you'll have:

  • Two draggable circles that you can move anywhere across the page.
  • A curved connector that stays aligned with both circle centres as they move.
  • Smooth shape transitions when the circles move close together, complete with circle or arrow endpoints that follow the line.
  • Secret Mission: An optional challenge to push your curved connector skills further.

Are there any prerequisites?

You'll need a text editor plus Chrome or Edge because this project uses newer browser features. Basic HTML and CSS knowledge helps, while the page runs locally at no cost.

Before We Start

The connector depends on young CSS features. A browser without full support can make correct code look broken.

In this step, you'll prepare a code editor for writing the page. You'll also prepare Chrome or Microsoft Edge for testing each visual change.

In this step, get ready to:
  • Prepare a code editor that can save plain text files.
  • Install or confirm access to Chrome or Microsoft Edge.
  • Load the source article in your compatible browser.
Prepare your code editor

A code editor gives you one place to write the page's HTML, CSS, and drag behaviour. No project files are needed in this step.

Which starting point matches your computer?

✔️ I already have a code editor

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type the name of your code editor into the search bar.
  • Press Enter to open the editor.

You should see the editor window with an empty workspace or welcome screen.

That is one tool ready. Your editor can now hold the page you build in the next step.

ⓧ I need a code editor

Any code editor that saves plain text files works for this project. Download yours from its official product website.

The installer may ask for permission to make changes to your computer. Approving that request allows the installation to continue.

  • Choose a code editor that supports HTML and CSS files.
  • Visit the editor's official download page.
  • Download the installer for your operating system.
  • Open the downloaded installer.
  • Follow the installer's on-screen instructions.
  • Press Cmd+Space on macOS or the Windows key on Windows to return to your search bar.
  • Type the editor's name into the search bar.
  • Press Enter to open the editor.

You should see the editor window with an empty workspace or welcome screen.

Prepare a compatible browser

The connector uses modern browser features such as border-shape and shape(). Firefox and Safari may not render the finished page correctly.

Which browser setup matches your computer?

✔️ Chrome or Edge is installed

  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Chrome or Microsoft Edge into the search bar.
  • Press Enter to open the browser.

You should see a Chrome or Microsoft Edge window with a new tab.

ⓧ I need a compatible browser

Choose one browser below. You only need Chrome or Microsoft Edge for this project.

Google Chrome

The Chrome installer detects your operating system from its official download page.

  • Visit the official Chrome download page.
  • Select the download option for your computer.
  • Open the downloaded installer.
  • Follow the installer's on-screen instructions.
  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Chrome into the search bar.
  • Press Enter to open Chrome.

You should see a Chrome window with a new tab.

Microsoft Edge

The Microsoft Edge download page provides an installer for each supported desktop operating system.

  • Visit the official Microsoft Edge download page.
  • Select the download option for your computer.
  • Open the downloaded installer.
  • Follow the installer's on-screen instructions.
  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Microsoft Edge into the search bar.
  • Press Enter to open Microsoft Edge.

You should see a Microsoft Edge window with a new tab.

Browser installation not working?

  • Confirm that you downloaded the desktop installer for your operating system.
  • Download the installer again from the official page if the first download is incomplete.
  • Use the browser installer for your computer's account if installation is blocked by administrator permissions.
  • Help me install a compatible browser for this CSS project.
Verify both tools

Before you check, do you expect both tools to open without another setup screen?

  • Switch back to the code editor from earlier.
  • Confirm that its empty workspace or welcome screen is visible.
  • Switch back to Chrome or Microsoft Edge from earlier.
  • Open the source article about connecting two circles.
  • Confirm that the page discusses drawing a CSS connection between two circles.

You should see the source article open in Chrome or Microsoft Edge. Your editor should also remain available when you switch back to it.

Your setup is ready. You can now build each visual change in a browser that supports the connector technique.

Your editor and compatible browser are ready. Next up, you'll create the page and drag its first two circles around the screen.

Create Draggable Circles

Every curved connector needs two endpoints whose positions can change. This step creates those endpoints before the curve exists.

You'll build a standalone HTML page with two circles. CSS gives each endpoint its shape. JavaScript updates each position while you drag.

In this step, get ready to:
  • Create a canvas-like area with two styled circles.
  • Add pointer handlers that update each circle's position.
  • Drag both circles independently in Chrome or Microsoft Edge.
Build the circle canvas

The canvas gives both circles a shared coordinate system. Absolute positioning lets each circle hold its own location inside that area.

  • Return to the code editor from earlier.
  • Use your editor's new-file control to create index.html on your Desktop.
  • Paste the complete starting page into index.html:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Draggable Circles</title>
    <style>
        * { box-sizing: border-box; }
        body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f4f7fb; }
        /* Give both circles one positioned area to move inside. */
        .canvas { position: relative; width: 90vw; max-width: 900px; height: 560px; border: 2px solid #cbd5e1; border-radius: 24px; background: #ffffff; overflow: hidden; }
        /* Make each endpoint visible and ready for pointer input. */
        .circle { position: absolute; width: 80px; height: 80px; border-radius: 50%; background: #2563eb; box-shadow: 0 12px 30px rgba(37, 99, 235, 0.25); cursor: grab; touch-action: none; user-select: none; }
        .circle:nth-child(2) { background: #f97316; box-shadow: 0 12px 30px rgba(249, 115, 22, 0.25); }
    </style>
</head>
<body>
    <!-- The two endpoints start at separate positions. -->
    <main class="canvas" aria-label="Draggable circle canvas">
        <div class="circle" style="left: 18%; top: 40%;"></div>
        <div class="circle" style="left: 72%; top: 52%;"></div>
    </main>
</body>
</html>

What does this page create?

  • The .canvas class creates the bordered movement area. Its relative position becomes the reference point for both circles.
  • The .circle class turns each element into an 80px circle. Pointer input can start anywhere inside that shape.
  • The inline left values give the circles separate horizontal positions.
  • The inline top values give the circles separate vertical positions.
  • Save index.html.
  • Press Cmd+Space on macOS or the Windows key on Windows to open your search bar.
  • Type Chrome or Microsoft Edge.
  • Press Enter to open the browser.
  • Drag index.html from your Desktop into the browser window.

You'll see a blue circle beside an orange circle inside a large white canvas. The circles stay still because the page does not have movement logic yet.

Don't see two circles?

  • Confirm the file is named index.html.
  • Check that both <div class="circle"> elements sit inside the <main> element.
  • Reload the file in Chrome or Microsoft Edge after saving your latest changes.
  • Help me find why my two circles are missing.
Add pointer dragging

Pointer Events provide coordinates for mouse, pen, or touch input. Pointer capture keeps each drag connected to the circle that started it.

  • Switch back to index.html in the code editor from earlier.
  • Find the closing </body> tag.
  • Add the drag logic immediately above that tag by pasting this code:
<script>
    const canvas = document.querySelector(".canvas");
    const circles = document.querySelectorAll(".circle");

    // Give every circle its own drag state and pointer handlers.
    for (const circle of circles) {
        let offsetX = 0;
        let offsetY = 0;

        circle.addEventListener("pointerdown", (event) => {
            const circleRect = circle.getBoundingClientRect();
            offsetX = event.clientX - circleRect.left;
            offsetY = event.clientY - circleRect.top;
            circle.setPointerCapture(event.pointerId);
        });

        circle.addEventListener("pointermove", (event) => {
            if (!circle.hasPointerCapture(event.pointerId)) return;
            const canvasRect = canvas.getBoundingClientRect();
            const nextLeft = event.clientX - canvasRect.left - offsetX;
            const nextTop = event.clientY - canvasRect.top - offsetY;
            const maxLeft = canvas.clientWidth - circle.offsetWidth;
            const maxTop = canvas.clientHeight - circle.offsetHeight;
            circle.style.left = `${Math.max(0, Math.min(nextLeft, maxLeft))}px`;
            circle.style.top = `${Math.max(0, Math.min(nextTop, maxTop))}px`;
        });
    }
</script>

What does the drag logic do?

  • The pointerdown handler records where you grabbed the circle. This keeps the circle from snapping its centre directly under the pointer.
  • The setPointerCapture() call keeps movement events connected to the active circle.
  • The pointermove handler converts browser coordinates into positions inside the canvas.
  • The Math.max() and Math.min() calls keep each circle within the canvas edges.

Before you test this change, decide whether a fast pointer movement should break the drag.

  • Save index.html.
  • Return to the browser from earlier.
  • Refresh the page.
  • Drag the blue circle quickly across the canvas.

You'll see the blue circle stay attached to your pointer. It stops at the canvas edges.

Circle stays in place?

  • Confirm the <script> block sits above the closing </body> tag.
  • Check that document.querySelectorAll(".circle") uses the same class name as both circle elements.
  • Refresh the browser after saving index.html.
  • Help me debug a circle that will not drag.

✔️ Awesome, I've got everything!

Your saved index.html now contains the canvas, two circles, and the pointer handlers.

ⓧ I'd like to double check the full code

  • Compare your saved index.html with this complete version:
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Draggable Circles</title>
    <style>
        * { box-sizing: border-box; }
        body { margin: 0; min-height: 100vh; display: grid; place-items: center; background: #f4f7fb; }
        /* Give both circles one positioned area to move inside. */
        .canvas { position: relative; width: 90vw; max-width: 900px; height: 560px; border: 2px solid #cbd5e1; border-radius: 24px; background: #ffffff; overflow: hidden; }
        /* Make each endpoint visible and ready for pointer input. */
        .circle { position: absolute; width: 80px; height: 80px; border-radius: 50%; background: #2563eb; box-shadow: 0 12px 30px rgba(37, 99, 235, 0.25); cursor: grab; touch-action: none; user-select: none; }
        .circle:nth-child(2) { background: #f97316; box-shadow: 0 12px 30px rgba(249, 115, 22, 0.25); }
    </style>
</head>
<body>
    <!-- The two endpoints start at separate positions. -->
    <main class="canvas" aria-label="Draggable circle canvas">
        <div class="circle" style="left: 18%; top: 40%;"></div>
        <div class="circle" style="left: 72%; top: 52%;"></div>
    </main>
    <script>
        const canvas = document.querySelector(".canvas");
        const circles = document.querySelectorAll(".circle");

        // Give every circle its own drag state and pointer handlers.
        for (const circle of circles) {
            let offsetX = 0;
            let offsetY = 0;

            circle.addEventListener("pointerdown", (event) => {
                const circleRect = circle.getBoundingClientRect();
                offsetX = event.clientX - circleRect.left;
                offsetY = event.clientY - circleRect.top;
                circle.setPointerCapture(event.pointerId);
            });

            circle.addEventListener("pointermove", (event) => {
                if (!circle.hasPointerCapture(event.pointerId)) return;
                const canvasRect = canvas.getBoundingClientRect();
                const nextLeft = event.clientX - canvasRect.left - offsetX;
                const nextTop = event.clientY - canvasRect.top - offsetY;
                const maxLeft = canvas.clientWidth - circle.offsetWidth;
                const maxTop = canvas.clientHeight - circle.offsetHeight;
                circle.style.left = `${Math.max(0, Math.min(nextLeft, maxLeft))}px`;
                circle.style.top = `${Math.max(0, Math.min(nextTop, maxTop))}px`;
            });
        }
    </script>
</body>
</html>
Verify independent movement

Before the final check, decide whether moving one circle should affect the other.

  • Drag the blue circle towards the top-left corner.
  • Release the pointer.
  • Drag the orange circle towards the bottom-right corner.
  • Release the pointer.

You'll see each circle remain where you release it. Moving one circle leaves the other circle in its existing position.

That is the movement layer working: both endpoints now hold independent positions. Next, you'll draw the first curved connection between them.

Draw the Curved Connector

Your two circles already respond when you drag them around the canvas. The page still has no visible relationship between them.

This step turns the space between the circles into a connector. CSS properties called border-shape and shape() use the circles' positions to draw the curve.

In this step, get ready to:
  • Calculate a connector rectangle from the centres of the circles.
  • Turn the connector's border into a coloured curve.
  • Drag both circles to confirm the curve follows their changing positions.
Calculate the connector geometry

The connector occupies an invisible rectangle that spans the two circle centres. Its width changes with the horizontal gap. Its height changes with the vertical gap.

How is the rectangle measured?

  • Each circle centre supplies one endpoint for the connector.
  • The absolute horizontal gap becomes the connector width.
  • The absolute vertical gap becomes the connector height.
  • The smaller centre coordinates set the connector's starting position.
  • The relative direction tells the curve which corner should contain its starting point.

The direction check is the fiddly part. It keeps the curve bending toward the second circle when either circle crosses above the other.

  • In index.html, add a connector element inside the same canvas as the two circle elements.
  • Save index.html.
  • Refresh the page in your browser.
  • Inspect the canvas markup with the browser developer tools.

You should see the connector element beside the two circle elements in the page structure. You now have a dedicated element for the line.

  • Extend the existing drag update in index.html so it calculates the connector's position from both circle centres.
  • Save index.html.
  • Refresh the page in your browser.
  • Compare the connector's computed width and height before moving a circle.
  • Drag one circle to a different position.
  • Compare the connector's computed width and height again.

You should see the computed dimensions change after the drag. This proves the connector geometry is responding to the circles.

Connector dimensions staying the same?

  • Confirm the geometry update reads both circles after their positions change.
  • Check that the connector dimensions use the absolute difference between the two centre coordinates.
  • Make sure the drag handler calls the connector update after every pointer movement.
  • Help me debug a connector that does not resize when I drag either circle.
Draw the curved border

A regular border reveals the connector rectangle. The border-shape property redirects that border along a path created with shape().

  • Choose a line thickness for the connector: 4px.
  • Choose a line colour that contrasts with the canvas: #7c3aed.
  • Style the connector with a solid border using 4px for its thickness and #7c3aed for its colour.
  • Save index.html.
  • Refresh the page in your browser.

You should see a coloured rectangular border spanning the gap between the circles. Its dimensions should match the connector geometry you calculated.

  • Update the connector rule so border-shape follows a curved shape() path selected from the stored direction.
  • Save index.html.
  • Refresh the page in your browser.

You should see the rectangular border become a coloured curve between the circles. There it is. Your two draggable nodes now read as one connected diagram.

Why use border-shape?

The connector already has a border with a visible thickness and colour. border-shape changes the route taken by that border.

The shape() path supplies the curve. Recalculating the surrounding rectangle gives that path new dimensions whenever a circle moves.

Still seeing a rectangle?

  • Confirm the connector rule contains border-shape with a shape() value.
  • Check that the shape path has a starting point and a curved segment leading to the opposite corner.
  • Use Chrome or Microsoft Edge for the page preview.
  • Help me debug a border-shape connector that still renders as a rectangle.

✔️ Awesome, I've got everything!

Great. Double check that you saved index.html after adding the connector geometry and curved border.

ⓧ I'd like to double check the full code

Your cumulative index.html should now contain these parts:

  • The existing standalone page with its canvas and two draggable circle elements.
  • The existing pointer or drag handlers that update each circle's position.
  • A connector element inside the same canvas as the circles.
  • A geometry update that calculates the connector's position from the circle centres.
  • Connector dimensions based on the absolute horizontal and vertical gaps.
  • A direction value that selects the correct curve orientation.
  • A visible border using your selected thickness and colour.
  • A border-shape declaration containing the curved shape() path.
Test the curve while dragging

Before you test the finished connector, do you think its curve will update when either circle moves?

  • Refresh index.html in Chrome or Microsoft Edge.
  • Drag the first circle across the canvas.
  • Release the first circle in a new position.
  • Drag the second circle across the canvas.
  • Move one circle above the other circle.

You should see the coloured curve resize as each circle moves. Its bend should respond when the circles swap vertical positions.

That proves the connector is calculated from the circles' live positions. Its selected thickness and colour remain visible throughout the drag.

Your curved connector now responds to both draggable circles. Next, you will move its border onto a pseudo-element so the line meets both circle centres more precisely.

Align the Curve to Circle Centres

Your draggable circles already update a visible CSS curve in Chrome or Microsoft Edge. The border still sits on the connector's own box.

Its thickness changes the area used to place the path. This can pull the curve's endpoints away from the circle centres.

A pseudo-element will separate the visible curve from the connector geometry. You will also compare two reference boxes to see how each one positions the path.

In this step, get ready to:
  • Move the curve's border onto a connector pseudo-element.
  • Compare border-box with padding-box.
  • Verify that the curve reaches the centre of each circle.
Move the curve onto a pseudo-element

The connector already calculates the rectangle between the circles. Its new pseudo-element handles the visual border inside that rectangle.

How the pseudo-element split works

  • The content: "" declaration gives the pseudo-element an empty generated box.
  • The position: absolute declaration lets that box follow the connector's dimensions.
  • The inset: 0 declaration stretches the box across the connector.
  • The existing border declaration controls the curve's colour and thickness.
  • In the index.html file from earlier, press Cmd+F on macOS or Ctrl+F on Windows.
  • Enter border-shape in the editor's search box.
  • Identify the connector rule that currently contains the coloured border.
  • Create a :before rule for the same connector immediately after its existing rule. Use the three pseudo-element settings explained above.
  • Move the existing border declaration into the new pseudo-element rule.
  • Move the existing border-shape declaration into the same pseudo-element rule.
  • Save index.html.

Before you refresh, do you expect the curve to keep the same colour and thickness?

  • Return to the browser preview from earlier.
  • Refresh the page.
  • Drag either circle across the page.

You should still see one coloured curve following the circles. The curve now belongs to the connector's pseudo-element.

Did the curve disappear?

  • Check that the pseudo-element rule includes content: "".
  • Check that the pseudo-element rule includes position: absolute.
  • Confirm that both the border declaration and the border-shape declaration moved into the new rule.
  • Confirm that the circle elements and their drag logic remain unchanged.
  • Help me find why my connector pseudo-element is not showing its border-shape curve.
Compare the curve reference boxes

A geometry-box keyword tells shape() which edge supplies its coordinates. The correct reference keeps the path centred on the connector's endpoints.

  • Return to the border-shape declaration inside the :before rule.
  • Add border-box after the closing parenthesis of the existing shape() value.
  • Save index.html.
  • Refresh the browser preview.
  • Drag one circle diagonally away from the other circle.

You should see the curve's midpoint line begin and end at the outer corners of the connector box. Those corners correspond to the two circle centres.

What does border-box change?

The border has its own thickness around the path. The border-box reference places the middle of that path on the connector's outer corners.

This removes the half-border offset from each endpoint. The visible curve now follows the centre-to-centre calculation.

The padding-box keyword offers a second coordinate reference. A quick comparison makes the effect of the geometry box visible.

  • Replace border-box with padding-box in the pseudo-element's border-shape declaration.
  • Save index.html.
  • Refresh the browser preview.
  • Drag either circle to compare the curve's endpoints with the circle centres.

You should see the path use the pseudo-element's padding edge as its reference. This shifts the endpoint placement for the current connector box.

  • Restore border-box at the end of the border-shape declaration.
  • Save index.html.
  • Refresh the browser preview.
  • Drag either circle to confirm the centre alignment returns.

The curve should once again meet the centre of each circle. Your comparison also shows where padding-box places the same path.

Are the endpoints still offset?

  • Confirm that border-box appears after the complete shape() value.
  • Confirm that the geometry-box keyword remains inside the border-shape declaration.
  • Check that the connector's size calculation still uses the two circle positions from earlier.
  • Help me debug an offset between my border-shape curve and the circle centres.
Verify the centre alignment

The final check tests both endpoints through several connector angles. Before you drag, where do you expect each end of the curve to remain?

  • Place the circles far apart on a diagonal.
  • Drag the first circle horizontally while watching the curve's starting point.
  • Drag the second circle vertically while watching the curve's ending point.
  • Move each circle through a different diagonal position.

You should see both ends remain centred on their circles throughout the movement. The connector geometry continues updating while the pseudo-element redraws the curve.

You have the centre alignment locked in. The curve now follows the circles without the border thickness shifting either endpoint.

✔️ Awesome, I've got everything!

Your connector geometry remains intact. Its pseudo-element now draws a curve that reaches both circle centres.

ⓧ I'd like to double check the full code

Compare your index.html with this completed structure:

  • Confirm that the page still contains the two independently draggable circles.
  • Confirm that the existing drag handlers still update both circle positions.
  • Confirm that the connector geometry still updates from those positions.
  • Confirm that the connector's :before pseudo-element owns the coloured border.
  • Confirm that the pseudo-element owns the existing border-shape declaration with its shape() path.
  • Confirm that border-box remains selected after your comparison with padding-box.

Your curve now lands on both circle centres. Next, you will smooth out the jump that appears when the connector becomes very narrow or short.

Handle Close Circle Positions

Your curve now lands on the centre of each circle. Its CSS border still creates a close-range edge case.

The pseudo-element can overflow when either connector dimension becomes smaller than its border. This overflow makes the curve shift or jump as the circles nearly align.

In this step, get ready to:
  • Reproduce the visible jump by moving the circles close together.
  • Apply a negative inset to keep the border overflow consistent.
  • Hide zero-size link boxes when either dimension reaches zero.
Reproduce the close-range jump

Before you test, predict how the thick connector border will behave as the circles nearly align.

  • Switch back to the preview in Chrome or Microsoft Edge from earlier.
  • Drag either circle until both centres are almost level horizontally.
  • Repeat the drag until both centres are almost level vertically.

You will see the curve shift or jump when one connector dimension becomes very small. This is the intended shortfall that the next change fixes.

Keep the border overflow consistent

A negative inset expands the pseudo-element by the border width on every side. The overflow then stays consistent across large and small connector sizes.

  • In index.html, find the existing .link > *:before rule.
  • Merge this snippet into that rule without deleting declarations omitted from the snippet:
.link > *:before {
  inset: -12px; /* instead of inset: 0 */
  border: 12px solid #556270;
  /* we add padding-box at the end */
  border-shape: shape(from 0 0,curve to 100% 100% with 100% 0) padding-box;
}

How does this stop the jump?

  • The negative inset expands the pseudo-element by the same distance as its border width.
  • The padding-box keyword keeps the path aligned with the original connector box after that expansion.
  • The border-shape declaration continues to draw the curve between the same two points.
  • Save index.html.
  • Refresh the preview in your browser.
  • Drag either circle close to the other circle.

Good work. The curve now stays centred as its link box becomes smaller.

Does the curve still jump?

  • Confirm that inset is set to -12px inside the connector pseudo-element rule.
  • Check that the border-shape value ends with padding-box.
  • Make sure the negative inset matches the 12px connector border width.
  • Help me check why the connector still jumps at small sizes.

A link box can still reach zero width or zero height when the circle centres align. Container query units let the pseudo-element detect that exact state.

  • Add the zero-size guard after the existing .link rules in index.html by pasting this code:
.link * {
  container-type: size;
}
.link *:before {
  display: if(style(calc(sign(1cqw)*sign(1cqh)) = 0):none);
}

How does the zero-size guard work?

  • The container-type declaration lets each link box expose its dimensions through container query units.
  • The sign() calculation becomes zero when either 1cqw or 1cqh becomes zero.
  • The display declaration removes the pseudo-element during that zero-size state.
  • Save index.html.
  • Refresh the preview in your browser.
  • Drag one circle until both centres align exactly horizontally.

You will see the connector disappear cleanly at exact alignment. The curve returns as soon as both connector dimensions have a non-zero size.

Do you see a stray border shape?

  • Confirm that container-type: size applies to the link boxes measured by the pseudo-element.
  • Check that the pseudo-element selector ends with :before.
  • Compare the parentheses in the display declaration with the code block above.
  • Help me debug the zero-size connector guard.
Verify the close-range behavior

Before the final check, predict whether the curve will stay stable as a circle crosses both alignment points.

  • Drag the circles until their edges nearly touch.
  • Move one circle slowly across the horizontal alignment point.
  • Move the same circle slowly across the vertical alignment point.

You should see a stable curve while both dimensions have a size. At exact horizontal or vertical alignment, the zero-size connector hides cleanly without a visible jump.

The connector now handles close circle positions without a stray shape or sudden jump. Next, you will teach the line to change shape based on distance.

Animate the Connector Shape

Your connector stays centred on both circles as they move. However, a deep curve looks oversized when the circles sit close together.

In this step, CSS if() uses a style query to choose a straight connector below 200px. A transition: border-shape declaration animates each shape change.

In this step, get ready to:
  • Use a style query to select a straight connector at short distances.
  • Add a transition that animates changes to the connector shape.
  • Drag the circles across the threshold to verify both shapes.
Switch to a straight connector nearby

The connector already measures its changing width with container query units. The conditional value uses those measurements to choose a shape that fits the available space.

  • In the open index.html file, locate the current border-shape declaration inside the connector pseudo-element.
  • Replace that declaration with this conditional value:
  border-shape: if(
    style((100cqw < 200px) or (100cqh < 200px)):
     shape(from 0 0,curve to 50% 50% with 0   0,smooth to 100% 100%);
    else:
     shape(from 0 0,curve to 50% 50% with 50% 0,smooth to 100% 100%);)
   padding-box;

How Does the Shape Query Work?

  • The style query reads the connector width through 100cqw.
  • The style query reads the connector height through 100cqh.
  • The or condition selects the first branch when either measurement falls below 200px.
  • The first shape() uses 0 0 as its control point. This collapses the bend into a straight line.
  • The else branch restores the curved control point at larger sizes.
  • The padding-box keyword preserves the centre alignment from the previous step.
  • Save index.html.
  • Return to the browser preview from the previous step.
  • Refresh the page.

Before you drag a circle, which connector shape do you expect when either measured span drops below 200px?

  • Drag one circle close to the other circle.

You will see the connector become straight when either measured span drops below the threshold.

  • Drag one circle farther away.

You will see the curved connector return once both measured spans reach 200px.

You have the distance-sensitive switch working. The connector now chooses a shape that fits the space between the circles.

Does the Connector Stay Curved?

  • Check that both shape branches remain inside the same if() declaration.
  • Confirm that the existing size container setup from the previous step is still present.
  • Check that padding-box appears after the closing parenthesis for if().
  • Help me debug a connector that does not switch shapes.
Animate the shape change

Both branches use the same sequence of shape() commands. That shared structure gives the browser matching points to interpolate between.

  • Locate the closing padding-box line in the conditional declaration.
  • Add the transition directly below that declaration by pasting this line:
  transition: border-shape .3s;

What Does the Transition Do?

  • The border-shape value identifies the property that changes.
  • The .3s duration gives the connector time to morph between its control points.
  • Save index.html.
  • Return to the browser preview.
  • Refresh the page.

Before you move a circle across the threshold, do you expect the connector to jump or morph between its shapes?

  • Drag one circle back and forth across the shape threshold.

Below 200px, you will see the connector straighten. Above the threshold, you will see the curve return.

Each change now animates over .3s. Both ends remain attached to the circle centres throughout the movement.

Does the Shape Change Instantly?

  • Confirm that the transition sits inside the same connector pseudo-element rule as border-shape.
  • Check that both branches retain the same sequence of curve commands.
  • Compare the two control points for accidental spelling changes.
  • Help me debug a connector shape that does not animate.

✔️ Awesome, I've got everything!

Great work. Your saved index.html now switches the connector shape at the threshold.

The change animates as you drag either circle.

ⓧ I'd like to double check the full code

Compare the edited connector section in index.html with this completed CSS:

  border-shape: if(
    style((100cqw < 200px) or (100cqh < 200px)):
     shape(from 0 0,curve to 50% 50% with 0   0,smooth to 100% 100%);
    else:
     shape(from 0 0,curve to 50% 50% with 50% 0,smooth to 100% 100%);)
   padding-box;
  transition: border-shape .3s;

The completed section contains two compatible shape branches. The transition follows the conditional border-shape declaration.

Your connector now changes shape to match the available space. Next up, you will draw endpoints that follow the line as the circles move.

Add Connector Endpoints

The connector now stays centred as you drag either circle. It also changes smoothly from a curve to a straight line when the circles move within 200px of each other.

The line still ends abruptly at each circle. A second CSS pseudo-element can draw endpoint markers inside the same moving area as the connector.

In this step, get ready to:
  • Draw circular markers at both ends of the connector.
  • Add a browser compatibility note for portfolio viewers.
  • Verify that the endpoints follow the connector during dragging.
Draw circle endpoints

The new pseudo-element uses two shape() arc commands to draw the endpoint circles. Its negative inset places both markers in the connector's coordinate space.

  • In index.html, find the existing connector pseudo-element rule.
  • Paste this rule immediately below it:
.link > *:after {
  content: "";
  position: absolute;
  inset: -12px;
  border: 12px solid #556270;
  display: if(style(calc(sign(1cqw)*sign(1cqh)) = 0):none);
  /* draw two circles */
  border-shape: 
    shape(from 6px 0,arc by 0 .1px of 6px large,
      move to calc(100% + 6px) 100%,arc by 0 .1px of 6px large) 
    padding-box
}

What does this CSS do?

  • The :after pseudo-element creates a separate drawing layer for the endpoint markers.
  • The inset: -12px declaration gives the markers the same extra drawing space as the connector line.
  • The display calculation hides the markers when the connector has zero width or height.
  • The first arc command draws the marker at the start of the connector.
  • The move command changes the drawing position to the opposite corner.
  • The second arc command draws the marker at the other end.
  • Save index.html.
  • Refresh the existing browser preview.
  • Drag either circle across the page.

You should see one circular marker at each end of the connector. Both markers remain attached while the line changes position.

Don't see the endpoint circles?

  • Confirm the new :after rule sits after the existing connector pseudo-element rule.
  • Check that the selector starts with .link > *.
  • Compare the opening brace with the closing brace to confirm the rule is complete.
  • Check that both arc commands remain inside the shape() value.
  • Help me debug my connector endpoint CSS.
Add the browser note

These modern CSS features need Chrome or Edge for this demo. Firefox and Safari may render the page differently.

  • In index.html, find the closing </body> tag.
  • Paste this compatibility note immediately above it:
<!-- Explain the browser requirement to portfolio viewers. -->
<p>At the time of writing, only Chrome and Edge fully support the features we’ll use. The demos in Firefox and Safari likely won’t look right.</p>

Why Include This Note?

The note tells portfolio viewers which browsers can render the connector correctly. It also explains why the page may look different in another browser.

  • Save index.html.
  • Refresh the browser preview.

You should see the compatibility note beneath the interactive demo. Your portfolio page now explains its browser requirement to every viewer.

Don't See the Compatibility Note?

  • Confirm the paragraph appears before the closing </body> tag.
  • Check that the paragraph has an opening <p> tag.
  • Check that the paragraph has a closing </p> tag.
  • Help me find why my compatibility note is missing.
Test the finished connector

Before you test the finished page, what do you expect the endpoint circles to do when the connector changes from curved to straight?

  • Refresh the page in Chrome or Edge.
  • Drag the two circles far apart.

You should see a curved connector with one endpoint circle attached to each end.

  • Drag one circle close enough to cross the 200px threshold.

You should see the connector animate into a straight line. The endpoint circles remain attached throughout the transition.

  • Drag each circle independently through several positions.
  • Move the circles into horizontal alignment.
  • Move the circles into vertical alignment.

The line should remain centred on both circles without a small-size jump. The endpoint markers should follow every movement without detaching.

You have finished the connector. Your portfolio page now has draggable circles, animated line shapes, attached endpoints, and a visible browser compatibility note.

Secret mission

Stress-Test Your Connector

Your connector works during normal drags. Can it stay stable when the circles cross paths, line up exactly, or move through the curved-to-straight threshold? Run a visual stress test and capture evidence that each edge case works.

Clean Up Your Resources

Clean Up Your Resources

The page runs entirely on your computer with no ongoing costs. Your cleanup choice is to keep, pause, or delete the local project.

Resources you used:

  • The index.html file currently open in your code editor contains the draggable circles, connector styles, drag behavior, endpoints, and compatibility note.
  • The local browser preview tab in Chrome or Microsoft Edge shows the finished page.

Keep everything running

No action needed. Choose this if you want to keep using or extending the draggable-circle demo.

  • Keep index.html in its current location.
  • Keep the Chrome-or-Edge compatibility note in the page.
  • Reopen index.html in Chrome or Microsoft Edge whenever you want to use the demo.

Pause - I'll come back to this later

Pausing means closing the browser preview while keeping index.html available for later.

  • Close the browser tab that shows the finished page.
  • Leave index.html open in the code editor from earlier.
  • Reopen index.html in Chrome or Microsoft Edge when you are ready to continue.

Delete - I don't want to use this again

Deleting index.html permanently removes the finished page from your computer. A separate copy preserves the work if you may want it again.

  • Save a copy of index.html elsewhere if you want a backup.
  • Close the browser tab that shows the finished page.
  • Return to the code editor from earlier.
  • Locate index.html in the editor's file list.
  • Use the editor's file deletion control to remove index.html.
  • Confirm the deletion if your editor asks.
  • Check the editor's file list.

You should no longer see index.html in the file list. Your local project is now removed.

Nice Work!

Nice Work!

You did it! Your finished portfolio page keeps a modern CSS connector attached to two draggable circles while they move.

You've learned how to:

  • Build an interactive canvas where draggable circles update a centre-aligned connector as they move.
  • Draw a stable curved line with border-shape, shape(), border-box, padding-box, negative inset handling, and zero-size hiding.
  • Create a distance-aware connector that straightens under 200px, animates between shapes, and keeps circle or arrow endpoints attached.
  • Complete an optional Secret Mission to extend the connector with an extra professional touch.

Ready to quiz yourself?