Nº 433 of 520
Swinging kraft hang tag
A kraft-paper price tag on a green string that swings when you brush past it, with a struck price, a sale sticker, size chips, a hold button and a real EAN barcode.
Preview
Use it
Free to use · credit appreciated1 Once, in your project
npx skills add SusanAcharya/Design-Lounge 2 Then ask
Using the Design Lounge skill, build "Swinging kraft hang tag" (piece card-retail-price-tag) in my stack. Match its brief; restyle it onto my product's theme.
No skill? Paste the brief instead. It carries the numbers, tokens and behaviour.
<!-- Design Lounge Nº 433 · "Swinging kraft hang tag" · www.designlounge.live -->
# Swinging kraft hang tag
> **Build brief for a coding agent.** Rebuild this piece in the reader's stack. If they haven't said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don't "improve" them.
## What it is
A product card dressed as the physical swing tag on a garment, for a store's markdown page or an in-store kiosk. A brass hook sits at the top of the frame; a loden-green string drops to the grommet of a kraft tag with clipped top corners. The tag carries the brand (Pike & Ollerton, est. 1931), the product (Brushed cotton overshirt), the old price struck with a red diagonal, the sale price in a 68px Didone, a round "−35% OFF" sticker, five size chips (XL sold out), a "Hold size M · 24 h" button, and a perforated stub with a real EAN-13 barcode. Brush the pointer across the tag and it swings like a pendulum in the direction you pushed, then settles. The detail worth copying is that the swing is a damped spring driven by the pointer's horizontal velocity at entry, not a canned keyframe, so it feels pushed rather than animated.
Not `receipt-slip` (a till receipt) and not `card-product-quick-add` (a grid product card). This is one item's tag.
## Reference behaviour
1. First frame: tag hangs at the rest angle of −3° (bottom slightly right). 500ms after load it gets one small push and swings, then settles within about 3s. Left column: "Store 14 · Rack C · Menswear", "Autumn markdown" (76px italic Didone), a two-line note, an "Ends Sunday, 11 October" pill with a red dot, and a hint "Brush past the tag to swing it." (hidden on touch-only devices).
2. Brush: when the pointer enters the swing element, take the pointer's horizontal speed from the last move (px/ms). Impulse = `(0.35 + min(1, |vx| / 1.2) × 0.9) × 0.045` deg/ms, signed so the tag's bottom moves the same way as the pointer. If there was no movement, push away from the side of entry.
3. Pendulum: angular acceleration = `−(a − rest) × 0.00005 − v × 0.0026` per ms². That gives a period of about 890ms and settles from an 8° swing in about 3s. Velocity is clamped to ±0.07 deg/ms (about ±10°). The loop stops when `|a − rest| < .02°` and `|v| < .0005`.
4. Hovering does not keep it swinging; only entering does. The chips stay clickable because the swing decays quickly.
5. Sizes: XS (2), S (6), M (3, selected), L (1), XL (0, sold out). Clicking a chip selects it, nudges the tag (±2.2°), and updates the stock line: "Only 3 left in M", "6 in S on this rack", "Last one in L on this rack" (bold with a red dot). Sold-out XL is dashed, struck, and cannot be selected.
6. Arrow keys in the size group move the selection and skip sold-out sizes, wrapping at the ends.
7. Hold: the button reads "Hold size M · 24 h" with a bookmark icon. Pressing it fills it ink, swaps to a check, and reads "Held till 5 Oct, 6 pm"; the stock line says "Size M is held at the fitting rooms until tomorrow, 6 pm". Pressing again releases. Changing size while held releases the hold quietly and relabels the button.
8. Keyboard: the tag itself is focusable. Focus gives a small nudge (±2°). Space or Enter on the tag gives a bigger one (±5°). Focus shows an inset ink ring inside the clipped shape.
9. Reduced motion: the tag stays at −3°, never swings; everything else works.
## Structure
```
1280 × 800, wall #e3ded2 with 47px pinstripes at 3.5% and a soft light upper right
┌──────────────────────────────────────────────────────────────────────────────┐
│ ║ brass rail 14 × 58 │
│ STORE 14 · RACK C · MENSWEAR ╲╱ green string 96px tall │
│ Autumn ╱──────────╲ │
│ markdown (76px italic) │ ◎ │ grommet 26px │
│ Green-string tags are already… │ est. 1931 │ │
│ [● Ends Sunday, 11 October] │PIKE & OLLERTON │
│ Brush past the tag to swing it. │────────────│ │
│ │Brushed cotton overshirt │
│ │Style 4471-LD · Loden · … │
│ col 1fr gap 56px │$1̶4̶8̶.̶0̶0̶ (−35%)│ │
│ │$96.00 (68px red) sticker 68 │
… 216 more lines. Copy or download for the full brief. Loading the source…Brief
256 linesBuild brief for a coding agent. Rebuild this piece in the reader’s stack. If they haven’t said which stack, ask once, then default to semantic HTML + CSS + a little vanilla JS. Match the numbers below; don’t “improve” them.
What it is
A product card dressed as the physical swing tag on a garment, for a store’s markdown page or an in-store kiosk. A brass hook sits at the top of the frame; a loden-green string drops to the grommet of a kraft tag with clipped top corners. The tag carries the brand (Pike & Ollerton, est. 1931), the product (Brushed cotton overshirt), the old price struck with a red diagonal, the sale price in a 68px Didone, a round “−35% OFF” sticker, five size chips (XL sold out), a “Hold size M · 24 h” button, and a perforated stub with a real EAN-13 barcode. Brush the pointer across the tag and it swings like a pendulum in the direction you pushed, then settles. The detail worth copying is that the swing is a damped spring driven by the pointer’s horizontal velocity at entry, not a canned keyframe, so it feels pushed rather than animated.
Not receipt-slip (a till receipt) and not card-product-quick-add (a grid product card). This is one item’s tag.
Reference behaviour
- First frame: tag hangs at the rest angle of −3° (bottom slightly right). 500ms after load it gets one small push and swings, then settles within about 3s. Left column: “Store 14 · Rack C · Menswear”, “Autumn markdown” (76px italic Didone), a two-line note, an “Ends Sunday, 11 October” pill with a red dot, and a hint “Brush past the tag to swing it.” (hidden on touch-only devices).
- Brush: when the pointer enters the swing element, take the pointer’s horizontal speed from the last move (px/ms). Impulse =
(0.35 + min(1, |vx| / 1.2) × 0.9) × 0.045deg/ms, signed so the tag’s bottom moves the same way as the pointer. If there was no movement, push away from the side of entry. - Pendulum: angular acceleration =
−(a − rest) × 0.00005 − v × 0.0026per ms². That gives a period of about 890ms and settles from an 8° swing in about 3s. Velocity is clamped to ±0.07 deg/ms (about ±10°). The loop stops when|a − rest| < .02°and|v| < .0005. - Hovering does not keep it swinging; only entering does. The chips stay clickable because the swing decays quickly.
- Sizes: XS (2), S (6), M (3, selected), L (1), XL (0, sold out). Clicking a chip selects it, nudges the tag (±2.2°), and updates the stock line: “Only 3 left in M”, “6 in S on this rack”, “Last one in L on this rack” (bold with a red dot). Sold-out XL is dashed, struck, and cannot be selected.
- Arrow keys in the size group move the selection and skip sold-out sizes, wrapping at the ends.
- Hold: the button reads “Hold size M · 24 h” with a bookmark icon. Pressing it fills it ink, swaps to a check, and reads “Held till 5 Oct, 6 pm”; the stock line says “Size M is held at the fitting rooms until tomorrow, 6 pm”. Pressing again releases. Changing size while held releases the hold quietly and relabels the button.
- Keyboard: the tag itself is focusable. Focus gives a small nudge (±2°). Space or Enter on the tag gives a bigger one (±5°). Focus shows an inset ink ring inside the clipped shape.
- Reduced motion: the tag stays at −3°, never swings; everything else works.
Structure
1280 × 800, wall #e3ded2 with 47px pinstripes at 3.5% and a soft light upper right
┌──────────────────────────────────────────────────────────────────────────────┐
│ ║ brass rail 14 × 58 │
│ STORE 14 · RACK C · MENSWEAR ╲╱ green string 96px tall │
│ Autumn ╱──────────╲ │
│ markdown (76px italic) │ ◎ │ grommet 26px │
│ Green-string tags are already… │ est. 1931 │ │
│ [● Ends Sunday, 11 October] │PIKE & OLLERTON │
│ Brush past the tag to swing it. │────────────│ │
│ │Brushed cotton overshirt │
│ │Style 4471-LD · Loden · … │
│ col 1fr gap 56px │$1̶4̶8̶.̶0̶0̶ (−35%)│ │
│ │$96.00 (68px red) sticker 68 │
│ │SIZE [XS][S][■M][L][X̸L̸] │
│ │Only 3 left in M │
│ │[ ⌑ HOLD SIZE M · 24 H ] │
│ │- - - - - - - - - - - - - - - - │ perforation
│ │||||||||||||| Sale ref. │
│ │5 012345 447186 MD-26-35 │
│ └────────────────┘ tag 330px wide│
└──────────────────────────────────────────────────────────────────────────────┘
main.wrap: gridminmax(0,1fr) minmax(0,420px), gap 56px, max 1040px, full height.- Left:
section.notelabelled by theh1. - Right:
.rack(relative, full height) with.rail(decorative) and.swing: absolute, top 66px, centred, 330px wide,transform-origin: 50% 0,rotate: var(--a). Inside: the string SVG (two cubic strands from the hook to the grommet), then a shadow wrapper, thenarticle.tag[tabindex=0]labelled by the product name and described by the price block. - Tag:
clip-path: polygon(18% 0, 82% 0, 100% 7%, 100% 100%, 0 100%, 0 7%), padding 58px 28px 0. Contents in order: grommetspan, brandp, hairline,h2product, metap,.prices(was, now, sticker),fieldset.sizeswithlegendand adiv[role=radiogroup]of fivebutton[role=radio], stockp[aria-live=polite], holdbutton[aria-pressed],.stubwith the barcode SVG (role=img) and the sale reference. - The shadow lives on a wrapper
divaround the tag, becauseclip-pathclips the tag’s ownfilter.
Tokens
:root {
--wall: #e3ded2; /* page */
--kraft-3: #d8b88f; /* tag top */
--kraft: #c9a47a; /* tag middle */
--kraft-2: #c29a6c; /* tag bottom */
--ink: #2a2017; /* text, barcode, selected chip, held button */
--ink-2: #3f3022; /* brand line, legend, stock, sku */
--ink-3: #4a3a2a; /* meta, struck price, eyebrow */
--sale: #8f2010; /* sale price, strike line, sticker, low-stock dot, ends dot */
--string: #4f6b3a; /* loden-green string */
--brass: #a8843f; /* rail and hook */
--focus: #2a2017;
--serif: "Bodoni Moda", Didot, Georgia, serif; /* opsz 6–96 */
--mono: "Courier Prime", ui-monospace, monospace;
--tw: 330px; /* tag width */
--rest: -3deg;
--k: 0.00005; /* spring, per ms² */
--damp: 0.0026; /* per ms */
--vmax: 0.07; /* deg per ms */
--ease: cubic-bezier(.2, .7, .2, 1);
--expo: cubic-bezier(.16, 1, .3, 1);
--t-micro: 150ms;
}
Kraft surface, top to bottom of the stack:
background:
radial-gradient(circle at 20% 30%, rgba(255,255,255,.12), transparent 40%),
repeating-linear-gradient(17deg, rgba(90,60,30,.05) 0 1px, transparent 1px 4px),
repeating-linear-gradient(-71deg, rgba(255,240,210,.06) 0 1px, transparent 1px 5px),
linear-gradient(170deg, var(--kraft-3), var(--kraft) 40%, var(--kraft-2));
Shadow wrapper: drop-shadow(0 22px 22px rgba(42,32,23,.2)) drop-shadow(0 2px 2px rgba(42,32,23,.15)). Grommet: 26px circle filled with the wall colour, ring 0 0 0 5px #d9c9a8, outer hairline 0 0 0 6px rgba(42,32,23,.3), inner shade inset 0 2px 3px rgba(42,32,23,.35).
Typography
| Role | Family | Size | Weight | Line-height | Tracking | Case |
|---|---|---|---|---|---|---|
| Eyebrow | Courier Prime | 12px | 400 | 1.5 | 0.18em | UPPER |
| Headline | Bodoni Moda italic, opsz 96 | 76px | 500 | 0.95 | -0.02em | Title |
| Note body | Courier Prime | 14px | 400 | 1.6 | 0 | sentence, 36ch |
| Brand est. | Bodoni Moda italic | 13px | 500 | 1 | 0.02em | lower |
| Brand name | Bodoni Moda | 22px | 700 | 1 | 0.14em | UPPER |
| Product | Bodoni Moda | 24px | 500 | 1.1 | -0.005em | Sentence |
| Meta | Courier Prime | 12px | 400 | 1.5 | 0.04em | as written |
| Was price | Courier Prime | 16px | 400 | 1.5 | 0 | — |
| Sale price | Bodoni Moda, opsz 96 | 68px ($ 30px, .00 26px superscript) | 700 | 0.9 | -0.02em | lining |
| Sticker | Courier Prime | 20px (OFF 10px, 0.12em) | 700 | 0.9 | 0 | UPPER |
| Legend | Courier Prime | 11px | 400 | 1 | 0.16em | UPPER |
| Chip, hold | Courier Prime | 13px | 700 | 1 | 0 / 0.08em | UPPER |
| Barcode digits | Courier Prime | 10px | 400 | 1.5 | 0.2em | — |
| Sale ref. | Courier Prime | 15px 700 over 10px labels | 1.2 | 0.02em | UPPER |
Motion
| Element | Trigger | Property | From → To | Duration | Easing | Reduced motion |
|---|---|---|---|---|---|---|
.swing | pointer enters | rotate via --a | rest → damped swing, up to ~10° | ~3s to settle | spring (k .00005, c .0026) | stays −3° |
.swing | load + 500ms | rotate | one push of .03 deg/ms (~4°) | ~3s | spring | none |
.swing | chip pick | rotate | ±2.2° impulse | ~2s | spring | none |
.swing | hold toggle | rotate | ±3° (hold) / ±1.5° (release) | ~2s | spring | none |
.swing | tag focus / Space | rotate | ±2° / ±5° | ~2–3s | spring | none |
| Chip, hold | hover / select | background, colour | — | 150ms | --ease | instant |
The string rotates with the tag because it is inside .swing; the rail does not move. Use the individual rotate property, not transform, so nothing else on the element fights it.
States
- Rest: −3°, still.
- Swinging: angle written every animation frame until it settles; the loop then stops entirely.
- Chip resting: 40px tall, 1.5px border at 45% ink, 3px radius. Hover: 8% ink fill. Selected: ink fill,
--kraft-3text,aria-checked="true",tabindex=0. - Sold out: dashed border, 42% ink text, a 1.5px diagonal strike at −24°,
aria-disabled="true", not selectable by click or arrows. - Low stock (1 left): stock line bold ink with an 8px red dot.
- Hold: 44px tall, 1.5px ink border. Hover: 8% ink fill. Held: ink fill, kraft text, check icon,
aria-pressed="true". - Tag focus-visible:
box-shadow: inset 0 0 0 3px ink, inset 0 0 0 5px kraft-3(an outline would be clipped). - Other focus-visible: 2px ink outline, 3px offset.
Accessibility
- The tag is an
articlelabelled by the product name and described by the price block, so it reads “Brushed cotton overshirt, Was $148.00 Now $96.00”. - The struck price has a visually hidden “Was” and the sale price a hidden “Now”; the decorative
$and sticker arearia-hiddenand a hidden$is spoken instead. - Sizes are a
radiogroupinside afieldsetwith a legend. Each radio is labelled with its stock: “M, 3 left”, “XL, sold out”. Rovingtabindex; arrows move and skip sold-out sizes. - Stock and hold messages share a polite live region.
- The barcode SVG is
role="img"with the number as its label; the digits are also printed. - The swing is decoration. No information depends on it, and it never runs under reduced motion.
- Contrast on kraft
#c9a47a: ink 6.9:1,--ink-25.5:1 (4.9:1 on the darker bottom#c29a6c),--ink-34.7:1 (used only in the lighter upper half). Sale red is used for the 68px price (3.8:1, large text) and as fills, never for small text. Sticker text#fbeee3on#8f20107.7:1. - Hit targets: chips 40px tall and at least 46px wide; hold 44px.
Responsive rules
- ≥ 1280: two columns, tag 330px, swing anchor 66px from the top. Tag bottom sits near y 760 at rest.
- 901–1279: same layout; the note column narrows first.
- ≤ 900: one column. Note centred on top (headline 48px), then the rack with
min-height: 820px; the page scrolls vertically. - ≤ 420: tag 300px, padding 54px 22px 0, sale price 58px, headline 40px, note body hidden.
.rackgetsoverflow-x: clipso the rotated tag never creates sideways scroll. Do not clip at wider sizes or the swing gets cut. - Touch: the hint hides under
(hover: none). A tap still triggerspointerenterand gives a push.
Acceptance checklist
Always
- The swing is a damped spring integrated per frame, pushed by pointer velocity on entry, settling and stopping on its own.
- Rotation origin is the top of the string, and the string rotates with the tag.
- The tag shape is a clip-path with clipped top corners and a grommet hole showing the wall colour.
- The shadow is on a wrapper, not on the clipped element.
- Old price is struck by a diagonal line, not just
text-decoration, and the sale price dominates. - Size chips are a radiogroup with roving tabindex; sold-out sizes are visible, struck, and skipped.
- The barcode is real bars generated from the digits, with the digits printed below.
- Tag focus ring is visible inside the clip shape.
- Reduced motion: no swing at all, everything else intact.
- No horizontal scroll at 375px.
This demo
- Pike & Ollerton, est. 1931; Brushed cotton overshirt, Style 4471-LD, Loden, 100% cotton twill.
- Was $148.00, now $96.00, sticker −35% OFF.
- Sizes XS 2, S 6, M 3 (selected), L 1, XL sold out.
- Barcode 5 012345 447186; sale ref. MD-26-35, Rack C · 14.
- Wall
#e3ded2, kraft#c9a47a, sale#8f2010, string#4f6b3a. - Bodoni Moda for brand, product and prices; Courier Prime for everything typed.
Implementation notes
The pendulum. A semi-implicit Euler step per frame, clamped dt, and a stop condition so it doesn’t run forever:
let a = -3, v = 0, raf = 0;
const REST = -3, K = 0.00005, DAMP = 0.0026;
function run() {
let last = performance.now();
const tick = t => {
const dt = Math.min(32, t - last); last = t;
v += (-(a - REST) * K - v * DAMP) * dt;
a += v * dt;
swing.style.setProperty('--a', a.toFixed(3) + 'deg');
if (Math.abs(a - REST) < .02 && Math.abs(v) < .0005) { a = REST; raf = 0; return; }
raf = requestAnimationFrame(tick);
};
raf = requestAnimationFrame(tick);
}
function kick(dv) { if (reduce) return; v = Math.max(-.07, Math.min(.07, v + dv)); if (!raf) run(); }
Sign: CSS rotate is clockwise-positive, and with the origin at the top a positive angle moves the bottom to the left. A pointer moving right must give a negative impulse: kick(-Math.sign(vx) * magnitude).
EAN-13 bars from the digits. The first digit chooses the L/G parity of the left six; the right six use R codes:
const L = ['0001101','0011001','0010011','0111101','0100011','0110001','0101111','0111011','0110111','0001011'];
const G = ['0100111','0110011','0011011','0100001','0011101','0111001','0000101','0010001','0001001','0010111'];
const R = L.map(p => [...p].map(c => c === '0' ? '1' : '0').join(''));
const PAR = ['LLLLLL','LLGLGG','LLGGLG','LLGGGL','LGLLGG','LGGLLG','LGGGLL','LGLGLG','LGLGGL','LGGLGL'];
const d = '5012345447186'.split('').map(Number);
let bits = '101';
d.slice(1, 7).forEach((n, k) => bits += (PAR[d[0]][k] === 'L' ? L : G)[n]);
bits += '01010';
d.slice(7).forEach(n => bits += R[n]);
bits += '101'; // 95 modules: one 1-unit rect per '1', guards 48 tall, others 42
Perforated stub. A 2px dashed top border plus two wall-coloured 14px circles half off each edge:
.stub { position: relative; margin: 22px -28px 0; padding: 16px 28px 18px; border-top: 2px dashed rgba(42,32,23,.4); }
.stub::before, .stub::after { content: ""; position: absolute; top: -8px; width: 14px; height: 14px; border-radius: 50%; background: var(--wall); }
.stub::before { left: -7px; } .stub::after { right: -7px; }
Common mistakes:
- A CSS
@keyframeswobble on hover. It loops while you try to click a chip, and it ignores direction. - Putting
filter: drop-shadowon the clipped tag; the shadow is clipped away. - A white string on a pale wall. It disappears; the string is the colour cue (“green-string tags”).
- Small text in sale red on kraft (2.8–3.8:1). Red is for the big price and for fills.
outlinefor the tag’s focus state;clip-pathcuts it off.overflow: hiddenon the rack at desktop widths, which slices the tag mid-swing.- A fake barcode of random stripes. Generate it from the printed number.
Rebuild order:
- Two-column layout, wall texture, rail and hook.
- Tag shape, kraft surface, grommet, shadow wrapper, and the string.
- Content: brand, product, prices with strike and sticker.
- Size radiogroup with stock line; hold toggle.
- Barcode and stub.
- Pendulum, pointer impulse, nudges, focus ring.
- Reduced motion and the narrow layouts.
Details
- Palette
- Type
- Bodoni Moda · Courier Prime
- Motion
- Rich motion
- Build
- One session
- Tags
On the shelves