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.
| Attribute | Values | Default | What it does |
|---|---|---|---|
data-vittles-venuerequired | slug | — | The venue. The slug from /menu/{slug}. |
data-mode | auto | menu | auto | menu 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-table | table 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-param | query parameter name | — | Read the table label from the host page's own URL, e.g. data-table-param="table" with ?table=12. |
data-k | table 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-param | query parameter name | — | Read the table signature from the host page's own URL, e.g. data-k-param="k" with ?table=12&k=…. |
data-lang | sk | en | venue language | Fixed language; the language switcher inside the frame is hidden. |
data-font | default | humanist | serif | slab | venue font | One 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 | #rrggbb | venue colours | Brand colour for buttons. Also readable as CSS --vittles-primary on the container. |
data-background | #rrggbb | venue colours | Page background. Also --vittles-background. |
data-text | #rrggbb | venue colours | Text colour. Also --vittles-text. |
data-surface | #rrggbb | derived | Card and field background. Also --vittles-surface. Optional. |
data-header | true | false | true | false hides the venue name, subtitle, logo and cover image. Opening hours and the ordering notice stay. |
data-layout | list | grid | venue setting | Items as a list, or as tiles (tiles need photos on). |
data-photos | none | small | large | venue setting | Item photos: hidden, small beside the text, or large above it. |
data-corners | square | soft | round | venue setting | Corner rounding of photos, tiles and the cover image. |
data-density | roomy | compact | venue setting | Spacing between items. Text size never changes. |
data-align | center | left | venue setting | Header alignment — left for sites with a left-aligned layout. |
data-width | fill | wide | reading | venue layout, otherwise fill | Menu width inside the frame: fill uses the whole frame, wide up to 72rem, reading a single comfortable column. |
data-columns | auto | 1 | 2 | 3 | auto | At most this many columns. Only narrows: below the cap the venue's menu layout decides, and a narrow frame stacks columns anyway. |
data-min-height | px | 480 | Frame 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-coupon | coupon code | — | Pre-fills and applies a coupon code (3–32 letters, digits, _ or -). Validity is decided by Vittles, not by the snippet. |
data-motion | auto | off | auto | off 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-param | query 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
| Attribute | Values |
|---|---|
data-rozlozenie | zoznam | dlazdice |
data-foto-rozlozenie | vlavo | vpravo | hore | skryte |
data-foto-tvar | squircle | zaoblene | kruh | stvorec |
data-foto-velkost | male | stredne | velke |
data-fotky | skryte | male | velke |
data-hustota | vzdusne | kompaktne |
data-zaoblenie | ostre | mierne | oble |
data-hlavicka | stred | vlavo |
data-banner | nizka | stredna | vysoka |
data-hlas | v1 | ziadny | klasicky | tichy | makky | zivy | svizny | hravy | filmovy | elegantny | pruzny | vlna |
data-kontext | stranka | vlozenie |
data-sirka | citatelna | siroka | plna |
data-podoba | v2 |
data-pozadie | prechod | obrazok | vzor |
data-vzor | bodky | mriezka | vlny |
data-stlpce | dva | tri |
data-stlpec | uzka | siroka |
data-kat-karta · data-cislovanie | ano |
data-kat-menovka | stitok |
data-karty | obrys | tien | sklo |
data-cena | stitok |
data-varianty | stitky |
data-logo | male | velke |
data-logo-tvar | zaobleny | kruh |
data-jazyk | text |
data-denne | obrys | skryte |
data-vstup | ziadny | prelinanie | posun | zvacsenie |
data-hover | zdvih | priblizenie |
data-lista | plna | priesvitna |
data-intenzita | jemna | silna |
data-banner-text · data-paralaxa · data-prekrytie · data-postupne · data-zvyraznenie | ano |
data-stlacenie | nie |
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
| Variable | What it carries |
|---|---|
--vt-bg | page background |
--vt-povrch | card and field background |
--vt-ink | text |
--vt-ink-2 | secondary text (descriptions, allergens) |
--vt-okraj | hairlines between items |
--vt-okraj-silny | stronger lines (category headings, dotted leaders) |
--vt-akcent | buttons and badges — the accent colour, or the brand colour when the venue has no accent |
--vt-akcent-ink | text on buttons and badges |
--vt-akcent-text | brand colour used as text (links, totals) |
--vt-font-ui | the venue font family |
Style variables (--vm-*)
| Variable | What it carries |
|---|---|
--vm-prechod-1 / -2 / -3 | Gradient stops (#rrggbb) when the background is a gradient. |
--vm-uhol | Gradient angle, e.g. 160deg. |
--vm-prekrytie-pozadia | Opacity (0–0.9) of the background colour over the background photo. |
--vm-rozostrenie | Blur of the background photo, e.g. 6px. |
--vm-prekrytie-banneru | Darkening (0–0.9) of the cover image. |
--vm-font-nadpis | Font stack of the venue name and category titles. |
--vm-pohyb-cas / --vm-pohyb-posun / --vm-pohyb-mierka | Entrance duration, distance and start scale (set by the intensity). |
--vm-postupne | Delay between items entering together (stagger). |
--vm-stlacenie | Scale of a pressed button, e.g. 0.94. |
--vm-variant-bg / -ink / -okraj | Item variant picker (.vm-vyber) and the read-only variant list: background, text and border of the trigger and the chips. |
--vm-variant-r | Corner radius of the picker trigger (and of the chips when data-varianty="stitky"). |
--vm-variant-bublina / --vm-variant-bublina-ink | Colour 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-tvar | Shape 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-r | Approximate corner radius of the same shape, for borders, shadows and focus rings the mask does not clip. |
--vm-foto-velkost | Edge of a dish thumbnail: 64px (S), 88px (M) or 112px (L). |
--vm-foto-pomer | Aspect ratio of the photo on top of a card, e.g. 3 / 2. |
--vm-variant-cas | Duration of the chips popping in and of the selection bubble, e.g. 320ms (no motion with reduced motion). |
--vm-stlpce | Most 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-min | Narrowest category column before the menu stacks them: 16rem, 20rem or 24rem. |
--vm-medzera-stlpcov | Gap 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.
| Hook | What it is |
|---|---|
.vm-vyber | The 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__spustac | The trigger: always shows the variant that Add will add (0.5 l · €2.50), aria-expanded. |
.vm-vyber__bubliny | role="radiogroup" holding the chips. |
.vm-bublina | One variant, role="radio"; the chosen one has aria-checked="true", a sold-out one data-vypredana="ano" (shown, never selectable). |
.vm-bublina__pocet | How many of that variant are already in the cart. |
.vm-varianty-zoznam__polozka | Read-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.