Preskočiť na obsah

Widget · embed.js

Vittles widget

The venue's live menu and ordering on its own website, with one container and one script. The widget is the same page as the hosted menu, rendered in an iframe from Vittles: your site never talks to a database, never holds a key, and never sees the guest's details. No cookies, no analytics.

API docsSnippetAttributesColours and fontsLayout and brandingStyling hooksEventsContent Security PolicyWithout a script (iframe)Troubleshooting

Snippet

Paste this where the menu should appear. The container becomes the frame; the script finds every [data-vittles-venue] on the page, so several venues on one page are several containers. The script is loaded once — a second tag only rescans the page.

<div data-vittles-venue="pizzeria-roma"></div>
<script async src="https://vittles.eu/embed.js"></script>

Before it works: the venue owner adds your site's origin (for example https://www.pizzeria-roma.sk) under Channels → Widget → Allowed websites in Vittles. Until then the frame shows an instruction page naming the exact origin to add. There is nothing to configure on your side beyond the snippet.

Keys and the widget are part of the Ultra plan; on a lower plan the API answers plan_required (403) and the embed shows a notice with a link to the hosted menu — the frame switches back to the menu on its own once the plan is restored.

Single-page app? Containers inserted after load are mounted with window.vittles.mount(element). Mounting the same element twice is a no-op.

Attributes

Everything is optional except the venue. Each attribute becomes a query parameter of the frame's address; what the owner switched off in Vittles, an attribute cannot switch on.

AttributeValuesDefaultWhat it does
data-vittles-venuerequiredslug—The venue. The slug from /menu/{slug}.
data-modeauto | menuautomenu shows the menu without a cart. auto follows the venue's ordering state; there is no order — what the owner switched off, the snippet cannot switch on.
data-tabletable label—Table from a QR code on the table. Drawn only when the venue offers table service and the table signature (data-k) is valid.
data-table-paramquery parameter name—Read the table label from the host page's own URL, e.g. data-table-param="table" with ?table=12.
data-ktable signature—Signature of the table QR code (the k= value of a code from Channels). Without a valid one the frame does not offer table service.
data-k-paramquery parameter name—Read the table signature from the host page's own URL, e.g. data-k-param="k" with ?table=12&k=….
data-langsk | envenue languageFixed language; the language switcher inside the frame is hidden.
data-fontdefault | humanist | serif | slabvenue fontOne of the venue font families. The host page's own web font never reaches the frame — that is the price of an iframe.
data-primary#rrggbbvenue coloursBrand colour for buttons. Also readable as CSS --vittles-primary on the container.
data-background#rrggbbvenue coloursPage background. Also --vittles-background.
data-text#rrggbbvenue coloursText colour. Also --vittles-text.
data-surface#rrggbbderivedCard and field background. Also --vittles-surface. Optional.
data-headertrue | falsetruefalse hides the venue name, subtitle, logo and cover image. Opening hours and the ordering notice stay.
data-layoutlist | gridvenue settingItems as a list, or as tiles (tiles need photos on).
data-photosnone | small | largevenue settingItem photos: hidden, small beside the text, or large above it.
data-cornerssquare | soft | roundvenue settingCorner rounding of photos, tiles and the cover image.
data-densityroomy | compactvenue settingSpacing between items. Text size never changes.
data-aligncenter | leftvenue settingHeader alignment — left for sites with a left-aligned layout.
data-widthfill | wide | readingvenue layout, otherwise fillMenu width inside the frame: fill uses the whole frame, wide up to 72rem, reading a single comfortable column.
data-columnsauto | 1 | 2 | 3autoAt most this many columns. Only narrows: below the cap the venue's menu layout decides, and a narrow frame stacks columns anyway.
data-min-heightpx480Frame height before the first measurement. The snippet generator writes the last measured height on the container (style="min-height:…"), so the page does not jump.
data-preview——Owner preview: no guest ticket, no Send button, a small “Preview” badge.
data-couponcoupon code—Pre-fills and applies a coupon code (3–32 letters, digits, _ or -). Validity is decided by Vittles, not by the snippet.
data-motionauto | offautooff turns off the venue's item entrance, hover and cover parallax. Guests who ask their device for reduced motion never get them anyway.
data-coupon-paramquery parameter name—Read the coupon code from the host page's own URL, e.g. data-coupon-param="c" with ?c=SUMMER10. data-coupon wins when both are set.
<div data-vittles-venue="pizzeria-roma"
     data-lang="en"
     data-mode="auto"
     data-header="false"
     data-layout="grid"
     data-photos="large"
     style="--vittles-primary:#b3261e; --vittles-background:#ffffff; --vittles-text:#1b1b1b"></div>
<script async src="https://vittles.eu/embed.js"></script>

Coupons. data-coupon="SUMMER10" pre-fills the code in the cart and applies it; data-coupon-param="c" takes it from your own page's URL instead (?c=SUMMER10), so one snippet serves every campaign link. The guest sees the discount before sending; whether the code is valid is decided by Vittles at pricing and again at intake — an invalid code shows a sentence next to the field and the guest can send without it. The coupon field itself appears when the owner has active coupons (or switched it on explicitly).

Colours and fonts

Set --vittles-primary, --vittles-background, --vittles-text and optionally --vittles-surface as CSS custom properties on the container (or the matching data-* attributes) as #rrggbb. All three of primary, background and text are needed; the surface is derived when missing. The frame re-reads them when the visitor's colour scheme changes, so a @media (prefers-color-scheme: dark) rule on your side carries over.

Colours pass the same contrast gate as the owner's own settings (WCAG 4.5:1 for text, 3:1 for the brand colour on the background). A combination that fails is ignored, the venue's colours stay, and a console.warn in the frame says why. Fonts are one of the venue font families (default, humanist, serif, slab); your own web font does not reach the frame.

Width, columns and loading

The widget fills the width of its container. data-width sets how wide the menu itself may grow inside it: fill (the whole frame), wide (up to 72rem) or reading (one comfortable column). Without it the frame uses the owner's layout from the menu editor; a venue that has not published one gets the full width with categories in up to three balanced columns. Columns follow the width of the frame, not of the browser window: a narrow container gets one column in the original order, a wide one up to three, and the reading order stays top to bottom, column by column.

The frame is created when its container comes within 800 px of the viewport, so a menu at the bottom of a long page costs nothing until the visitor scrolls towards it. To keep your page from jumping, give the container the height the menu last had, for example style="min-height:2700px" (the snippet generator in Channels writes it for you); the script releases it after the first measurement. Height changes grow first and shrink once the content has settled, at most once per frame.

Layout and branding

The frame follows the owner's menu design in Vittles (Menu design in the owner app): a ready-made look, sections of columns or tiles, the header with logo and cover image, colours, fonts, dish photos, backgrounds and motion. Set nothing and the widget looks like the hosted menu, minus what does not belong in a frame (below). To match your site, set data-width, data-photos, data-font and the colours; an unknown value is ignored and the owner's choice stays. In a published design data-photos changes only the photo mode the blocks inherit, never a gallery, the intro or a poster the owner chose on purpose. data-layout, data-corners, data-density and data-align still shape a venue that has not published a design yet. data-header="false" hides the name, subtitle, logo and cover image when your page already has its own.

The logo and cover image are uploaded by the owner and served by Vittles over https. Fonts are system font families, never downloaded from a third party, so the widget sends your visitors to no one else.

Inside the frame nothing is sticky or fixed and nothing is as tall as the viewport: the category bar and the filters scroll with the menu, a live background runs only behind the intro (a full-page one becomes a still poster), and details open in place while the frame grows first and shrinks once the content has settled. Every combination passed the same contrast gate as the colours; your colour attributes replace the venue's, and a background, glass or poster that would not stay readable on your colours falls back to a plain one. Animations are transform and opacity only and never run for visitors who ask for reduced motion; data-motion="off" turns them off for everyone on your page. Guest sounds play in the widget only when the owner allowed them there, and never without a tap.

Dishes and categories can have several photos that guests swipe through. When the owner turns on automatic photo transitions, at most one gallery moves at a time, only while most of it is on screen, and it stops after three rounds; hovering pauses it, a tap or focus stops it, and every running gallery has a pause button. data-motion="off" keeps every gallery still on your page; guests can still swipe through the photos.

The language switch sits at the top centre of the header. On days with a published daily menu a Today's daily menu button jumps to it; that jump and the category bar ask your page to scroll (the same vittles:scroll message the cart uses — the frame has no scrollbar of its own), smoothly unless the visitor asked for reduced motion.

Styling hooks

Your page cannot style the inside of the frame, and that is what keeps it safe. Beyond the attributes, the owner can add a custom stylesheet for the menu in the menu design (Advanced), or ask us to. It uses these stable hooks, which will not be renamed; anything else in the markup may change.

The stylesheet can recolour, space, round and shadow these hooks, and nudge them with a bounded transform: rotate() up to ±15deg, scale() from 0.8 to 1.2 and translate() up to ±24px or ±10%, plus transform-origin. Categories can sit in up to 3 columns: display: grid and grid-template-columns: repeat(2, minmax(18rem, 1fr)) on .vm-kategorie (or simply the look editor's column option). Vittles rewrites the value so the menu never scrolls sideways and phones always get one column. Allergens, the “sold out” note and the footer always stay upright and visible.

Classes

.vm-koren .vm-hlavicka .vm-hlavicka__banner .vm-hlavicka__logo .vm-hlavicka__nazov .vm-hlavicka__podtitul .vm-obsah .vm-kategorie .vm-kategoria .vm-kategoria__nazov .vm-kategoria__fotka .vm-zoznam .vm-polozka .vm-polozka__fotka .vm-polozka__hlava .vm-polozka__nazov .vm-polozka__cena .vm-polozka__popis .vm-hlavicka__jazyk .vm-denne-skok .vm-lista .vm-lista__odkaz .vm-polozka__varianty .vm-vyber__spustac .vm-bublina .vm-bublina__cena .vm-denne .vm-denne__nazov .vm-denne__poznamka .vm-sekcia .vm-sekcia--stlpce .vm-sekcia--dlazdice .vm-stlpec .vm-dlazdica .vm-blok .vm-blok--kategoria .vm-blok--ostatne .vm-blok--denne .vm-blok--denne_suhrn .vm-blok--vyber .vm-blok--oblubene .vm-blok--uvod .vm-blok--oznam .vm-blok--info .vm-blok--rozcestnik .vm-blok--stoh .vm-blok--galeria .vm-blok--pribeh .vm-blok__hlava .vm-blok__nazov .vm-vyber .vm-oblubene .vm-stoh .vm-galeria .vm-oznam .vm-info .vm-uvod .vm-pribeh .vm-rozcestnik .vm-filtre .vm-stitok .vm-polozka__stitky .vm-fotky .vm-fotky__pas .vm-fotky__snimka .vm-fotky__bodky .vm-fotky__bodka .vm-fotky__pauza .vm-fotky-pocet

State on .vm-koren

AttributeValues
data-rozlozeniezoznam | dlazdice
data-foto-rozlozenievlavo | vpravo | hore | skryte
data-foto-tvarsquircle | zaoblene | kruh | stvorec
data-foto-velkostmale | stredne | velke
data-fotkyskryte | male | velke
data-hustotavzdusne | kompaktne
data-zaoblenieostre | mierne | oble
data-hlavickastred | vlavo
data-bannernizka | stredna | vysoka
data-hlasv1 | ziadny | klasicky | tichy | makky | zivy | svizny | hravy | filmovy | elegantny | pruzny | vlna
data-kontextstranka | vlozenie
data-sirkacitatelna | siroka | plna
data-podobav2
data-pozadieprechod | obrazok | vzor
data-vzorbodky | mriezka | vlny
data-stlpcedva | tri
data-stlpecuzka | siroka
data-kat-karta · data-cislovanieano
data-kat-menovkastitok
data-kartyobrys | tien | sklo
data-cenastitok
data-variantystitky
data-logomale | velke
data-logo-tvarzaobleny | kruh
data-jazyktext
data-denneobrys | skryte
data-vstupziadny | prelinanie | posun | zvacsenie
data-hoverzdvih | priblizenie
data-listaplna | priesvitna
data-intenzitajemna | silna
data-banner-text · data-paralaxa · data-prekrytie · data-postupne · data-zvyraznenieano
data-stlacenienie

data-vlozenie marks the embedded frame. The menu root has id="vm-listok"and the custom stylesheet is scoped to it, so its rules win over the built-in ones without !important. Sections (.vm-sekcia), columns (.vm-stlpec), tiles (.vm-dlazdica) and blocks (.vm-blok, .vm-blok--kategoria, .vm-blok-<id> for one block) are stable hooks too; the header carries its options as data-hl-* attributes on .vm-hlavicka. On the root, the stylesheet may set --vt-akcent, --vt-povrch, --vt-bg, --vt-ink, --vm-povrch-bloku (colours as #rrggbb) and --vm-r, --vm-medzera, --vm-okraj (0 to 64 px or rem).

CSS variables

VariableWhat it carries
--vt-bgpage background
--vt-povrchcard and field background
--vt-inktext
--vt-ink-2secondary text (descriptions, allergens)
--vt-okrajhairlines between items
--vt-okraj-silnystronger lines (category headings, dotted leaders)
--vt-akcentbuttons and badges — the accent colour, or the brand colour when the venue has no accent
--vt-akcent-inktext on buttons and badges
--vt-akcent-textbrand colour used as text (links, totals)
--vt-font-uithe venue font family

Style variables (--vm-*)

VariableWhat it carries
--vm-prechod-1 / -2 / -3Gradient stops (#rrggbb) when the background is a gradient.
--vm-uholGradient angle, e.g. 160deg.
--vm-prekrytie-pozadiaOpacity (0–0.9) of the background colour over the background photo.
--vm-rozostrenieBlur of the background photo, e.g. 6px.
--vm-prekrytie-banneruDarkening (0–0.9) of the cover image.
--vm-font-nadpisFont stack of the venue name and category titles.
--vm-pohyb-cas / --vm-pohyb-posun / --vm-pohyb-mierkaEntrance duration, distance and start scale (set by the intensity).
--vm-postupneDelay between items entering together (stagger).
--vm-stlacenieScale of a pressed button, e.g. 0.94.
--vm-variant-bg / -ink / -okrajItem variant picker (.vm-vyber) and the read-only variant list: background, text and border of the trigger and the chips.
--vm-variant-rCorner radius of the picker trigger (and of the chips when data-varianty="stitky").
--vm-variant-bublina / --vm-variant-bublina-inkColour of the bubble behind the chosen variant (and the dot in the trigger), and the text on it — keep them 4.5:1 apart.
--vm-foto-tvarShape of every dish thumbnail (menu, cart, widget) as a mask-image value: a squircle, rounded square or circle SVG, or none for square. Use mask: var(--vm-foto-tvar) center / 100% 100% no-repeat (plus -webkit-mask for older Safari).
--vm-foto-rApproximate corner radius of the same shape, for borders, shadows and focus rings the mask does not clip.
--vm-foto-velkostEdge of a dish thumbnail: 64px (S), 88px (M) or 112px (L).
--vm-foto-pomerAspect ratio of the photo on top of a card, e.g. 3 / 2.
--vm-variant-casDuration of the chips popping in and of the selection bubble, e.g. 320ms (no motion with reduced motion).
--vm-stlpceMost category columns side by side (2 or 3, from data-stlpce). Fewer appear when they would be narrower than --vm-stlpec-min; phones always get one.
--vm-stlpec-minNarrowest category column before the menu stacks them: 16rem, 20rem or 24rem.
--vm-medzera-stlpcovGap between category columns, e.g. 24px.

Dish photos

A photo is optional per dish and only dishes that have one get it; there is never an empty placeholder. The owner picks the look under Appearance: where the photo sits (data-foto-rozlozenie: vpravo by default, vlavo, hore for a photo on top of the card, or skryte), its shape (data-foto-tvar: squircle by default, zaoblene, kruh, stvorec) and size (data-foto-velkost: male 64 px, stredne 88 px, velke 112 px). Every dish thumbnail in the menu and the cart is drawn from the same --vm-foto-* variables, so a custom stylesheet can reuse the shape:

.my-thumb {
  width: var(--vm-foto-velkost);
  aspect-ratio: 1;
  object-fit: cover;
  -webkit-mask: var(--vm-foto-tvar) center / 100% 100% no-repeat;
  mask: var(--vm-foto-tvar) center / 100% 100% no-repeat;
}

Items with a photo carry data-fotka="ano" on .vm-polozka; the photo is .vm-polozka__obraz > img.vm-polozka__fotka with the dish name as its alt. The same choices reach integrations as venue.theme.photos in GET /api/v1/menu.

Variant picker

Items sold in several sizes or options (Kofola 0.3 l / 0.5 l) show the lowest price as from €1.80 and a picker under the description. The first available variant is preselected, so one tap on Add still works; the trigger always shows what will be added. Two sizes of one item are two lines in the cart. The picker is a radio group: one Tab stop, arrow keys move (skipping sold-out variants), Home/End jump to the ends, Enter or Space confirms, Escape closes and returns focus to the trigger.

HookWhat it is
.vm-vyberThe picker. data-rezim="rozbalovac" on the menu and in the widget (a trigger that opens the chips), "rozbaleny" on the kiosk (chips always visible); data-otvoreny="ano" | "nie".
.vm-vyber__spustacThe trigger: always shows the variant that Add will add (0.5 l · €2.50), aria-expanded.
.vm-vyber__bublinyrole="radiogroup" holding the chips.
.vm-bublinaOne variant, role="radio"; the chosen one has aria-checked="true", a sold-out one data-vypredana="ano" (shown, never selectable).
.vm-bublina__pocetHow many of that variant are already in the cart.
.vm-varianty-zoznam__polozkaRead-only list of variants with prices, where the menu takes no orders.

Colours, the trigger's radius and the timing come from the --vm-variant-* variables above; data-varianty="stitky" on .vm-koren turns the round pills into tags. The chips pop in one after another with a small spring and the chosen one fills with a bubble — transform and opacity only, and nothing moves for visitors who ask for reduced motion.

Allergens, the “sold out” label and the footer cannot be hidden or faded by a custom stylesheet — the guest must always see them.

Events

Two custom events bubble from the container. vittles:ready fires once the frame has rendered; vittles:order fires when a guest's order is accepted, with detail = { number, total_cents, fulfillment } — the order number the guest sees, the total in cents and table | pickup | delivery. No order id and nothing about the guest ever leaves the frame.

document.addEventListener("vittles:ready", () => console.log("menu is up"));
document.addEventListener("vittles:order", (e) => {
  // e.detail = { number, total_cents, fulfillment } — no id, no guest data
  console.log("order", e.detail.number, e.detail.total_cents / 100, e.detail.fulfillment);
});

The frame also adds a floating cart bar at the bottom of your page while the guest has items in the cart (it is drawn in your document so it can stay on screen; the frame itself grows with its content and never scrolls). Tapping it expands the cart inside the frame and scrolls to it.

Content Security Policy

If your site sends a CSP, allow the Vittles origin for the script and for frames. Nothing else is requested from your page — the frame talks to Vittles from inside itself.

Content-Security-Policy: script-src 'self' https://vittles.eu; frame-src https://vittles.eu;

The frame is created with sandbox="allow-scripts allow-same-origin allow-forms allow-popups allow-popups-to-escape-sandbox" and referrerpolicy="strict-origin": it cannot navigate your page, and Vittles only ever sees your origin, never the full URL.

Without a script (iframe)

Site builders that do not allow scripts (Wix, Squarespace and similar) can embed the frame directly. The cart is then inline at the end of the menu and the height is yours to set; the query parameters are the ones from the attributes table (lang, table, k, mode, font, primary, background, text, surface, header, layout, photos, corners, density, align, width, columns, motion, coupon). Keep referrerpolicy="strict-origin" — without a referrer the frame cannot tell which site is asking and redirects to the hosted menu.

<iframe src="https://vittles.eu/embed/pizzeria-roma?lang=en"
        referrerpolicy="strict-origin" loading="lazy"
        style="width:100%;height:900px;border:0"></iframe>

Troubleshooting

“This site is not on the allowed list.” The frame shows the origin it received; the owner adds exactly that string under Channels → Widget → Allowed websites. Wix and some other editors embed custom HTML from a sandbox on another host (for example https://…filesusr.com) — that sandbox origin is the one to add, not the site's own address. The change reaches open frames within a minute.

A link to the menu instead of the widget. The frame did not report ready within 10 seconds: the slug is wrong, your CSP blocks frame-src, or Vittles is unreachable. The console of your page shows [vittles] <slug>: no ready in 10 s — check the slug and your frame-src first.

The menu shows, but there is no cart. Either data-mode="menu" is set, or the owner has online ordering switched off or paused — the frame says so in a sentence under the opening hours. An attribute cannot override the owner.

Two scrollbars. Do not set a fixed height on the container when using the script; the frame reports its height and the container should let it grow. A fixed height is only for the plain iframe variant.